diff --git a/.gitignore b/.gitignore index 7bc3f845..55e2eaf3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,29 @@ migrate_working_dir/ # is commented out by default. #.vscode/ +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/api_docs.zip +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id @@ -31,15 +54,83 @@ migrate_working_dir/ .pub-cache/ .pub/ /build/ - -# Symbolication related -app.*.symbols +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds # Obfuscation related app.*.map.json -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release -/*/flutter/[Gg]enerated*[Pp]lugin* +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock +!.vscode/settings.json \ No newline at end of file diff --git a/change_log/1.0.17.0125.md b/change_log/1.0.17.0125.md new file mode 100644 index 00000000..dc8bcb62 --- /dev/null +++ b/change_log/1.0.17.0125.md @@ -0,0 +1,39 @@ +## 1.0.17 + + +### 功能 ++ 视频全屏时隐藏进度条 ++ 动态内容增加投稿跳转 ++ 未开启自动播放时点击封面播放 ++ 弹幕发送标识 ++ 定时关闭 ++ 推荐视频卡片拉黑up功能 ++ 首页tabbar编辑排序 + +### 修复 ++ 连续跳转搜索页未刷新 ++ 搜索结果为空时页面异常 ++ 评论区链接解析 ++ 视频全屏状态栏背景色 ++ 私信对话气泡位置 ++ 设置up关注分组样式 ++ 每次推荐请求数据相同 ++ iOS代理网络异常 ++ 双击切换播放状态无声 ++ 设置自定义倍速白屏 ++ 免登录查看1080p + +### 优化 ++ 首页web端推荐观看数展示 ++ 首页web端推荐接口更新 ++ 首页样式 ++ 搜索页跳转 ++ 弹幕资源优化 ++ 图片渲染占用内存优化(部分) ++ 两次返回退出应用 ++ schame 补充 + + + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 0a8bb028..03adc166 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/utils/extension.dart'; import '../../utils/storage.dart'; import '../constants.dart'; @@ -12,55 +13,75 @@ class NetworkImgLayer extends StatelessWidget { this.src, required this.width, required this.height, - this.cacheW, - this.cacheH, this.type, this.fadeOutDuration, this.fadeInDuration, // 图片质量 默认1% this.quality, + this.origAspectRatio, }); final String? src; - final double? width; - final double? height; - final double? cacheW; - final double? cacheH; + final double width; + final double height; final String? type; final Duration? fadeOutDuration; final Duration? fadeInDuration; final int? quality; + final double? origAspectRatio; @override Widget build(BuildContext context) { - final double pr = MediaQuery.of(context).devicePixelRatio; - final int picQuality = - setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10) as int; + final String imageUrl = + '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? 100}q.webp'; + int? memCacheWidth, memCacheHeight; + double aspectRatio = (width / height).toDouble(); - // double pr = 2; - return src != '' + void setMemCacheSizes() { + if (aspectRatio > 1) { + memCacheHeight = height.cacheSize(context); + } else if (aspectRatio < 1) { + memCacheWidth = width.cacheSize(context); + } else { + if (origAspectRatio != null && origAspectRatio! > 1) { + memCacheWidth = width.cacheSize(context); + } else if (origAspectRatio != null && origAspectRatio! < 1) { + memCacheHeight = height.cacheSize(context); + } else { + memCacheWidth = width.cacheSize(context); + memCacheHeight = height.cacheSize(context); + } + } + } + + setMemCacheSizes(); + + if (memCacheWidth == null && memCacheHeight == null) { + memCacheWidth = width.toInt(); + } + + return src != '' && src != null ? ClipRRect( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(type == 'avatar' - ? 50 - : type == 'emote' - ? 0 - : StyleString.imgRadius.x), + clipBehavior: Clip.antiAlias, + borderRadius: BorderRadius.circular( + type == 'avatar' + ? 50 + : type == 'emote' + ? 0 + : StyleString.imgRadius.x, + ), child: CachedNetworkImage( - imageUrl: - '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? picQuality}q.webp', - width: width ?? double.infinity, - height: height ?? double.infinity, - maxWidthDiskCache: ((cacheW ?? width!) * pr).toInt(), - // maxHeightDiskCache: (cacheH ?? height!).toInt(), - memCacheWidth: ((cacheW ?? width!) * pr).toInt(), - // memCacheHeight: (cacheH ?? height!).toInt(), + imageUrl: imageUrl, + width: width, + height: height, + memCacheWidth: memCacheWidth, + memCacheHeight: memCacheHeight, fit: BoxFit.cover, fadeOutDuration: - fadeOutDuration ?? const Duration(milliseconds: 200), + fadeOutDuration ?? const Duration(milliseconds: 120), fadeInDuration: - fadeInDuration ?? const Duration(milliseconds: 200), - // filterQuality: FilterQuality.high, + fadeInDuration ?? const Duration(milliseconds: 120), + filterQuality: FilterQuality.high, errorWidget: (BuildContext context, String url, Object error) => placeholder(context), placeholder: (BuildContext context, String url) => @@ -72,9 +93,9 @@ class NetworkImgLayer extends StatelessWidget { Widget placeholder(BuildContext context) { return Container( - width: width ?? double.infinity, - height: height ?? double.infinity, - clipBehavior: Clip.hardEdge, + width: width, + height: height, + clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4), borderRadius: BorderRadius.circular(type == 'avatar' @@ -84,13 +105,16 @@ class NetworkImgLayer extends StatelessWidget { : StyleString.imgRadius.x), ), child: Center( - child: Image.asset( - type == 'avatar' - ? 'assets/images/noface.jpeg' - : 'assets/images/loading.png', - width: 300, - height: 300, - )), + child: Image.asset( + type == 'avatar' + ? 'assets/images/noface.jpeg' + : 'assets/images/loading.png', + width: width, + height: height, + cacheWidth: width.cacheSize(context), + cacheHeight: height.cacheSize(context), + ), + ), ); } } diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 5d39bc65..c78643db 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -69,7 +69,7 @@ class VideoCardH extends StatelessWidget { final double width = (boxConstraints.maxWidth - StyleString.cardSpace * 6 / - MediaQuery.of(context).textScaleFactor) / + MediaQuery.textScalerOf(context).scale(1.0)) / 2; return Container( constraints: const BoxConstraints(minHeight: 88), diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index dfc7eb94..04950d5b 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -326,7 +326,8 @@ class VideoStat extends StatelessWidget { maxLines: 1, text: TextSpan( style: TextStyle( - fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + fontSize: MediaQuery.textScalerOf(context) + .scale(Theme.of(context).textTheme.labelSmall!.fontSize!), color: Theme.of(context).colorScheme.outline, ), children: [ diff --git a/lib/http/api.dart b/lib/http/api.dart index c8edf863..532ca341 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -185,7 +185,7 @@ class Api { static const String searchDefault = '/x/web-interface/wbi/search/default'; // 搜索关键词 - static const String serachSuggest = + static const String searchSuggest = 'https://s.search.bilibili.com/main/suggest'; // 分类搜索 @@ -467,4 +467,7 @@ class Api { /// page_size static const getSeasonDetailApi = '/x/polymer/web-space/seasons_archives_list'; + + /// 获取未读动态数 + static const getUnreadDynamic = '/x/web-interface/dynamic/entrance'; } diff --git a/lib/http/common.dart b/lib/http/common.dart new file mode 100644 index 00000000..d711a7e7 --- /dev/null +++ b/lib/http/common.dart @@ -0,0 +1,17 @@ +import 'index.dart'; + +class CommonHttp { + static Future unReadDynamic() async { + var res = await Request().get(Api.getUnreadDynamic, + data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']['dyn_basic_infos']}; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } +} diff --git a/lib/http/init.dart b/lib/http/init.dart index 7840499f..6244df04 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -8,6 +8,7 @@ import 'package:dio/io.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; // import 'package:dio_http2_adapter/dio_http2_adapter.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/utils/id_utils.dart'; import '../utils/storage.dart'; import '../utils/utils.dart'; import 'constants.dart'; @@ -77,10 +78,11 @@ class Request { static setOptionsHeaders(userInfo, bool status) { if (status) { dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); + dio.options.headers['x-bili-aurora-eid'] = + IdUtils.genAuroraEid(userInfo.mid); } dio.options.headers['env'] = 'prod'; dio.options.headers['app-key'] = 'android64'; - dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH'; dio.options.headers['x-bili-aurora-zone'] = 'sh001'; dio.options.headers['referer'] = 'https://www.bilibili.com/'; } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index 68f5742d..48ba7e60 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -70,14 +70,14 @@ class ApiInterceptor extends Interceptor { case DioExceptionType.sendTimeout: return '发送请求超时,请检查网络设置'; case DioExceptionType.unknown: - final String res = await checkConect(); + final String res = await checkConnect(); return '$res \n 网络异常,请稍后重试!'; // default: // return 'Dio异常'; } } - static Future checkConect() async { + static Future checkConnect() async { final ConnectivityResult connectivityResult = await Connectivity().checkConnectivity(); if (connectivityResult == ConnectivityResult.mobile) { diff --git a/lib/http/search.dart b/lib/http/search.dart index 7239f435..18481ea8 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -36,7 +36,7 @@ class SearchHttp { // 获取搜索建议 static Future searchSuggest({required term}) async { - var res = await Request().get(Api.serachSuggest, + var res = await Request().get(Api.searchSuggest, data: {'term': term, 'main_ver': 'v1', 'highlight': term}); if (res.data is String) { Map resultMap = json.decode(res.data); diff --git a/lib/main.dart b/lib/main.dart index 29b8d118..f834d5d2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -158,9 +158,8 @@ class MyApp extends StatelessWidget { return FlutterSmartDialog( toastBuilder: (String msg) => CustomToast(msg: msg), child: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: - MediaQuery.of(context).textScaleFactor * textScale), + data: MediaQuery.of(context) + .copyWith(textScaler: TextScaler.linear(textScale)), child: child!, ), ); diff --git a/lib/models/common/tab_type.dart b/lib/models/common/tab_type.dart index 90d19029..e530d7e5 100644 --- a/lib/models/common/tab_type.dart +++ b/lib/models/common/tab_type.dart @@ -9,6 +9,7 @@ enum TabType { live, rcmd, hot, bangumi } extension TabTypeDesc on TabType { String get description => ['直播', '推荐', '热门', '番剧'][index]; + String get id => ['live', 'rcmd', 'hot', 'bangumi'][index]; } List tabsConfig = [ diff --git a/lib/models/home/rcmd/result.dart b/lib/models/home/rcmd/result.dart index 4e810491..9363beb3 100644 --- a/lib/models/home/rcmd/result.dart +++ b/lib/models/home/rcmd/result.dart @@ -1,8 +1,3 @@ -import 'package:hive/hive.dart'; - -part 'result.g.dart'; - -@HiveType(typeId: 0) class RecVideoItemAppModel { RecVideoItemAppModel({ this.id, @@ -27,47 +22,27 @@ class RecVideoItemAppModel { this.adInfo, }); - @HiveField(0) int? id; - @HiveField(1) int? aid; - @HiveField(2) String? bvid; - @HiveField(3) int? cid; - @HiveField(4) String? pic; - @HiveField(5) RcmdStat? stat; - @HiveField(6) int? duration; - @HiveField(7) String? title; - @HiveField(8) int? isFollowed; - @HiveField(9) RcmdOwner? owner; - @HiveField(10) RcmdReason? rcmdReason; - @HiveField(11) String? goto; - @HiveField(12) int? param; - @HiveField(13) String? uri; - @HiveField(14) String? talkBack; // 番剧 - @HiveField(15) String? bangumiView; - @HiveField(16) String? bangumiFollow; - @HiveField(17) String? bangumiBadge; - @HiveField(18) String? cardType; - @HiveField(19) Map? adInfo; RecVideoItemAppModel.fromJson(Map json) { @@ -116,18 +91,14 @@ class RecVideoItemAppModel { } } -@HiveType(typeId: 1) class RcmdStat { RcmdStat({ this.view, this.like, this.danmu, }); - @HiveField(0) String? view; - @HiveField(1) String? like; - @HiveField(2) String? danmu; RcmdStat.fromJson(Map json) { @@ -136,13 +107,10 @@ class RcmdStat { } } -@HiveType(typeId: 2) class RcmdOwner { RcmdOwner({this.name, this.mid}); - @HiveField(0) String? name; - @HiveField(1) int? mid; RcmdOwner.fromJson(Map json) { @@ -155,13 +123,11 @@ class RcmdOwner { } } -@HiveType(typeId: 8) class RcmdReason { RcmdReason({ this.content, }); - @HiveField(0) String? content; RcmdReason.fromJson(Map json) { diff --git a/lib/models/home/rcmd/result.g.dart b/lib/models/home/rcmd/result.g.dart deleted file mode 100644 index f46886e8..00000000 --- a/lib/models/home/rcmd/result.g.dart +++ /dev/null @@ -1,209 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'result.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class RecVideoItemAppModelAdapter extends TypeAdapter { - @override - final int typeId = 0; - - @override - RecVideoItemAppModel read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return RecVideoItemAppModel( - id: fields[0] as int?, - aid: fields[1] as int?, - bvid: fields[2] as String?, - cid: fields[3] as int?, - pic: fields[4] as String?, - stat: fields[5] as RcmdStat?, - duration: fields[6] as int?, - title: fields[7] as String?, - isFollowed: fields[8] as int?, - owner: fields[9] as RcmdOwner?, - rcmdReason: fields[10] as RcmdReason?, - goto: fields[11] as String?, - param: fields[12] as int?, - uri: fields[13] as String?, - talkBack: fields[14] as String?, - bangumiView: fields[15] as String?, - bangumiFollow: fields[16] as String?, - bangumiBadge: fields[17] as String?, - cardType: fields[18] as String?, - adInfo: (fields[19] as Map?)?.cast(), - ); - } - - @override - void write(BinaryWriter writer, RecVideoItemAppModel obj) { - writer - ..writeByte(20) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.aid) - ..writeByte(2) - ..write(obj.bvid) - ..writeByte(3) - ..write(obj.cid) - ..writeByte(4) - ..write(obj.pic) - ..writeByte(5) - ..write(obj.stat) - ..writeByte(6) - ..write(obj.duration) - ..writeByte(7) - ..write(obj.title) - ..writeByte(8) - ..write(obj.isFollowed) - ..writeByte(9) - ..write(obj.owner) - ..writeByte(10) - ..write(obj.rcmdReason) - ..writeByte(11) - ..write(obj.goto) - ..writeByte(12) - ..write(obj.param) - ..writeByte(13) - ..write(obj.uri) - ..writeByte(14) - ..write(obj.talkBack) - ..writeByte(15) - ..write(obj.bangumiView) - ..writeByte(16) - ..write(obj.bangumiFollow) - ..writeByte(17) - ..write(obj.bangumiBadge) - ..writeByte(18) - ..write(obj.cardType) - ..writeByte(19) - ..write(obj.adInfo); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is RecVideoItemAppModelAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class RcmdStatAdapter extends TypeAdapter { - @override - final int typeId = 1; - - @override - RcmdStat read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return RcmdStat( - view: fields[0] as String?, - like: fields[1] as String?, - danmu: fields[2] as String?, - ); - } - - @override - void write(BinaryWriter writer, RcmdStat obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.view) - ..writeByte(1) - ..write(obj.like) - ..writeByte(2) - ..write(obj.danmu); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is RcmdStatAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class RcmdOwnerAdapter extends TypeAdapter { - @override - final int typeId = 2; - - @override - RcmdOwner read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return RcmdOwner( - name: fields[0] as String?, - mid: fields[1] as int?, - ); - } - - @override - void write(BinaryWriter writer, RcmdOwner obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.name) - ..writeByte(1) - ..write(obj.mid); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is RcmdOwnerAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class RcmdReasonAdapter extends TypeAdapter { - @override - final int typeId = 8; - - @override - RcmdReason read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return RcmdReason( - content: fields[0] as String?, - ); - } - - @override - void write(BinaryWriter writer, RcmdReason obj) { - writer - ..writeByte(1) - ..writeByte(0) - ..write(obj.content); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is RcmdReasonAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index 5acfb7f3..997adbfe 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -86,6 +86,14 @@ class _AboutPageState extends State { style: subTitleStyle, ), ), + ListTile( + onTap: () => _aboutController.webSiteUrl(), + title: const Text('访问官网'), + trailing: Text( + 'https://pilipalanet.mysxl.cn', + style: subTitleStyle, + ), + ), ListTile( onTap: () => _aboutController.panDownload(), title: const Text('网盘下载'), @@ -244,4 +252,12 @@ class AboutController extends GetxController { print(e); } } + + // 官网 + webSiteUrl() { + launchUrl( + Uri.parse('https://pilipalanet.mysxl.cn'), + mode: LaunchMode.externalApplication, + ); + } } diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index 9a101f16..560f09a5 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -224,7 +224,7 @@ class _BangumiPageState extends State // 列数 crossAxisCount: 3, mainAxisExtent: Get.size.width / 3 / 0.65 + - 32 * MediaQuery.of(context).textScaleFactor, + MediaQuery.textScalerOf(context).scale(32.0), ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index 680d21a2..eee4255c 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -1,5 +1,6 @@ // 内容 import 'package:flutter/material.dart'; +import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/pages/preview/index.dart'; @@ -48,6 +49,13 @@ class _ContentState extends State { WidgetSpan( child: LayoutBuilder( builder: (context, BoxConstraints box) { + double maxWidth = box.maxWidth.truncateToDouble(); + double maxHeight = box.maxWidth * 0.6; // 设置最大高度 + double height = maxWidth * + 0.5 * + (pictureItem.height != null && pictureItem.width != null + ? pictureItem.height! / pictureItem.width! + : 1); return GestureDetector( onTap: () { showDialog( @@ -58,18 +66,29 @@ class _ContentState extends State { }, ); }, - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: NetworkImgLayer( - src: pictureItem.url, + child: Container( + padding: const EdgeInsets.only(top: 4), + constraints: BoxConstraints(maxHeight: maxHeight), width: box.maxWidth / 2, - height: box.maxWidth * - 0.5 * - (pictureItem.height != null && pictureItem.width != null - ? pictureItem.height! / pictureItem.width! - : 1), - ), - ), + height: height, + child: Stack( + children: [ + Positioned.fill( + child: NetworkImgLayer( + src: pictureItem.url, + width: maxWidth / 2, + height: height, + ), + ), + height > maxHeight + ? const PBadge( + text: '长图', + right: 8, + bottom: 8, + ) + : const SizedBox(), + ], + )), ); }, ), @@ -83,6 +102,7 @@ class _ContentState extends State { list.add( LayoutBuilder( builder: (context, BoxConstraints box) { + double maxWidth = box.maxWidth.truncateToDouble(); return GestureDetector( onTap: () { showDialog( @@ -95,8 +115,10 @@ class _ContentState extends State { }, child: NetworkImgLayer( src: pics[i].url, - width: box.maxWidth, - height: box.maxWidth, + width: maxWidth, + height: maxWidth, + origAspectRatio: + pics[i].width!.toInt() / pics[i].height!.toInt(), ), ); }, @@ -107,7 +129,7 @@ class _ContentState extends State { WidgetSpan( child: LayoutBuilder( builder: (context, BoxConstraints box) { - double maxWidth = box.maxWidth; + double maxWidth = box.maxWidth.truncateToDouble(); double crossCount = len < 3 ? 2 : 3; double height = maxWidth / crossCount * diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 3a742519..e6fafba6 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -5,15 +5,17 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/common/tab_type.dart'; import 'package:pilipala/utils/storage.dart'; +import '../../http/index.dart'; class HomeController extends GetxController with GetTickerProviderStateMixin { bool flag = false; - late List tabs; + late RxList tabs = [].obs; RxInt initialIndex = 1.obs; late TabController tabController; late List tabsCtrList; late List tabsPageList; Box userInfoCache = GStrorage.userInfo; + Box settingStorage = GStrorage.setting; RxBool userLogin = false.obs; RxString userFace = ''.obs; var userInfo; @@ -21,6 +23,9 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { late final StreamController searchBarStream = StreamController.broadcast(); late bool hideSearchBar; + late List defaultTabs; + late List tabbarSort; + RxString defaultSearch = ''.obs; @override void onInit() { @@ -28,19 +33,13 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { userInfo = userInfoCache.get('userInfoCache'); userLogin.value = userInfo != null; userFace.value = userInfo != null ? userInfo.face : ''; - // 进行tabs配置 - tabs = tabsConfig; - tabsCtrList = tabsConfig.map((e) => e['ctr']).toList(); - tabsPageList = tabsConfig.map((e) => e['page']).toList(); - - tabController = TabController( - initialIndex: initialIndex.value, - length: tabs.length, - vsync: this, - ); + setTabConfig(); hideSearchBar = setting.get(SettingBoxKey.hideSearchBar, defaultValue: true); + if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) { + searchDefault(); + } } void onRefresh() { @@ -62,4 +61,49 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { if (val) return; userFace.value = userInfo != null ? userInfo.face : ''; } + + void setTabConfig() async { + defaultTabs = tabsConfig; + tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, + defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); + + tabs.value = defaultTabs + .where((i) => tabbarSort.contains((i['type'] as TabType).id)) + .toList(); + + if (tabbarSort.contains(TabType.rcmd.id)) { + initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id); + } else { + initialIndex.value = 0; + } + tabsCtrList = tabs.map((e) => e['ctr']).toList(); + tabsPageList = tabs.map((e) => e['page']).toList(); + + tabController = TabController( + initialIndex: initialIndex.value, + length: tabs.length, + vsync: this, + ); + // 监听 tabController 切换 + tabController.animation!.addListener(() { + if (tabController.indexIsChanging) { + if (initialIndex.value != tabController.index) { + initialIndex.value = tabController.index; + } + } else { + final int temp = tabController.animation!.value.round(); + if (initialIndex.value != temp) { + initialIndex.value = temp; + tabController.index = initialIndex.value; + } + } + }); + } + + void searchDefault() async { + var res = await Request().get(Api.searchDefault); + if (res.data['code'] == 0) { + defaultSearch.value = res.data['data']['name']; + } + } } diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 49982c7d..ce7b46c6 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/mine/index.dart'; -import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/utils/feed_back.dart'; import './controller.dart'; @@ -46,6 +46,13 @@ class _HomePageState extends State @override Widget build(BuildContext context) { super.build(context); + Brightness currentBrightness = MediaQuery.of(context).platformBrightness; + // 设置状态栏图标的亮度 + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarIconBrightness: currentBrightness == Brightness.light + ? Brightness.dark + : Brightness.light, + )); return Scaffold( extendBody: true, extendBodyBehindAppBar: true, @@ -82,7 +89,11 @@ class _HomePageState extends State ctr: _homeController, callback: showUserBottomSheet, ), - const CustomTabs(), + if (_homeController.tabs.length > 1) ...[ + const CustomTabs(), + ] else ...[ + const SizedBox(height: 6), + ], Expanded( child: TabBarView( controller: _homeController.tabController, @@ -129,9 +140,10 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { curve: Curves.easeInOutCubicEmphasized, duration: const Duration(milliseconds: 500), height: snapshot.data ? top + 52 : top, - padding: EdgeInsets.fromLTRB(14, top, 14, 0), + padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0), child: UserInfoWidget( top: top, + ctr: ctr, userLogin: isUserLoggedIn, userFace: ctr?.userFace.value, callback: () => callback!(), @@ -150,18 +162,20 @@ class UserInfoWidget extends StatelessWidget { required this.userLogin, required this.userFace, required this.callback, + required this.ctr, }) : super(key: key); final double top; final RxBool userLogin; final String? userFace; final VoidCallback? callback; + final HomeController? ctr; @override Widget build(BuildContext context) { return Row( children: [ - const SearchBar(), + SearchBar(ctr: ctr), if (userLogin.value) ...[ const SizedBox(width: 4), ClipRect( @@ -242,17 +256,6 @@ class CustomTabs extends StatefulWidget { class _CustomTabsState extends State { final HomeController _homeController = Get.put(HomeController()); - int currentTabIndex = 1; - - @override - void initState() { - super.initState(); - _homeController.tabController.addListener(listen); - } - - void listen() { - _homeController.initialIndex.value = _homeController.tabController.index; - } void onTap(int index) { feedBack(); @@ -263,34 +266,30 @@ class _CustomTabsState extends State { _homeController.tabController.index = index; } - @override - void dispose() { - super.dispose(); - _homeController.tabController.removeListener(listen); - } - @override Widget build(BuildContext context) { return Container( height: 44, margin: const EdgeInsets.only(top: 4), - child: ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 14.0), - scrollDirection: Axis.horizontal, - itemCount: _homeController.tabs.length, - separatorBuilder: (BuildContext context, int index) { - return const SizedBox(width: 10); - }, - itemBuilder: (BuildContext context, int index) { - String label = _homeController.tabs[index]['label']; - return Obx( - () => CustomChip( - onTap: () => onTap(index), - label: label, - selected: index == _homeController.initialIndex.value, - ), - ); - }, + child: Obx( + () => ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 14.0), + scrollDirection: Axis.horizontal, + itemCount: _homeController.tabs.length, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(width: 10); + }, + itemBuilder: (BuildContext context, int index) { + String label = _homeController.tabs[index]['label']; + return Obx( + () => CustomChip( + onTap: () => onTap(index), + label: label, + selected: index == _homeController.initialIndex.value, + ), + ); + }, + ), ), ); } @@ -338,11 +337,15 @@ class CustomChip extends StatelessWidget { } class SearchBar extends StatelessWidget { - const SearchBar({super.key}); + const SearchBar({ + Key? key, + required this.ctr, + }) : super(key: key); + + final HomeController? ctr; @override Widget build(BuildContext context) { - final SSearchController searchController = Get.put(SSearchController()); final ColorScheme colorScheme = Theme.of(context).colorScheme; return Expanded( child: Container( @@ -356,7 +359,10 @@ class SearchBar extends StatelessWidget { color: colorScheme.onSecondaryContainer.withOpacity(0.05), child: InkWell( splashColor: colorScheme.primaryContainer.withOpacity(0.3), - onTap: () => Get.toNamed('/search'), + onTap: () => Get.toNamed( + '/search', + parameters: {'hintText': ctr!.defaultSearch.value}, + ), child: Row( children: [ const SizedBox(width: 14), @@ -365,14 +371,12 @@ class SearchBar extends StatelessWidget { color: colorScheme.onSecondaryContainer, ), const SizedBox(width: 10), - Expanded( - child: Obx( - () => Text( - searchController.defaultSearch.value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: colorScheme.outline), - ), + Obx( + () => Text( + ctr!.defaultSearch.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: colorScheme.outline), ), ), ], diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 302d226d..f693acf1 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -162,8 +162,9 @@ class _LivePageState extends State crossAxisCount: crossAxisCount, mainAxisExtent: Get.size.width / crossAxisCount / StyleString.aspectRatio + - (crossAxisCount == 1 ? 48 : 68) * - MediaQuery.of(context).textScaleFactor, + MediaQuery.textScalerOf(context).scale( + (crossAxisCount == 1 ? 48 : 68), + ), ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 15962da4..a55c143e 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/http/common.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/media/index.dart'; @@ -27,6 +29,7 @@ class MainController extends GetxController { size: 21, ), 'label': "首页", + 'count': 0, }, { 'icon': const Icon( @@ -38,6 +41,7 @@ class MainController extends GetxController { size: 21, ), 'label': "动态", + 'count': 0, }, { 'icon': const Icon( @@ -49,6 +53,7 @@ class MainController extends GetxController { size: 21, ), 'label': "媒体库", + 'count': 0, } ].obs; final StreamController bottomBarStream = @@ -56,6 +61,10 @@ class MainController extends GetxController { Box setting = GStrorage.setting; DateTime? _lastPressedAt; late bool hideTabBar; + late PageController pageController; + int selectedIndex = 0; + Box userInfoCache = GStrorage.userInfo; + RxBool userLogin = false.obs; @override void onInit() { @@ -64,17 +73,47 @@ class MainController extends GetxController { Utils.checkUpdata(); } hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); + var userInfo = userInfoCache.get('userInfoCache'); + userLogin.value = userInfo != null; + getUnreadDynamic(); } - Future onBackPressed(BuildContext context) { + void onBackPressed(BuildContext context) { if (_lastPressedAt == null || DateTime.now().difference(_lastPressedAt!) > const Duration(seconds: 2)) { // 两次点击时间间隔超过2秒,重新记录时间戳 _lastPressedAt = DateTime.now(); + if (selectedIndex != 0) { + pageController.jumpTo(0); + } SmartDialog.showToast("再按一次退出Pili"); - return Future.value(false); // 不退出应用 + return; // 不退出应用 } - return Future.value(true); // 退出应用 + SystemNavigator.pop(); // 退出应用 + } + + void getUnreadDynamic() async { + if (!userLogin.value) { + return; + } + int dynamicItemIndex = + navigationBars.indexWhere((item) => item['label'] == "动态"); + var res = await CommonHttp.unReadDynamic(); + var data = res['data']; + if (dynamicItemIndex != -1) { + navigationBars[dynamicItemIndex]['count'] = + data == null ? 0 : data.length; // 修改 count 属性为新的值 + } + navigationBars.refresh(); + } + + void clearUnread() async { + int dynamicItemIndex = + navigationBars.indexWhere((item) => item['label'] == "动态"); + if (dynamicItemIndex != -1) { + navigationBars[dynamicItemIndex]['count'] = 0; // 修改 count 属性为新的值 + } + navigationBars.refresh(); } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index eb3444e1..5a570a8f 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -24,8 +24,6 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { final DynamicsController _dynamicController = Get.put(DynamicsController()); final MediaController _mediaController = Get.put(MediaController()); - PageController? _pageController; - int selectedIndex = 0; int? _lastSelectTime; //上次点击时间 Box setting = GStrorage.setting; late bool enableMYBar; @@ -34,13 +32,14 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { void initState() { super.initState(); _lastSelectTime = DateTime.now().millisecondsSinceEpoch; - _pageController = PageController(initialPage: selectedIndex); + _mainController.pageController = + PageController(initialPage: _mainController.selectedIndex); enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true); } void setIndex(int value) async { feedBack(); - _pageController!.jumpToPage(value); + _mainController.pageController.jumpToPage(value); var currentPage = _mainController.pages[value]; if (currentPage is HomePage) { if (_homeController.flag) { @@ -68,6 +67,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { _lastSelectTime = DateTime.now().millisecondsSinceEpoch; } _dynamicController.flag = true; + _mainController.clearUnread(); } else { _dynamicController.flag = false; } @@ -94,14 +94,17 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { localCache.put('sheetHeight', sheetHeight); localCache.put('statusBarHeight', statusBarHeight); return PopScope( - onPopInvoked: (bool status) => _mainController.onBackPressed(context), + canPop: false, + onPopInvoked: (bool didPop) async { + _mainController.onBackPressed(context); + }, child: Scaffold( extendBody: true, body: PageView( physics: const NeverScrollableScrollPhysics(), - controller: _pageController, + controller: _mainController.pageController, onPageChanged: (index) { - selectedIndex = index; + _mainController.selectedIndex = index; setState(() {}); }, children: _mainController.pages, @@ -116,36 +119,48 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { curve: Curves.easeInOutCubicEmphasized, duration: const Duration(milliseconds: 500), offset: Offset(0, snapshot.data ? 0 : 1), - child: enableMYBar - ? NavigationBar( - onDestinationSelected: (value) => setIndex(value), - selectedIndex: selectedIndex, - destinations: [ - ..._mainController.navigationBars.map((e) { - return NavigationDestination( - icon: e['icon'], - selectedIcon: e['selectIcon'], - label: e['label'], - ); - }).toList(), - ], - ) - : BottomNavigationBar( - currentIndex: selectedIndex, - onTap: (value) => setIndex(value), - iconSize: 16, - selectedFontSize: 12, - unselectedFontSize: 12, - items: [ - ..._mainController.navigationBars.map((e) { - return BottomNavigationBarItem( - icon: e['icon'], - activeIcon: e['selectIcon'], - label: e['label'], - ); - }).toList(), - ], - ), + child: Obx( + () => enableMYBar + ? NavigationBar( + onDestinationSelected: (value) => setIndex(value), + selectedIndex: _mainController.selectedIndex, + destinations: [ + ..._mainController.navigationBars.map((e) { + return NavigationDestination( + icon: Badge( + label: Text(e['count'].toString()), + padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: e['count'] > 0, + child: e['icon'], + ), + selectedIcon: e['selectIcon'], + label: e['label'], + ); + }).toList(), + ], + ) + : BottomNavigationBar( + currentIndex: _mainController.selectedIndex, + onTap: (value) => setIndex(value), + iconSize: 16, + selectedFontSize: 12, + unselectedFontSize: 12, + items: [ + ..._mainController.navigationBars.map((e) { + return BottomNavigationBarItem( + icon: Badge( + label: Text(e['count'].toString()), + padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: e['count'] > 0, + child: e['icon'], + ), + activeIcon: e['selectIcon'], + label: e['label'], + ); + }).toList(), + ], + ), + ), ); }, ), diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index 9bfac15c..c3bad4b9 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -163,7 +163,7 @@ class _MediaPageState extends State // const SizedBox(height: 10), SizedBox( width: double.infinity, - height: 200 * MediaQuery.of(context).textScaleFactor, + height: MediaQuery.textScalerOf(context).scale(200), child: FutureBuilder( future: _futureBuilderFuture, builder: (context, snapshot) { diff --git a/lib/pages/rcmd/controller.dart b/lib/pages/rcmd/controller.dart index 54cb8c14..6ecc9d63 100644 --- a/lib/pages/rcmd/controller.dart +++ b/lib/pages/rcmd/controller.dart @@ -13,7 +13,6 @@ class RcmdController extends GetxController { // RxList webVideoList = [].obs; bool isLoadingMore = true; OverlayEntry? popupDialog; - Box recVideo = GStrorage.recVideo; Box setting = GStrorage.setting; RxInt crossAxisCount = 2.obs; late bool enableSaveLastData; @@ -25,15 +24,6 @@ class RcmdController extends GetxController { super.onInit(); crossAxisCount.value = setting.get(SettingBoxKey.customRows, defaultValue: 2); - // 读取app端缓存内容 - // if (recVideo.get('cacheList') != null && - // recVideo.get('cacheList').isNotEmpty) { - // List list = []; - // for (var i in recVideo.get('cacheList')) { - // list.add(i); - // } - // videoList.value = list; - // } enableSaveLastData = setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false); defaultRcmdType = @@ -84,10 +74,6 @@ class RcmdController extends GetxController { } else if (type == 'onLoad') { videoList.addAll(res['data']); } - // 目前仅支持app端系列保存缓存 - if (defaultRcmdType != 'web') { - recVideo.put('cacheList', res['data']); - } _currentPage += 1; // 若videoList数量太小,可能会影响翻页,此时再次请求 // 为避免请求到的数据太少时还在反复请求,要求本次返回数据大于1条才触发 diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 52a521e2..5ec1710a 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:hive/hive.dart'; -import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/search.dart'; import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/suggest.dart'; @@ -27,9 +26,6 @@ class SSearchController extends GetxController { @override void onInit() { super.onInit(); - if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) { - searchDefault(); - } // 其他页面跳转过来 if (Get.parameters.keys.isNotEmpty) { if (Get.parameters['keyword'] != null) { @@ -130,12 +126,4 @@ class SSearchController extends GetxController { historyList.refresh(); histiryWord.put('cacheList', []); } - - void searchDefault() async { - var res = await Request().get(Api.searchDefault); - if (res.data['code'] == 0) { - searchKeyWord.value = - hintText = defaultSearch.value = res.data['data']['name']; - } - } } diff --git a/lib/pages/search_panel/widgets/article_panel.dart b/lib/pages/search_panel/widgets/article_panel.dart index 6e73151a..35e39640 100644 --- a/lib/pages/search_panel/widgets/article_panel.dart +++ b/lib/pages/search_panel/widgets/article_panel.dart @@ -26,10 +26,9 @@ Widget searchArticlePanel(BuildContext context, ctr, list) { StyleString.safeSpace, 5, StyleString.safeSpace, 5), child: LayoutBuilder(builder: (context, boxConstraints) { double width = (boxConstraints.maxWidth - - StyleString.cardSpace * - 6 / - MediaQuery.of(context).textScaleFactor) / - 2; + StyleString.cardSpace * + 6 / + MediaQuery.textScalerOf(context).scale(2.0)); return Container( constraints: const BoxConstraints(minHeight: 88), height: width / StyleString.aspectRatio, diff --git a/lib/pages/search_panel/widgets/live_panel.dart b/lib/pages/search_panel/widgets/live_panel.dart index 606b44f6..6fb5f5b8 100644 --- a/lib/pages/search_panel/widgets/live_panel.dart +++ b/lib/pages/search_panel/widgets/live_panel.dart @@ -17,7 +17,7 @@ Widget searchLivePanel(BuildContext context, ctr, list) { mainAxisSpacing: StyleString.cardSpace + 3, mainAxisExtent: MediaQuery.sizeOf(context).width / 2 / StyleString.aspectRatio + - 66 * MediaQuery.of(context).textScaleFactor), + MediaQuery.textScalerOf(context).scale(66.0)), itemCount: list.length, itemBuilder: (context, index) { return LiveItem(liveItem: list![index]); diff --git a/lib/pages/search_panel/widgets/media_bangumi_panel.dart b/lib/pages/search_panel/widgets/media_bangumi_panel.dart index b8ae8d2e..18799d3a 100644 --- a/lib/pages/search_panel/widgets/media_bangumi_panel.dart +++ b/lib/pages/search_panel/widgets/media_bangumi_panel.dart @@ -67,11 +67,11 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) { TextSpan( text: i['text'], style: TextStyle( - fontSize: Theme.of(context) + fontSize: MediaQuery.textScalerOf(context) + .scale(Theme.of(context) .textTheme .titleSmall! - .fontSize! * - MediaQuery.of(context).textScaleFactor, + .fontSize!), fontWeight: FontWeight.bold, color: i['type'] == 'em' ? Theme.of(context).colorScheme.primary diff --git a/lib/pages/search_panel/widgets/video_panel.dart b/lib/pages/search_panel/widgets/video_panel.dart index 0b5d5eb8..2f1a3a0f 100644 --- a/lib/pages/search_panel/widgets/video_panel.dart +++ b/lib/pages/search_panel/widgets/video_panel.dart @@ -90,7 +90,7 @@ class SearchVideoPanel extends StatelessWidget { style: ButtonStyle( padding: MaterialStateProperty.all(EdgeInsets.zero), ), - onPressed: () => controller.onShowFilterDialog(), + onPressed: () => controller.onShowFilterDialog(ctr), icon: Icon( Icons.filter_list_outlined, size: 18, @@ -175,7 +175,7 @@ class VideoPanelController extends GetxController { super.onInit(); } - onShowFilterDialog() { + onShowFilterDialog(searchPanelCtr) { SmartDialog.show( animationType: SmartAnimationType.centerFade_otherSlide, builder: (BuildContext context) { @@ -199,7 +199,8 @@ class VideoPanelController extends GetxController { SmartDialog.dismiss(); SmartDialog.showToast("「${i['label']}」的筛选结果"); SearchPanelController ctr = - Get.find(tag: 'video'); + Get.find( + tag: 'video${searchPanelCtr.keyword!}'); ctr.duration.value = i['value']; SmartDialog.showLoading(msg: 'loooad'); await ctr.onRefresh(); diff --git a/lib/pages/search_result/view.dart b/lib/pages/search_result/view.dart index 054e2a78..ff5bf780 100644 --- a/lib/pages/search_result/view.dart +++ b/lib/pages/search_result/view.dart @@ -86,7 +86,8 @@ class _SearchResultPageState extends State onTap: (index) { if (index == _searchResultController!.tabIndex) { Get.find( - tag: SearchType.values[index].type) + tag: SearchType.values[index].type + + _searchResultController!.keyword!) .animateToTop(); } diff --git a/lib/pages/setting/pages/home_tabbar_set.dart b/lib/pages/setting/pages/home_tabbar_set.dart new file mode 100644 index 00000000..445ca4f5 --- /dev/null +++ b/lib/pages/setting/pages/home_tabbar_set.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/models/common/tab_type.dart'; +import 'package:pilipala/utils/storage.dart'; + +class TabbarSetPage extends StatefulWidget { + const TabbarSetPage({super.key}); + + @override + State createState() => _TabbarSetPageState(); +} + +class _TabbarSetPageState extends State { + Box settingStorage = GStrorage.setting; + late List defaultTabs; + late List tabbarSort; + + @override + void initState() { + super.initState(); + defaultTabs = tabsConfig; + tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, + defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); + } + + void saveEdit() { + List sortedTabbar = defaultTabs + .where((i) => tabbarSort.contains((i['type'] as TabType).id)) + .map((i) => (i['type'] as TabType).id) + .toList(); + if (sortedTabbar.isEmpty) { + SmartDialog.showToast('请至少设置一项!'); + return; + } + settingStorage.put(SettingBoxKey.tabbarSort, sortedTabbar); + SmartDialog.showToast('保存成功,下次启动时生效'); + } + + void onReorder(int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final tabsItem = defaultTabs.removeAt(oldIndex); + defaultTabs.insert(newIndex, tabsItem); + }); + } + + @override + Widget build(BuildContext context) { + final listTiles = [ + for (int i = 0; i < defaultTabs.length; i++) ...[ + CheckboxListTile( + key: Key(defaultTabs[i]['label']), + value: tabbarSort.contains((defaultTabs[i]['type'] as TabType).id), + onChanged: (bool? newValue) { + String tabTypeId = (defaultTabs[i]['type'] as TabType).id; + if (!newValue!) { + tabbarSort.remove(tabTypeId); + } else { + tabbarSort.add(tabTypeId); + } + setState(() {}); + }, + title: Text(defaultTabs[i]['label']), + secondary: const Icon(Icons.drag_indicator_rounded), + ) + ] + ]; + + return Scaffold( + appBar: AppBar( + title: const Text('Tabbar编辑'), + actions: [ + TextButton(onPressed: () => saveEdit(), child: const Text('保存')), + const SizedBox(width: 12) + ], + ), + body: ReorderableListView( + onReorder: onReorder, + physics: const NeverScrollableScrollPhysics(), + footer: SizedBox( + height: MediaQuery.of(context).padding.bottom + 30, + ), + children: listTiles, + ), + ); + } +} diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index a4e39aa0..57ec110a 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -254,6 +254,11 @@ class _StyleSettingState extends State { onTap: () => Get.toNamed('/fontSizeSetting'), title: Text('字体大小', style: titleStyle), ), + ListTile( + dense: false, + onTap: () => Get.toNamed('/tabbarSetting'), + title: Text('首页tabbar', style: titleStyle), + ), if (Platform.isAndroid) ListTile( dense: false, diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart index 5b6fdc96..51c296f3 100644 --- a/lib/pages/video/detail/related/view.dart +++ b/lib/pages/video/detail/related/view.dart @@ -9,7 +9,8 @@ import './controller.dart'; class RelatedVideoPanel extends StatelessWidget { final ReleatedController _releatedController = - Get.put(ReleatedController(), tag: Get.arguments['heroTag']); + Get.put(ReleatedController(), tag: Get.arguments?['heroTag']); + RelatedVideoPanel({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 8701025f..475c0939 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -849,6 +849,13 @@ InlineSpan buildContent( WidgetSpan( child: LayoutBuilder( builder: (BuildContext context, BoxConstraints box) { + double maxHeight = box.maxWidth * 0.6; // 设置最大高度 + // double width = (box.maxWidth / 2).truncateToDouble(); + double height = ((box.maxWidth / + 2 * + pictureItem['img_height'] / + pictureItem['img_width'])) + .truncateToDouble(); return GestureDetector( onTap: () { showDialog( @@ -859,15 +866,28 @@ InlineSpan buildContent( }, ); }, - child: Padding( + child: Container( padding: const EdgeInsets.only(top: 4), - child: NetworkImgLayer( - src: pictureItem['img_src'], - width: box.maxWidth / 2, - height: box.maxWidth * - 0.5 * - pictureItem['img_height'] / - pictureItem['img_width'], + constraints: BoxConstraints(maxHeight: maxHeight), + width: box.maxWidth / 2, + height: height, + child: Stack( + children: [ + Positioned.fill( + child: NetworkImgLayer( + src: pictureItem['img_src'], + width: box.maxWidth / 2, + height: height, + ), + ), + height > maxHeight + ? const PBadge( + text: '长图', + right: 8, + bottom: 8, + ) + : const SizedBox(), + ], ), ), ); diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index 6f538e4e..01c95adc 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -2,12 +2,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:hive/hive.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/utils/feed_back.dart'; -import 'package:pilipala/utils/storage.dart'; class VideoReplyNewDialog extends StatefulWidget { final int? oid; @@ -34,25 +32,16 @@ class _VideoReplyNewDialogState extends State final TextEditingController _replyContentController = TextEditingController(); final FocusNode replyContentFocusNode = FocusNode(); final GlobalKey _formKey = GlobalKey(); - double _keyboardHeight = 0.0; // 键盘高度 - final _debouncer = Debouncer(milliseconds: 100); // 设置延迟时间 - bool ableClean = false; - Timer? timer; - Box localCache = GStrorage.localCache; - late double sheetHeight; @override void initState() { super.initState(); // 监听输入框聚焦 // replyContentFocusNode.addListener(_onFocus); - _replyContentController.addListener(_printLatestValue); // 界面观察者 必须 WidgetsBinding.instance.addObserver(this); // 自动聚焦 _autoFocus(); - - sheetHeight = localCache.get('sheetHeight'); } _autoFocus() async { @@ -62,12 +51,6 @@ class _VideoReplyNewDialogState extends State } } - _printLatestValue() { - setState(() { - ableClean = _replyContentController.text != ''; - }); - } - Future submitReplyAdd() async { feedBack(); String message = _replyContentController.text; @@ -90,24 +73,6 @@ class _VideoReplyNewDialogState extends State } } - @override - void didChangeMetrics() { - super.didChangeMetrics(); - WidgetsBinding.instance.addPostFrameCallback((_) { - // 键盘高度 - final viewInsets = EdgeInsets.fromViewPadding( - View.of(context).viewInsets, View.of(context).devicePixelRatio); - _debouncer.run(() { - if (mounted) { - setState(() { - _keyboardHeight = - _keyboardHeight == 0.0 ? viewInsets.bottom : _keyboardHeight; - }); - } - }); - }); - } - @override void dispose() { WidgetsBinding.instance.removeObserver(this); @@ -117,8 +82,10 @@ class _VideoReplyNewDialogState extends State @override Widget build(BuildContext context) { + double keyboardHeight = EdgeInsets.fromViewPadding( + View.of(context).viewInsets, View.of(context).devicePixelRatio) + .bottom; return Container( - height: 500, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: const BorderRadius.only( @@ -130,26 +97,32 @@ class _VideoReplyNewDialogState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Expanded( + ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200, + minHeight: 120, + ), child: Container( padding: const EdgeInsets.only( - top: 6, right: 15, left: 15, bottom: 10), - child: Form( - key: _formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: TextField( - controller: _replyContentController, - minLines: 1, - maxLines: null, - autofocus: false, - focusNode: replyContentFocusNode, - decoration: const InputDecoration( - hintText: "输入回复内容", - border: InputBorder.none, - hintStyle: TextStyle( - fontSize: 14, - )), - style: Theme.of(context).textTheme.bodyLarge, + top: 12, right: 15, left: 15, bottom: 10), + child: SingleChildScrollView( + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextField( + controller: _replyContentController, + minLines: 1, + maxLines: null, + autofocus: false, + focusNode: replyContentFocusNode, + decoration: const InputDecoration( + hintText: "输入回复内容", + border: InputBorder.none, + hintStyle: TextStyle( + fontSize: 14, + )), + style: Theme.of(context).textTheme.bodyLarge, + ), ), ), ), @@ -168,22 +141,23 @@ class _VideoReplyNewDialogState extends State width: 36, height: 36, child: IconButton( - onPressed: () { - FocusScope.of(context) - .requestFocus(replyContentFocusNode); - }, - icon: Icon(Icons.keyboard, - size: 22, - color: Theme.of(context).colorScheme.onBackground), - highlightColor: - Theme.of(context).colorScheme.onInverseSurface, - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - backgroundColor: - MaterialStateProperty.resolveWith((states) { - return Theme.of(context).highlightColor; - }), - )), + onPressed: () { + FocusScope.of(context) + .requestFocus(replyContentFocusNode); + }, + icon: Icon(Icons.keyboard, + size: 22, + color: Theme.of(context).colorScheme.onBackground), + highlightColor: + Theme.of(context).colorScheme.onInverseSurface, + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + backgroundColor: + MaterialStateProperty.resolveWith((states) { + return Theme.of(context).highlightColor; + }), + ), + ), ), const Spacer(), TextButton( @@ -196,7 +170,7 @@ class _VideoReplyNewDialogState extends State duration: const Duration(milliseconds: 300), child: SizedBox( width: double.infinity, - height: _keyboardHeight, + height: keyboardHeight, ), ), ], @@ -204,22 +178,3 @@ class _VideoReplyNewDialogState extends State ); } } - -typedef DebounceCallback = void Function(); - -class Debouncer { - DebounceCallback? callback; - final int? milliseconds; - Timer? _timer; - - Debouncer({this.milliseconds}); - - run(DebounceCallback callback) { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(Duration(milliseconds: milliseconds!), () { - callback(); - }); - } -} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index aafca3e3..59fa39b2 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -122,6 +122,7 @@ class _VideoDetailPageState extends State plPlayerController!.triggerFullScreen(status: false); } shutdownTimerService.handleWaitingFinished(); + /// 顺序播放 列表循环 if (plPlayerController!.playRepeat != PlayRepeat.pause && plPlayerController!.playRepeat != PlayRepeat.singleCycle) { @@ -234,7 +235,7 @@ class _VideoDetailPageState extends State videoIntroController.isPaused = false; if (_extendNestCtr.position.pixels == 0 && autoplay) { await Future.delayed(const Duration(milliseconds: 300)); - plPlayerController!.seekTo(videoDetailController.defaultST); + plPlayerController?.seekTo(videoDetailController.defaultST); plPlayerController?.play(); } plPlayerController?.addStatusLister(playerListener); @@ -308,7 +309,7 @@ class _VideoDetailPageState extends State body: ExtendedNestedScrollView( controller: _extendNestCtr, headerSliverBuilder: - (BuildContext _context, bool innerBoxIsScrolled) { + (BuildContext context, bool innerBoxIsScrolled) { return [ Obx( () => SliverAppBar( @@ -418,7 +419,6 @@ class _VideoDetailPageState extends State ), ), ), - Obx( () => Visibility( visible: videoDetailController diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index fd3f02c9..d35987f8 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -621,7 +621,7 @@ class PlPlayerController { if (duration.value.inSeconds != 0) { if (type != 'slider') { /// 拖动进度条调节时,不等待第一帧,防止抖动 - await _videoPlayerController!.stream.buffer.first; + await _videoPlayerController?.stream.buffer.first; } await _videoPlayerController?.seek(position); // if (playerStatus.stopped) { @@ -786,7 +786,7 @@ class PlPlayerController { volume.value = volumeNew; try { - FlutterVolumeController.showSystemUI = false; + FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(volumeNew); } catch (err) { print(err); @@ -1086,12 +1086,13 @@ class PlPlayerController { localCache.put(LocalCacheKey.danmakuOpacity, opacityVal); localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal); localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal); - - var pp = _videoPlayerController!.platform as NativePlayer; - await pp.setProperty('audio-files', ''); - removeListeners(); - await _videoPlayerController?.dispose(); - _videoPlayerController = null; + if (_videoPlayerController != null) { + var pp = _videoPlayerController!.platform as NativePlayer; + await pp.setProperty('audio-files', ''); + removeListeners(); + await _videoPlayerController?.dispose(); + _videoPlayerController = null; + } _instance = null; // 关闭所有视频页面恢复亮度 resetBrightness(); diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 860342ef..3980453b 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -130,7 +129,7 @@ class _PLVideoPlayerState extends State setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false); Future.microtask(() async { try { - FlutterVolumeController.showSystemUI = true; + FlutterVolumeController.updateShowSystemUI(true); _ctr.volumeValue.value = (await FlutterVolumeController.getVolume())!; FlutterVolumeController.addListener((double value) { if (mounted && !_ctr.volumeInterceptEventStream.value) { @@ -154,7 +153,7 @@ class _PLVideoPlayerState extends State Future setVolume(double value) async { try { - FlutterVolumeController.showSystemUI = false; + FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(value); } catch (_) {} _ctr.volumeValue.value = value; @@ -703,7 +702,7 @@ class _PLVideoPlayerState extends State ), ); } else { - return nil; + return const SizedBox(); } }), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index d6b2f9a9..5a86ecf6 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -36,6 +36,7 @@ import '../pages/setting/index.dart'; import '../pages/setting/pages/color_select.dart'; import '../pages/setting/pages/display_mode.dart'; import '../pages/setting/pages/font_size_select.dart'; +import '../pages/setting/pages/home_tabbar_set.dart'; import '../pages/setting/pages/play_speed_set.dart'; import '../pages/setting/recommend_setting.dart'; import '../pages/setting/play_setting.dart'; @@ -114,6 +115,8 @@ class Routes { // CustomGetPage(name: '/blackListPage', page: () => const BlackListPage()), CustomGetPage(name: '/colorSetting', page: () => const ColorSelectPage()), + // 首页tabbar + CustomGetPage(name: '/tabbarSetting', page: () => const TabbarSetPage()), CustomGetPage( name: '/fontSizeSetting', page: () => const FontSizeSelectPage()), // 屏幕帧率 diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart new file mode 100644 index 00000000..1b54c628 --- /dev/null +++ b/lib/utils/extension.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +extension ImageExtension on num { + int cacheSize(BuildContext context) { + return (this * MediaQuery.of(context).devicePixelRatio).round(); + } +} diff --git a/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index e3814df6..a68bfb40 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -1,5 +1,6 @@ // ignore_for_file: constant_identifier_names +import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -72,4 +73,19 @@ class IdUtils { } return result; } + + // eid生成 + static String? genAuroraEid(int uid) { + if (uid == 0) { + return null; + } + String uidString = uid.toString(); + List resultBytes = List.generate( + uidString.length, + (i) => uidString.codeUnitAt(i) ^ "ad1va46a7lza".codeUnitAt(i % 12), + ); + String auroraEid = base64Url.encode(resultBytes); + auroraEid = auroraEid.replaceAll(RegExp(r'=*$', multiLine: true), ''); + return auroraEid; + } } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index fdb9ee02..1bcb2b9c 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -3,13 +3,11 @@ import 'dart:io'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:pilipala/models/home/rcmd/result.dart'; import 'package:pilipala/models/model_owner.dart'; import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/user/info.dart'; class GStrorage { - static late final Box recVideo; static late final Box userInfo; static late final Box historyword; static late final Box localCache; @@ -21,13 +19,6 @@ class GStrorage { final String path = dir.path; await Hive.initFlutter('$path/hive'); regAdapter(); - // 首页推荐视频 - recVideo = await Hive.openBox( - 'recVideo', - compactionStrategy: (int entries, int deletedEntries) { - return deletedEntries > 12; - }, - ); // 登录用户信息 userInfo = await Hive.openBox( 'userInfo', @@ -54,10 +45,6 @@ class GStrorage { } static void regAdapter() { - Hive.registerAdapter(RecVideoItemAppModelAdapter()); - Hive.registerAdapter(RcmdReasonAdapter()); - Hive.registerAdapter(RcmdStatAdapter()); - Hive.registerAdapter(RcmdOwnerAdapter()); Hive.registerAdapter(OwnerAdapter()); Hive.registerAdapter(UserInfoDataAdapter()); Hive.registerAdapter(LevelInfoAdapter()); @@ -73,8 +60,6 @@ class GStrorage { static Future close() async { // user.compact(); // user.close(); - recVideo.compact(); - recVideo.close(); userInfo.compact(); userInfo.close(); historyword.compact(); @@ -151,7 +136,8 @@ class SettingBoxKey { customRows = 'customRows', // 自定义列 enableMYBar = 'enableMYBar', hideSearchBar = 'hideSearchBar', // 收起顶栏 - hideTabBar = 'hideTabBar'; // 收起底栏 + hideTabBar = 'hideTabBar', // 收起底栏 + tabbarSort = 'tabbarSort'; // 首页tabbar } class LocalCacheKey { diff --git a/pubspec.yaml b/pubspec.yaml index e808367a..5f22a7de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.16+1016 +version: 1.0.17+1017 environment: sdk: ">=2.19.6 <3.0.0"