import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:PiliPlus/build_config.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:crypto/crypto.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:share_plus/share_plus.dart'; import 'package:path/path.dart' as path; import 'package:uuid/v4.dart'; import '../models/home/rcmd/result.dart'; import '../models/model_rec_video_item.dart'; import '../models/model_video.dart'; class Utils { static final Random random = Random(); static const channel = MethodChannel("PiliPlus"); static final _numRegExp = RegExp(r'([\d\.]+)([千万亿])?'); static final regExp = RegExp(r'(@(\d+[a-z]_?)*)(\..*)?$', caseSensitive: false); static String thumbnailImgUrl(String? src, [int? quality]) { if (src != null && quality != 100) { bool hasMatch = false; src = src.splitMapJoin( regExp, onMatch: (Match match) { hasMatch = true; String suffix = match.group(3) ?? '.webp'; return '${match.group(1)}_${quality ?? GlobalData().imgQuality}q$suffix'; }, onNonMatch: (String str) { return str; }, ); if (hasMatch.not) { src += '@${quality ?? GlobalData().imgQuality}q.webp'; } } return src.http2https; } static bool? _isIpad; static FutureOr isIpad() async { if (Platform.isIOS.not) { return false; } if (_isIpad != null) { return _isIpad!; } DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); IosDeviceInfo info = await deviceInfo.iosInfo; _isIpad = info.model.toLowerCase().contains("ipad"); return _isIpad!; } static void shareText(String text) async { try { Rect? sharePositionOrigin; if (await isIpad()) { sharePositionOrigin = Rect.fromLTWH(0, 0, Get.width, Get.height / 2); } Share.share( text, sharePositionOrigin: sharePositionOrigin, ); } catch (e) { SmartDialog.showToast(e.toString()); } } static String buildShadersAbsolutePath( String baseDirectory, List shaders) { List absolutePaths = shaders.map((shader) { return path.join(baseDirectory, shader); }).toList(); return absolutePaths.join(':'); } static void showCopyTextDialog(text) { Get.dialog( AlertDialog( content: SelectableText('$text'), ), ); } static bool isStringNumeric(str) { RegExp numericRegex = RegExp(r'^[\d\.]+$'); return numericRegex.hasMatch(str.toString()); } static bool isDefaultFav(int attr) { return (attr & 2) == 0; } static String isPublicFavText(int attr) { return isPublicFav(attr) ? '公开' : '私密'; } static bool isPublicFav(int attr) { return (attr & 1) == 0; } static String generateRandomString(int length) { const characters = '0123456789abcdefghijklmnopqrstuvwxyz'; return String.fromCharCodes(Iterable.generate(length, (_) => characters.codeUnitAt(random.nextInt(characters.length)))); } static String genAuroraEid(int uid) { if (uid == 0) { return ''; // Return null for a UID of 0 } // 1. Convert UID to a byte array. var midByte = utf8.encode(uid.toString()); // 2. XOR each byte with the corresponding byte from the key. const key = 'ad1va46a7lza'; for (int i = 0; i < midByte.length; i++) { midByte[i] ^= key.codeUnitAt(i % key.length); } // 3. Perform Base64 encoding without padding. String base64Encoded = base64.encode(midByte).replaceAll('=', ''); // Remove padding // Return the resulting x-bili-aurora-eid. return base64Encoded; } static String genTraceId() { String randomId = generateRandomString(32); StringBuffer randomTraceId = StringBuffer(randomId.substring(0, 24)); int ts = DateTime.now().millisecondsSinceEpoch ~/ 1000; for (int i = 2; i >= 0; i--) { ts >>= 8; randomTraceId.write((ts & 0xFF).toRadixString(16).padLeft(2, '0')); } randomTraceId.write(randomId.substring(30, 32)); return '${randomTraceId.toString()}:${randomTraceId.toString().substring(16, 32)}:0:0'; } static Future copyText( String text, { bool needToast = true, String? toastText, }) async { Clipboard.setData(ClipboardData(text: text)); if (needToast) { SmartDialog.showToast(toastText ?? '已复制'); } } // static Future getCookiePath() async { // final Directory tempDir = await getApplicationSupportDirectory(); // final String tempPath = "${tempDir.path}/.plpl/"; // final Directory dir = Directory(tempPath); // final bool b = await dir.exists(); // if (!b) { // dir.createSync(recursive: true); // } // return tempPath; // } static int getUnit(String? unit) { switch (unit) { case '千': return 1000; case '万': return 10000; case '亿': return 100000000; default: return 1; } } static int parseNum(String numberStr) { if (numberStr == '-') return 0; try { final match = _numRegExp.firstMatch(numberStr)!; var number = double.parse(match.group(1)!); number *= getUnit(match.group(2)); return number.toInt(); } catch (e) { debugPrint('parse failed: "$numberStr" : $e'); return 0; } } static String numFormat(dynamic number) { if (number == null) { return '00:00'; } if (number is String) { number = int.tryParse(number) ?? number; if (number is String) { return number; } } String format(first, second) { double result = ((number / first) as double).toPrecision(1); int intRes = result.toInt(); if (result == intRes) { return '$intRes$second'; } else { return '$result$second'; } } if (number >= 100000000) { return format(100000000, '亿'); } else if (number >= 10000) { return format(10000, '万'); } else { return number.toString(); } } static String durationReadFormat(String duration) { List durationParts = duration.split(':'); if (durationParts.length == 3) { if (durationParts[0] != '00') { return '${int.parse(durationParts[0])}小时${durationParts[1]}分钟${durationParts[2]}秒'; } durationParts.removeAt(0); } if (durationParts.length == 2) { if (durationParts[0] != '00') { return '${int.parse(durationParts[0])}分钟${durationParts[1]}秒'; } durationParts.removeAt(0); } return '${int.parse(durationParts[0])}秒'; } static String videoItemSemantics(BaseVideoItemModel videoItem) { StringBuffer semanticsLabel = StringBuffer(); bool emptyStatCheck(int? stat) { return stat == null || stat <= 0; } if (videoItem is RecVideoItemAppModel) { if (videoItem.goto == 'picture') { semanticsLabel.write('动态,'); } else if (videoItem.goto == 'bangumi') { semanticsLabel.write('番剧,'); } } semanticsLabel.write(videoItem.title); if (!emptyStatCheck(videoItem.stat.view)) { semanticsLabel.write(',${Utils.numFormat(videoItem.stat.view)}'); semanticsLabel.write( (videoItem is RecVideoItemAppModel && videoItem.goto == 'picture') ? '浏览' : '播放'); } if (!emptyStatCheck(videoItem.stat.danmu)) { semanticsLabel.write(',${Utils.numFormat(videoItem.stat.danmu)}弹幕'); } if ((videoItem is BaseRecVideoItemModel) && videoItem.rcmdReason != null) { semanticsLabel.write(',${videoItem.rcmdReason}'); } if (!emptyStatCheck(videoItem.duration) && videoItem.duration > 0) { semanticsLabel.write( ',时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}'); } if (videoItem.pubdate != null) { semanticsLabel .write(',${Utils.dateFormat(videoItem.pubdate!, formatType: 'day')}'); } if (videoItem.owner.name != '') { semanticsLabel.write(',Up主:${videoItem.owner.name}'); } if (videoItem is BaseRecVideoItemModel && videoItem.isFollowed) { semanticsLabel.write(',已关注'); } return semanticsLabel.toString(); } static String timeFormat(dynamic time) { if (time is String && time.contains(':')) { return time; } if (time == null || time == 0) { return '00:00'; } return formatDuration(time); } static String shortenChineseDateString(String date) { return date.contains("年") ? RegExp(r'\d+') .allMatches(date) .map((match) => match.group(0)?.length == 4 ? match.group(0)!.substring(2) : match.group(0)) .join('-') : date; // if (date.contains("年")) return '${date.split("年").first}年'; // return date; } // 完全相对时间显示 static String formatTimestampToRelativeTime(timeStamp) { var difference = DateTime.now() .difference(DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000)); if (difference.inDays > 365) { return '${difference.inDays ~/ 365}年前'; } else if (difference.inDays > 30) { return '${difference.inDays ~/ 30}个月前'; } else if (difference.inDays > 0) { return '${difference.inDays}天前'; } else if (difference.inHours > 0) { return '${difference.inHours}小时前'; } else if (difference.inMinutes > 0) { return '${difference.inMinutes}分钟前'; } else { return '刚刚'; } } // 时间显示,刚刚,x分钟前 static String dateFormat(timeStamp, {formatType = 'list'}) { if (timeStamp == null || timeStamp == 0 || timeStamp == '') { return ''; } // 当前时间 int time = (DateTime.now().millisecondsSinceEpoch / 1000).round(); // 对比 int distance = (time - timeStamp).toInt(); // 当前年日期 String currentYearStr = 'MM月DD日 hh:mm'; String lastYearStr = 'YY年MM月DD日 hh:mm'; if (formatType == 'detail') { currentYearStr = 'MM-DD hh:mm'; lastYearStr = 'YY-MM-DD hh:mm'; return customStampStr( timestamp: timeStamp, date: lastYearStr, toInt: false); } else if (formatType == 'day') { if (distance <= 43200) { return customStampStr( timestamp: timeStamp, date: 'hh:mm', toInt: true, ); } return customStampStr( timestamp: timeStamp, date: 'YY-MM-DD', toInt: true, ); } if (distance <= 60) { return '刚刚'; } else if (distance <= 3600) { return '${(distance / 60).floor()}分钟前'; } else if (distance <= 43200) { return '${(distance / 60 / 60).floor()}小时前'; } else if (DateTime.fromMillisecondsSinceEpoch(time * 1000).year == DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000).year) { return customStampStr( timestamp: timeStamp, date: currentYearStr, toInt: false); } else { return customStampStr( timestamp: timeStamp, date: lastYearStr, toInt: false); } } // 时间戳转时间 static String customStampStr({ int? timestamp, // 为空则显示当前时间 String? date, // 显示格式,比如:'YY年MM月DD日 hh:mm:ss' bool toInt = true, // 去除0开头 }) { timestamp ??= (DateTime.now().millisecondsSinceEpoch / 1000).round(); String timeStr = (DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)).toString(); dynamic dateArr = timeStr.split(' ')[0]; dynamic timeArr = timeStr.split(' ')[1]; // ignore: non_constant_identifier_names String YY = dateArr.split('-')[0]; // ignore: non_constant_identifier_names String MM = dateArr.split('-')[1]; // ignore: non_constant_identifier_names String DD = dateArr.split('-')[2]; String hh = timeArr.split(':')[0]; String mm = timeArr.split(':')[1]; String ss = timeArr.split(':')[2]; ss = ss.split('.')[0]; // 去除0开头 if (toInt) { MM = (int.parse(MM)).toString(); DD = (int.parse(DD)).toString(); hh = (int.parse(hh)).toString(); // mm = (int.parse(mm)).toString(); } if (date == null) { return timeStr; } date = date .replaceAll('YY', YY) .replaceAll('MM', MM) .replaceAll('DD', DD) .replaceAll('hh', hh) .replaceAll('mm', mm) .replaceAll('ss', ss); // if (int.parse(YY) == DateTime.now().year && // int.parse(MM) == DateTime.now().month) { // // 当天 // if (int.parse(DD) == DateTime.now().day) { // return '今天'; // } // } return date; } static String makeHeroTag(v) { return v.toString() + random.nextInt(9999).toString(); } static String formatDuration(num seconds) { int hours = seconds ~/ 3600; int minutes = (seconds % 3600) ~/ 60; num remainingSeconds = seconds % 60; if (remainingSeconds is double) { remainingSeconds = remainingSeconds.toPrecision(3); } String minutesStr = minutes.toString().padLeft(2, '0'); String secondsStr = remainingSeconds.toString().padLeft(2, '0'); if (hours > 0) { String hoursStr = hours.toString().padLeft(2, '0'); return "$hoursStr:$minutesStr:$secondsStr"; } else { return "$minutesStr:$secondsStr"; } } static int duration(String duration) { List timeList = duration.split(':'); int len = timeList.length; if (len == 2) { return int.parse(timeList[0]) * 60 + int.parse(timeList[1]); } if (len == 3) { return int.parse(timeList[0]) * 3600 + int.parse(timeList[1]) * 60 + int.parse(timeList[2]); } return 0; } static int findClosestNumber(int target, List numbers) { List filterNums = numbers.where((number) => number <= target).toList(); return filterNums.isNotEmpty ? filterNums.reduce((a, b) => a > b ? a : b) : numbers.reduce((a, b) => a > b ? b : a); } // 检查更新 static Future checkUpdate([bool isAuto = true]) async { if (BuildConfig.isDebug) return; SmartDialog.dismiss(); try { dynamic res = await Request().get(Api.latestApp, extra: {'ua': 'mob'}); if (res.data.isEmpty) { if (isAuto.not) { SmartDialog.showToast('检查更新失败,GitHub接口未返回数据,请检查网络'); } return; } DateTime latest = DateTime.parse(res.data[0]['created_at']); DateTime current = DateTime.parse('${BuildConfig.buildTime}Z'); current = current.copyWith(hour: current.hour - 8); if (current.compareTo(latest) >= 0) { if (isAuto.not) { SmartDialog.showToast('已是最新版本'); } } else { SmartDialog.show( animationType: SmartAnimationType.centerFade_otherSlide, builder: (context) { return AlertDialog( title: const Text('🎉 发现新版本 '), content: SizedBox( height: 280, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${res.data[0]['tag_name']}', style: const TextStyle(fontSize: 20), ), const SizedBox(height: 8), Text('${res.data[0]['body']}'), TextButton( onPressed: () { PageUtils.launchURL( 'https://github.com/bggRGjQaUbCoE/PiliPlus/commits/main'); }, child: Text( "点此查看完整更新(即commit)内容", style: TextStyle( color: Theme.of(context).colorScheme.primary, ), ), ), ], ), ), ), actions: [ TextButton( onPressed: () { SmartDialog.dismiss(); GStorage.setting.put(SettingBoxKey.autoUpdate, false); }, child: Text( '不再提醒', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: SmartDialog.dismiss, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () { onDownload(res.data[0]); }, child: const Text('Github'), ), ], ); }, ); } } catch (e) { debugPrint('failed to check update: $e'); } } // 下载适用于当前系统的安装包 static Future onDownload(data) async { await SmartDialog.dismiss(); try { void download(plat) { if (data['assets'].isNotEmpty) { for (dynamic i in data['assets']) { if (i['name'].contains(plat)) { PageUtils.launchURL(i['browser_download_url']); break; } } } } if (Platform.isAndroid) { // 获取设备信息 AndroidDeviceInfo androidInfo = await DeviceInfoPlugin().androidInfo; // [arm64-v8a] download(androidInfo.supportedAbis.first); } else { download('ios'); } } catch (_) { PageUtils.launchURL( 'https://github.com/bggRGjQaUbCoE/PiliPlus/releases/latest'); } } static void appSign(Map params, [String appkey = Constants.appKey, String appsec = Constants.appSec]) { params['appkey'] = appkey; var searchParams = Uri( queryParameters: params.map((key, value) => MapEntry(key, value.toString()))).query; var sortedQueryString = (searchParams.split('&')..sort()).join('&'); params['sign'] = md5 .convert(utf8.encode(sortedQueryString + appsec)) .toString(); // 获取MD5哈希值 } static List generateRandomBytes(int minLength, int maxLength) { return List.generate( minLength + random.nextInt(maxLength - minLength + 1), (_) => 0x26 + random.nextInt(0x59), // dm_img_str不能有`%` ); } static String base64EncodeRandomString(int minLength, int maxLength) { final randomBytes = generateRandomBytes(minLength, maxLength); final randomBase64 = base64.encode(randomBytes); return randomBase64.substring(0, randomBase64.length - 2); } static String getFileName(String uri, {bool fileExt = true}) { final i0 = uri.lastIndexOf('/') + 1; final i1 = fileExt ? uri.length : uri.lastIndexOf('.'); return uri.substring(i0, i1); } static String genBuvid3() { return '${const UuidV4().generate().toUpperCase()}${random.nextInt(100000).toString().padLeft(5, "0")}infoc'; } }