diff --git a/README.md b/README.md index 56c748ec..a5274cc1 100644 --- a/README.md +++ b/README.md @@ -74,12 +74,14 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码 - [ ] 弹幕 - [ ] 字幕 - [x] 记忆播放 + - [x] 视频比例:高度/宽度适应、填充、包含等 - [x] 搜索相关 - [x] 热搜 - [x] 搜索历史 - [x] 默认搜索词 - [x] 投稿、番剧、直播间、用户搜索 + - [x] 视频搜索排序、按时长筛选 - [x] 视频详情页相关 - [x] 视频选集(分p)切换 diff --git a/assets/images/error.svg b/assets/images/error.svg new file mode 100644 index 00000000..ef27bea4 --- /dev/null +++ b/assets/images/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/change_log/1.0.2.0819.md b/change_log/1.0.2.0819.md new file mode 100644 index 00000000..b270775f --- /dev/null +++ b/change_log/1.0.2.0819.md @@ -0,0 +1,19 @@ +## 1.0.2 + +### 新功能 ++ 自动检查更新 ++ 封面图片保存 ++ 动态跳转番剧 ++ 历史记录番剧记忆播放 ++ 一键清空稍后再看 + +### 修复 ++ 切换分P cid未切换 ++ cookie存储问题 ++ 登录/退出登录问题 + +### 优化 ++ 页面空/异常状态样式 ++ 退出登录提示 ++ 请求节流 ++ 全屏播放 \ No newline at end of file diff --git a/change_log/1.0.3.0821.md b/change_log/1.0.3.0821.md new file mode 100644 index 00000000..4c845976 --- /dev/null +++ b/change_log/1.0.3.0821.md @@ -0,0 +1,19 @@ +## 1.0.3 + +建议卸载1.0.2版本,重新安装 +### 新功能 ++ 底部播放进度条设置 ++ 复制图片链接 + + +### 修复 ++ 用户数据格式修改 ++ video Fit ++ 没有audio 资源的视频异常 ++ 评论区域图片无法点击 ++ 视频进度条拖动问题 + +### 优化 ++ 页面空/异常状态样式 ++ 部分页面样式 ++ 图片预览页面样式 \ No newline at end of file diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart index 3295721d..b02182c6 100644 --- a/lib/common/widgets/http_error.dart +++ b/lib/common/widgets/http_error.dart @@ -1,31 +1,41 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class HttpError extends StatelessWidget { - const HttpError({required this.errMsg, required this.fn, super.key}); + const HttpError( + {required this.errMsg, required this.fn, this.btnText, super.key}); final String? errMsg; final Function()? fn; + final String? btnText; @override Widget build(BuildContext context) { return SliverToBoxAdapter( child: SizedBox( - height: 150, + height: 400, child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ + SvgPicture.asset( + "assets/images/error.svg", + height: 200, + ), + const SizedBox(height: 20), Text( errMsg ?? '请求异常', textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium, + style: Theme.of(context).textTheme.titleSmall, ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: () { - fn!(); - }, - child: const Text('点击重试')) + const SizedBox(height: 30), + OutlinedButton.icon( + onPressed: () { + fn!(); + }, + icon: const Icon(Icons.arrow_forward_outlined, size: 20), + label: Text(btnText ?? '点击重试'), + ) ], ), ), diff --git a/lib/common/widgets/no_data.dart b/lib/common/widgets/no_data.dart new file mode 100644 index 00000000..8b6a8214 --- /dev/null +++ b/lib/common/widgets/no_data.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class NoData extends StatelessWidget { + const NoData({super.key}); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: SizedBox( + height: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + "assets/images/error.svg", + height: 200, + ), + const SizedBox(height: 20), + Text( + '没有数据', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall, + ), + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 4cfce1ef..ed7b299e 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -55,7 +55,7 @@ class VideoCardH extends StatelessWidget { }, child: Padding( padding: const EdgeInsets.fromLTRB( - StyleString.safeSpace, 7, StyleString.safeSpace, 7), + StyleString.safeSpace, 5, StyleString.safeSpace, 5), child: LayoutBuilder( builder: (context, boxConstraints) { double width = diff --git a/lib/http/init.dart b/lib/http/init.dart index 05bf5e28..5f9e1e2b 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -20,7 +20,7 @@ class Request { /// 设置cookie static setCookie() async { - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; var cookiePath = await Utils.getCookiePath(); var cookieJar = PersistCookieJar( ignoreExpires: true, @@ -30,7 +30,8 @@ class Request { dio.interceptors.add(cookieManager); var cookie = await cookieManager.cookieJar .loadForRequest(Uri.parse(HttpString.baseUrl)); - if (user.get(UserBoxKey.userMid) != null) { + var userInfo = userInfoCache.get('userInfoCache'); + if (userInfo != null && userInfo.mid != null) { var cookie2 = await cookieManager.cookieJar .loadForRequest(Uri.parse(HttpString.tUrl)); if (cookie2.isEmpty) { @@ -86,9 +87,10 @@ class Request { }, ); - Box user = GStrorage.user; - if (user.get(UserBoxKey.userMid) != null) { - options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString(); + Box userInfoCache = GStrorage.userInfo; + var userInfo = userInfoCache.get('userInfoCache'); + if (userInfo != null && userInfo.mid != null) { + options.headers['x-bili-mid'] = userInfo.mid.toString(); options.headers['env'] = 'prod'; options.headers['app-key'] = 'android64'; options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH'; diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index 2521d02d..9a86fbb9 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -17,7 +17,7 @@ class ApiInterceptor extends Interceptor { handler.next(options); } - Box user = GStrorage.user; + Box localCache = GStrorage.localCache; @override void onResponse(Response response, ResponseInterceptorHandler handler) { @@ -29,7 +29,8 @@ class ApiInterceptor extends Interceptor { final uri = Uri.parse(locations.first); final accessKey = uri.queryParameters['access_key']; final mid = uri.queryParameters['mid']; - user.put(UserBoxKey.accessKey, {'mid': mid, 'value': accessKey}); + localCache + .put(LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey}); } } } diff --git a/lib/http/search.dart b/lib/http/search.dart index 4a1a4fdd..5d99e3e0 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -46,14 +46,19 @@ class SearchHttp { required SearchType searchType, required String keyword, required page, + String? order, + int? duration, }) async { - var res = await Request().get(Api.searchByType, data: { + var reqData = { 'search_type': searchType.type, 'keyword': keyword, // 'order_sort': 0, // 'user_type': 0, - 'page': page - }); + 'page': page, + if (order != null) 'order': order, + if (duration != null) 'duration': duration, + }; + var res = await Request().get(Api.searchByType, data: reqData); if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) { Object data; switch (searchType) { diff --git a/lib/http/user.dart b/lib/http/user.dart index 050470fb..26739b02 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,3 +1,4 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; @@ -197,8 +198,12 @@ class UserHttp { 'sign': Constants.thirdSign, }, ); - if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) { - Request().get(res.data['data']['confirm_uri']); + try { + if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) { + Request().get(res.data['data']['confirm_uri']); + } + } catch (err) { + SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error); } } diff --git a/lib/http/video.dart b/lib/http/video.dart index 18048377..f67702f0 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -18,7 +18,7 @@ import 'package:pilipala/utils/storage.dart'; /// 返回{'status': bool, 'data': List} /// view层根据 status 判断渲染逻辑 class VideoHttp { - static Box user = GStrorage.user; + static Box localCache = GStrorage.localCache; static Box setting = GStrorage.setting; // 首页推荐视频 @@ -61,8 +61,9 @@ class VideoHttp { 'device_name': 'vivo', 'pull': freshIdx == 0 ? 'true' : 'false', 'appkey': Constants.appKey, - 'access_key': - user.get(UserBoxKey.accessKey, defaultValue: {})['value'] ?? '' + 'access_key': localCache + .get(LocalCacheKey.accessKey, defaultValue: {})['value'] ?? + '' }, ); if (res.data['code'] == 0) { @@ -137,7 +138,12 @@ class VideoHttp { 'data': PlayUrlModel.fromJson(res.data['data']) }; } else { - return {'status': false, 'data': []}; + return { + 'status': false, + 'data': [], + 'code': res.data['code'], + 'msg': res.data['message'], + }; } } catch (err) { return {'status': false, 'data': [], 'msg': err}; @@ -154,13 +160,14 @@ class VideoHttp { Map errMap = { -400: '请求错误', -403: '权限不足', - -404: '无视频', + -404: '视频资源失效', 62002: '稿件不可见', 62004: '稿件审核中', }; return { 'status': false, 'data': null, + 'code': result.code, 'msg': errMap[result.code] ?? '请求异常', }; } diff --git a/lib/models/common/search_type.dart b/lib/models/common/search_type.dart index b4caecc1..491ee7b4 100644 --- a/lib/models/common/search_type.dart +++ b/lib/models/common/search_type.dart @@ -27,3 +27,20 @@ extension SearchTypeExtension on SearchType { ['video', 'media_bangumi', 'live_room', 'bili_user'][index]; String get label => ['视频', '番剧', '直播间', '用户'][index]; } + +// 搜索类型为视频、专栏及相簿时 +enum ArchiveFilterType { + totalrank, + click, + pubdate, + dm, + stow, + scores, + // 专栏 + // attention, +} + +extension ArchiveFilterTypeExtension on ArchiveFilterType { + String get description => + ['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index]; +} diff --git a/lib/models/dynamics/result.dart b/lib/models/dynamics/result.dart index 78991418..79a385b3 100644 --- a/lib/models/dynamics/result.dart +++ b/lib/models/dynamics/result.dart @@ -408,6 +408,7 @@ class DynamicMajorModel { this.live, this.none, this.type, + this.courses, }); DynamicArchiveModel? archive; @@ -422,6 +423,7 @@ class DynamicMajorModel { // MAJOR_TYPE_ARCHIVE 视频 // MAJOR_TYPE_OPUS 图文/文章 String? type; + Map? courses; DynamicMajorModel.fromJson(Map json) { archive = json['archive'] != null @@ -444,6 +446,7 @@ class DynamicMajorModel { none = json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null; type = json['type']; + courses = json['courses'] ?? {}; } } diff --git a/lib/models/dynamics/up.dart b/lib/models/dynamics/up.dart index acc61bc1..cfd1fa7d 100644 --- a/lib/models/dynamics/up.dart +++ b/lib/models/dynamics/up.dart @@ -8,7 +8,9 @@ class FollowUpModel { List? upList; FollowUpModel.fromJson(Map json) { - liveUsers = LiveUsers.fromJson(json['live_users']); + liveUsers = json['live_users'] != null + ? LiveUsers.fromJson(json['live_users']) + : null; upList = json['up_list'] != null ? json['up_list'].map((e) => UpItem.fromJson(e)).toList() : []; diff --git a/lib/models/github/latest.dart b/lib/models/github/latest.dart index 1b2d0706..8730a4ba 100644 --- a/lib/models/github/latest.dart +++ b/lib/models/github/latest.dart @@ -4,12 +4,14 @@ class LatestDataModel { this.tagName, this.createdAt, this.assets, + this.body, }); String? url; String? tagName; String? createdAt; List? assets; + String? body; LatestDataModel.fromJson(Map json) { url = json['url']; @@ -17,6 +19,7 @@ class LatestDataModel { createdAt = json['created_at']; assets = json['assets'].map((e) => AssetItem.fromJson(e)).toList(); + body = json['body']; } } diff --git a/lib/models/search/suggest.dart b/lib/models/search/suggest.dart index eff7cb50..ff161476 100644 --- a/lib/models/search/suggest.dart +++ b/lib/models/search/suggest.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + class SearchSuggestModel { SearchSuggestModel({ this.tag, @@ -19,32 +22,74 @@ class SearchSuggestItem { SearchSuggestItem({ this.value, this.term, - this.name, this.spid, + this.textRich, }); String? value; String? term; - List? name; int? spid; + Widget? textRich; SearchSuggestItem.fromJson(Map json, String inputTerm) { value = json['value']; term = json['term']; - String reg = '$inputTerm'; - try { - if (json['name'].indexOf(inputTerm) != -1) { - String str = json['name'].replaceAll(reg, '^'); - List arr = str.split('^'); - arr.insert(arr.length - 1, inputTerm); - name = arr; - } else { - name = ['', '', json['term']]; - } - } catch (err) { - name = ['', '', json['term']]; - } - - spid = json['spid']; + textRich = highlightText(json['name']); } } + +Widget highlightText(String str) { + // 创建正则表达式,匹配 ... 格式的文本 + RegExp regex = RegExp(r'(.*?)<\/em>'); + + // 用于存储每个匹配项的列表 + List children = []; + + // 获取所有匹配项 + Iterable matches = regex.allMatches(str); + + // 当前索引位置 + int currentIndex = 0; + + // 遍历每个匹配项 + for (var match in matches) { + // 获取当前匹配项之前的普通文本部分 + String normalText = str.substring(currentIndex, match.start); + + // 获取需要高亮显示的文本部分 + String highlightedText = match.group(1)!; + + // 如果普通文本部分不为空,则将其添加到 children 列表中 + if (normalText.isNotEmpty) { + children.add(TextSpan( + text: normalText, + style: DefaultTextStyle.of(Get.context!).style, + )); + } + + // 将需要高亮显示的文本部分添加到 children 列表中,并设置相应样式 + children.add(TextSpan( + text: highlightedText, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(Get.context!).colorScheme.primary), + )); + + // 更新当前索引位置 + currentIndex = match.end; + } + + // 如果当前索引位置小于文本长度,表示还有剩余的普通文本部分 + if (currentIndex < str.length) { + String remainingText = str.substring(currentIndex); + + // 将剩余的普通文本部分添加到 children 列表中 + children.add(TextSpan( + text: remainingText, + style: DefaultTextStyle.of(Get.context!).style, + )); + } + + // 使用 Text.rich 创建包含高亮显示的富文本小部件,并返回 + return Text.rich(TextSpan(children: children)); +} diff --git a/lib/models/user/history.dart b/lib/models/user/history.dart index 19698aa9..669874b4 100644 --- a/lib/models/user/history.dart +++ b/lib/models/user/history.dart @@ -121,7 +121,7 @@ class HisListItem { viewAt = json['view_at']; progress = json['progress']; badge = json['badge']; - showTitle = json['show_title']; + showTitle = json['show_title'] == '' ? null : json['show_title']; duration = json['duration']; current = json['current']; total = json['total']; diff --git a/lib/models/user/info.dart b/lib/models/user/info.dart index 143c4db2..c520443f 100644 --- a/lib/models/user/info.dart +++ b/lib/models/user/info.dart @@ -43,7 +43,7 @@ class UserInfoData { @HiveField(5) int? mobileVerified; @HiveField(6) - int? money; + double? money; @HiveField(7) int? moral; @HiveField(8) @@ -88,7 +88,7 @@ class UserInfoData { : LevelInfo(); mid = json['mid']; mobileVerified = json['mobile_verified']; - money = json['money']; + money = json['money'] is int ? json['money'].toDouble() : json['money']; moral = json['moral']; official = json['official']; officialVerify = json['officialVerify']; @@ -130,6 +130,7 @@ class LevelInfo { currentLevel = json['current_level']; currentMin = json['current_min']; currentExp = json['current_exp']; - nextExp = json['next_exp']; + nextExp = + json['current_level'] == 6 ? json['current_exp'] : json['next_exp']; } } diff --git a/lib/models/user/info.g.dart b/lib/models/user/info.g.dart index ed3aa62c..a2eae152 100644 --- a/lib/models/user/info.g.dart +++ b/lib/models/user/info.g.dart @@ -23,7 +23,7 @@ class UserInfoDataAdapter extends TypeAdapter { levelInfo: fields[3] as LevelInfo?, mid: fields[4] as int?, mobileVerified: fields[5] as int?, - money: fields[6] as int?, + money: fields[6] as double?, moral: fields[7] as int?, official: (fields[8] as Map?)?.cast(), officialVerify: (fields[9] as Map?)?.cast(), diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index 66c66abb..04a5efec 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:pilipala/http/index.dart'; import 'package:pilipala/models/github/latest.dart'; import 'package:pilipala/utils/utils.dart'; @@ -165,7 +164,7 @@ class AboutController extends GetxController { } } - // 获取啊当前版本 + // 获取当前版本 Future getCurrentApp() async { var result = await PackageInfo.fromPlatform(); currentVersion.value = result.version; diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index 7f9f6a61..09afc43a 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -11,17 +11,19 @@ class BangumiController extends GetxController { RxList bangumiFollowList = [BangumiListItemModel()].obs; int _currentPage = 1; bool isLoadingMore = true; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; RxBool userLogin = false.obs; late int mid; + var userInfo; @override void onInit() { super.onInit(); - if (user.get(UserBoxKey.userMid) != null) { - mid = int.parse(user.get(UserBoxKey.userMid).toString()); + userInfo = userInfoCache.get('userInfoCache'); + if (userInfo != null) { + mid = userInfo.mid; } - userLogin.value = user.get(UserBoxKey.userLogin) != null; + userLogin.value = userInfo != null; } Future queryBangumiListFeed({type = 'init'}) async { @@ -48,7 +50,11 @@ class BangumiController extends GetxController { // 我的订阅 Future queryBangumiFollow() async { - var result = await BangumiHttp.bangumiFollow(mid: 17340771); + userInfo = userInfo ?? userInfoCache.get('userInfoCache'); + if (userInfo == null) { + return; + } + var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid); if (result['status']) { bangumiFollowList.value = result['data'].list; } else {} diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index b0ce9452..e63e797d 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -49,7 +49,7 @@ class BangumiIntroController extends GetxController { RxBool hasCoin = false.obs; // 是否收藏 RxBool hasFav = false.obs; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; bool userLogin = false; Rx favFolderData = FavFolderData().obs; List addMediaIdsNew = []; @@ -57,6 +57,7 @@ class BangumiIntroController extends GetxController { // 关注状态 默认未关注 RxMap followStatus = {}.obs; int _tempThemeValue = -1; + var userInfo; @override void onInit() { @@ -82,7 +83,8 @@ class BangumiIntroController extends GetxController { // videoItem!['owner'] = args.owner; } } - userLogin = user.get(UserBoxKey.userLogin) != null; + userInfo = userInfoCache.get('userInfoCache'); + userLogin = userInfo != null; } // 获取番剧简介&选集 @@ -142,7 +144,7 @@ class BangumiIntroController extends GetxController { // 投币 Future actionCoinVideo() async { - if (user.get(UserBoxKey.userMid) == null) { + if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } @@ -283,7 +285,7 @@ class BangumiIntroController extends GetxController { Future queryVideoInFolder() async { var result = await VideoHttp.videoInFolder( - mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid)); + mid: userInfo.mid, rid: IdUtils.bv2av(bvid)); if (result['status']) { favFolderData.value = result['data']; } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index 456f532c..7d31c108 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -121,7 +121,7 @@ class _BangumiInfoState extends State { // 收藏 showFavBottomSheet() { - if (bangumiIntroController.user.get(UserBoxKey.userMid) == null) { + if (bangumiIntroController.userInfo.mid == null) { SmartDialog.showToast('账号未登录'); return; } diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index 6c25e508..3032bb4d 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -22,24 +23,28 @@ class _BangumiPageState extends State with AutomaticKeepAliveClientMixin { final BangumiController _bangumidController = Get.put(BangumiController()); late Future? _futureBuilderFuture; + late Future? _futureBuilderFutureFollow; + late ScrollController scrollController; + @override bool get wantKeepAlive => true; @override void initState() { super.initState(); - ScrollController scrollController = _bangumidController.scrollController; + scrollController = _bangumidController.scrollController; StreamController mainStream = Get.find().bottomBarStream; _futureBuilderFuture = _bangumidController.queryBangumiListFeed(); + _futureBuilderFutureFollow = _bangumidController.queryBangumiFollow(); scrollController.addListener( () async { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) { - if (!_bangumidController.isLoadingMore) { + EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () { _bangumidController.isLoadingMore = true; - await _bangumidController.onLoad(); - } + _bangumidController.onLoad(); + }); } final ScrollDirection direction = @@ -53,6 +58,12 @@ class _BangumiPageState extends State ); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); @@ -80,43 +91,61 @@ class _BangumiPageState extends State '最近追番', style: Theme.of(context).textTheme.titleMedium, ), + IconButton( + onPressed: () { + setState(() { + _futureBuilderFutureFollow = + _bangumidController.queryBangumiFollow(); + }); + }, + icon: const Icon( + Icons.refresh, + size: 20, + ), + ), ], ), ), SizedBox( height: 258, child: FutureBuilder( - future: _bangumidController.queryBangumiFollow(), + future: _futureBuilderFutureFollow, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data as Map; + List list = _bangumidController.bangumiFollowList; if (data['status']) { return Obx( - () => ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _bangumidController - .bangumiFollowList.length, - itemBuilder: (context, index) { - return Container( - width: Get.size.width / 3, - height: 254, - margin: EdgeInsets.only( - left: StyleString.safeSpace, - right: index == - _bangumidController - .bangumiFollowList - .length - - 1 - ? StyleString.safeSpace - : 0), - child: BangumiCardV( - bangumiItem: _bangumidController - .bangumiFollowList[index], + () => list.isNotEmpty + ? ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: list.length, + itemBuilder: (context, index) { + return Container( + width: Get.size.width / 3, + height: 254, + margin: EdgeInsets.only( + left: StyleString.safeSpace, + right: index == + _bangumidController + .bangumiFollowList + .length - + 1 + ? StyleString.safeSpace + : 0), + child: BangumiCardV( + bangumiItem: _bangumidController + .bangumiFollowList[index], + ), + ); + }, + ) + : const SizedBox( + child: Center( + child: Text('还没有追番'), + ), ), - ); - }, - ), ); } else { return const SizedBox(); diff --git a/lib/pages/blacklist/index.dart b/lib/pages/blacklist/index.dart index 27aa770f..63792532 100644 --- a/lib/pages/blacklist/index.dart +++ b/lib/pages/blacklist/index.dart @@ -46,6 +46,7 @@ class _BlackListPageState extends State { List blackMidsList = _blackListController.blackList.map((e) => e.mid!).toList(); setting.put(SettingBoxKey.blackMidsList, blackMidsList); + scrollController.removeListener(() {}); super.dispose(); } diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart index 5bef4794..d8417d50 100644 --- a/lib/pages/dynamics/controller.dart +++ b/lib/pages/dynamics/controller.dart @@ -51,29 +51,42 @@ class DynamicsController extends GetxController { ]; bool flag = false; RxInt initialValue = 1.obs; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; RxBool userLogin = false.obs; + var userInfo; + RxBool isLoadingDynamic = false.obs; @override void onInit() { - userLogin.value = user.get(UserBoxKey.userLogin, defaultValue: false); + userInfo = userInfoCache.get('userInfoCache'); + userLogin.value = userInfo != null; super.onInit(); } Future queryFollowDynamic({type = 'init'}) async { if (!userLogin.value) { - return {'status': false, 'msg': '未登录'}; + return {'status': false, 'msg': '账号未登录'}; } if (type == 'init') { dynamicsList.clear(); } + // 下拉刷新数据渲染时会触发onLoad + if (type == 'onLoad' && page == 1) { + return; + } + isLoadingDynamic.value = true; var res = await DynamicsHttp.followDynamic( page: type == 'init' ? 1 : page, type: dynamicsType.value.values, offset: offset, mid: mid.value, ); + isLoadingDynamic.value = false; if (res['status']) { + if (type == 'onLoad' && res['data'].items.isEmpty) { + SmartDialog.showToast('没有更多了'); + return; + } if (type == 'init') { dynamicsList.value = res['data'].items; } else { @@ -188,12 +201,19 @@ class DynamicsController extends GetxController { } Future queryFollowUp({type = 'init'}) async { + if (!userLogin.value) { + return {'status': false, 'msg': '账号未登录'}; + } if (type == 'init') { - upData = FollowUpModel().obs; + upData.value.upList = []; + upData.value.liveUsers = LiveUsers(); } var res = await DynamicsHttp.followUp(); if (res['status']) { upData.value = res['data']; + if (upData.value.upList!.isEmpty) { + mid.value = -1; + } } return res; } @@ -207,7 +227,8 @@ class DynamicsController extends GetxController { onRefresh() async { page = 1; - queryFollowUp(); + print('onRefresh'); + await queryFollowUp(); await queryFollowDynamic(); } @@ -227,7 +248,7 @@ class DynamicsController extends GetxController { mid.value = -1; dynamicsType.value = DynamicsType.values[0]; initialValue.value = 1; - SmartDialog.showToast('还原默认加载', alignment: Alignment.topCenter); + SmartDialog.showToast('还原默认加载'); dynamicsList.value = [DynamicItemModel()]; queryFollowDynamic(); } diff --git a/lib/pages/dynamics/deatil/view.dart b/lib/pages/dynamics/deatil/view.dart index 129494c9..6a779ba3 100644 --- a/lib/pages/dynamics/deatil/view.dart +++ b/lib/pages/dynamics/deatil/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_reply.dart'; @@ -40,7 +41,9 @@ class _DynamicDetailPageState extends State { } else { oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id; } - type = Get.arguments['item'].basic!['comment_type']; + int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11; + type = (commentType == 0) ? 11 : commentType; + action = Get.arguments.containsKey('action') ? Get.arguments['action'] : null; _dynamicDetailController = Get.put(DynamicDetailController(oid, type)); @@ -56,10 +59,9 @@ class _DynamicDetailPageState extends State { void _listen() async { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 300) { - if (!_dynamicDetailController!.isLoadingMore) { - _dynamicDetailController!.isLoadingMore = true; - await _dynamicDetailController!.queryReplyList(reqType: 'onLoad'); - } + EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { + _dynamicDetailController!.queryReplyList(reqType: 'onLoad'); + }); } if (scrollController.offset > 55 && !_visibleTitle) { @@ -95,6 +97,12 @@ class _DynamicDetailPageState extends State { ); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -236,6 +244,11 @@ class _DynamicDetailPageState extends State { replyReply: (replyItem) => replyReply(replyItem), replyType: ReplyType.values[type], + addReply: (replyItem) { + _dynamicDetailController! + .replyList[index].replies! + .add(replyItem); + }, ); } }, diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index 4f6b6a40..a29e1f91 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/common/skeleton/dynamic_card.dart'; import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/utils/event_bus.dart'; @@ -29,9 +31,9 @@ class _DynamicsPageState extends State final DynamicsController _dynamicsController = Get.put(DynamicsController()); late Future _futureBuilderFuture; late Future _futureBuilderFutureUp; - bool _isLoadingMore = false; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; EventBus eventBus = EventBus(); + late ScrollController scrollController; @override bool get wantKeepAlive => true; @@ -41,18 +43,17 @@ class _DynamicsPageState extends State super.initState(); _futureBuilderFuture = _dynamicsController.queryFollowDynamic(); _futureBuilderFutureUp = _dynamicsController.queryFollowUp(); - ScrollController scrollController = _dynamicsController.scrollController; + scrollController = _dynamicsController.scrollController; StreamController mainStream = Get.find().bottomBarStream; scrollController.addListener( () async { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) { - if (!_isLoadingMore) { - _isLoadingMore = true; - await _dynamicsController.queryFollowDynamic(type: 'onLoad'); - _isLoadingMore = false; - } + EasyThrottle.throttle( + 'queryFollowDynamic', const Duration(seconds: 1), () { + _dynamicsController.queryFollowDynamic(type: 'onLoad'); + }); } final ScrollDirection direction = @@ -74,6 +75,12 @@ class _DynamicsPageState extends State }); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); @@ -145,14 +152,12 @@ class _DynamicsPageState extends State .textTheme .labelMedium! .fontSize)), - // 4: Text( - // '专栏', - // style: TextStyle( - // fontSize: Theme.of(context) - // .textTheme - // .labelMedium! - // .fontSize), - // ), + 4: Text('专栏', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize)), }, padding: 13.0, decoration: BoxDecoration( @@ -179,22 +184,22 @@ class _DynamicsPageState extends State ) ], ), - Obx( - () => Visibility( - visible: _dynamicsController.userLogin.value, - child: Positioned( - right: 4, - top: 0, - bottom: 0, - child: IconButton( - padding: EdgeInsets.zero, - onPressed: () => - {feedBack(), _dynamicsController.resetSearch()}, - icon: const Icon(Icons.history, size: 21), - ), - ), - ), - ), + // Obx( + // () => Visibility( + // visible: _dynamicsController.userLogin.value, + // child: Positioned( + // right: 4, + // top: 0, + // bottom: 0, + // child: IconButton( + // padding: EdgeInsets.zero, + // onPressed: () => + // {feedBack(), _dynamicsController.resetSearch()}, + // icon: const Icon(Icons.history, size: 21), + // ), + // ), + // ), + // ), ], ), ), @@ -233,14 +238,24 @@ class _DynamicsPageState extends State List list = _dynamicsController.dynamicsList; return Obx( - () => list.isEmpty - ? skeleton() - : SliverList( - delegate: - SliverChildBuilderDelegate((context, index) { + () { + if (list.isEmpty) { + if (_dynamicsController.isLoadingDynamic.value) { + return skeleton(); + } else { + return const NoData(); + } + } else { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { return DynamicPanel(item: list[index]); - }, childCount: list.length), + }, + childCount: list.length, ), + ); + } + }, ); } else { return HttpError( @@ -261,6 +276,7 @@ class _DynamicsPageState extends State } }, ), + const SliverToBoxAdapter(child: SizedBox(height: 40)) ], ), ), diff --git a/lib/pages/dynamics/widgets/action_panel.dart b/lib/pages/dynamics/widgets/action_panel.dart index 34b7f6af..53c9e497 100644 --- a/lib/pages/dynamics/widgets/action_panel.dart +++ b/lib/pages/dynamics/widgets/action_panel.dart @@ -37,7 +37,7 @@ class _ActionPanelState extends State { String dynamicId = item.idStr!; // 1 已点赞 2 不喜欢 0 未操作 Like like = item.modules.moduleStat.like; - int count = int.parse(like.count!); + int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0'); bool status = like.status!; int up = status ? 2 : 1; var res = await DynamicsHttp.likeDynamic(dynamicId: dynamicId, up: up); @@ -47,7 +47,11 @@ class _ActionPanelState extends State { item.modules.moduleStat.like.count = (count + 1).toString(); item.modules.moduleStat.like.status = true; } else { - item.modules.moduleStat.like.count = (count - 1).toString(); + if (count == 1) { + item.modules.moduleStat.like.count = '点赞'; + } else { + item.modules.moduleStat.like.count = (count - 1).toString(); + } item.modules.moduleStat.like.status = false; } setState(() {}); @@ -63,54 +67,63 @@ class _ActionPanelState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - TextButton.icon( - onPressed: () {}, - icon: const Icon( - FontAwesomeIcons.shareFromSquare, - size: 16, + Expanded( + flex: 1, + child: TextButton.icon( + onPressed: () {}, + icon: const Icon( + FontAwesomeIcons.shareFromSquare, + size: 16, + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), + foregroundColor: Theme.of(context).colorScheme.outline, + ), + label: Text(stat.forward!.count ?? '转发'), ), - style: TextButton.styleFrom( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), - foregroundColor: Theme.of(context).colorScheme.outline, - ), - label: Text(stat.forward!.count ?? '转发'), ), - TextButton.icon( - onPressed: () => - _dynamicsController.pushDetail(widget.item, 1, action: 'comment'), - icon: const Icon( - FontAwesomeIcons.comment, - size: 16, + Expanded( + flex: 1, + child: TextButton.icon( + onPressed: () => _dynamicsController.pushDetail(widget.item, 1, + action: 'comment'), + icon: const Icon( + FontAwesomeIcons.comment, + size: 16, + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), + foregroundColor: Theme.of(context).colorScheme.outline, + ), + label: Text(stat.comment!.count ?? '评论'), ), - style: TextButton.styleFrom( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), - foregroundColor: Theme.of(context).colorScheme.outline, - ), - label: Text(stat.comment!.count ?? '评论'), ), - TextButton.icon( - onPressed: () => onLikeDynamic(), - icon: Icon( - stat.like!.status! - ? FontAwesomeIcons.solidThumbsUp - : FontAwesomeIcons.thumbsUp, - size: 16, - color: stat.like!.status! ? primary : color, - ), - style: TextButton.styleFrom( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), - foregroundColor: Theme.of(context).colorScheme.outline, - ), - label: AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: Text( - stat.like!.count ?? '点赞', - key: ValueKey(stat.like!.count ?? '点赞'), - style: TextStyle( - color: stat.like!.status! ? primary : color, + Expanded( + flex: 1, + child: TextButton.icon( + onPressed: () => onLikeDynamic(), + icon: Icon( + stat.like!.status! + ? FontAwesomeIcons.solidThumbsUp + : FontAwesomeIcons.thumbsUp, + size: 16, + color: stat.like!.status! ? primary : color, + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), + foregroundColor: Theme.of(context).colorScheme.outline, + ), + label: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Text( + stat.like!.count ?? '点赞', + key: ValueKey(stat.like!.count ?? '点赞'), + style: TextStyle( + color: stat.like!.status! ? primary : color, + ), ), ), ), diff --git a/lib/pages/dynamics/widgets/additional_panel.dart b/lib/pages/dynamics/widgets/additional_panel.dart index b3d6e98e..e283fcf9 100644 --- a/lib/pages/dynamics/widgets/additional_panel.dart +++ b/lib/pages/dynamics/widgets/additional_panel.dart @@ -60,43 +60,47 @@ Widget addWidget(item, context, type, {floor = 1}) { ), ); case 'ADDITIONAL_TYPE_RESERVE': - return Padding( - padding: const EdgeInsets.only(top: 8), - child: InkWell( - onTap: () {}, - child: Container( - width: double.infinity, - padding: - const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10), - color: bgColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - dynamicProperty[type].title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 1), - Text.rich( - TextSpan( - style: TextStyle( - color: Theme.of(context).colorScheme.outline, - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize), + return dynamicProperty[type].state != -1 + ? Padding( + padding: const EdgeInsets.only(top: 8), + child: InkWell( + onTap: () {}, + child: Container( + width: double.infinity, + padding: const EdgeInsets.only( + left: 12, top: 10, right: 12, bottom: 10), + color: bgColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan(text: dynamicProperty[type].desc1['text']), - const TextSpan(text: ' '), - TextSpan(text: dynamicProperty[type].desc2['text']), + Text( + dynamicProperty[type].title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 1), + Text.rich( + TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize), + children: [ + TextSpan(text: dynamicProperty[type].desc1['text']), + const TextSpan(text: ' '), + TextSpan(text: dynamicProperty[type].desc2['text']), + ], + ), + ) ], ), - ) - ], - ), - // TextButton(onPressed: () {}, child: Text('123')) - ), - ), - ); + // TextButton(onPressed: () {}, child: Text('123')) + ), + ), + ) + : const SizedBox(); case 'ADDITIONAL_TYPE_GOODS': return Padding( padding: const EdgeInsets.only(top: 6), diff --git a/lib/pages/dynamics/widgets/forward_panel.dart b/lib/pages/dynamics/widgets/forward_panel.dart index e9e290b3..55972e37 100644 --- a/lib/pages/dynamics/widgets/forward_panel.dart +++ b/lib/pages/dynamics/widgets/forward_panel.dart @@ -100,6 +100,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) { // 直播 case 'DYNAMIC_TYPE_LIVE_RCMD': return liveRcmdPanel(item, context, floor: floor); + // 直播 case 'DYNAMIC_TYPE_LIVE': return livePanel(item, context, floor: floor); // 合集 @@ -147,6 +148,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) { return videoSeasonWidget(item, context, 'pgc', floor: floor); case 'DYNAMIC_TYPE_PGC_UNION': return videoSeasonWidget(item, context, 'pgc', floor: floor); + // 直播结束 case 'DYNAMIC_TYPE_NONE': return Row( children: [ @@ -158,7 +160,23 @@ Widget forWard(item, context, ctr, source, {floor = 1}) { Text(item.modules.moduleDynamic.major.none.tips) ], ); + // 课堂 + case 'DYNAMIC_TYPE_COURSES_SEASON': + return Row( + children: [ + Expanded( + child: Text( + "课堂💪:${item.modules.moduleDynamic.major.courses['title']}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + ], + ); default: - return const SizedBox(height: 0); + return const SizedBox( + width: double.infinity, + child: Text('🙏 暂未支持的类型,请联系开发者反馈 '), + ); } } diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index 2c0a63f7..a12956a5 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -24,24 +24,28 @@ class _UpPanelState extends State { List upList = []; List liveList = []; static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0); - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; + var userInfo; @override void initState() { super.initState(); upList = widget.upData!.upList!; - liveList = widget.upData!.liveUsers!.items!; + if (widget.upData!.liveUsers != null) { + liveList = widget.upData!.liveUsers!.items!; + } upList.insert( 0, UpItem( face: 'https://files.catbox.moe/8uc48f.png', uname: '全部动态', mid: -1), ); + userInfo = userInfoCache.get('userInfoCache'); upList.insert( 1, UpItem( - face: user.get(UserBoxKey.userFace), + face: userInfo.face, uname: '我', - mid: user.get(UserBoxKey.userMid), + mid: userInfo.mid, ), ); } @@ -64,15 +68,20 @@ class _UpPanelState extends State { controller: scrollController, children: [ const SizedBox(width: 10), - for (int i = 0; i < liveList.length; i++) ...[ - upItemBuild(liveList[i], i) + if (liveList.isNotEmpty) ...[ + for (int i = 0; i < liveList.length; i++) ...[ + upItemBuild(liveList[i], i) + ], + VerticalDivider( + indent: 20, + endIndent: 40, + width: 26, + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.5), + ), ], - VerticalDivider( - indent: 20, - endIndent: 40, - width: 26, - color: Theme.of(context).primaryColor.withOpacity(0.5), - ), for (int i = 0; i < upList.length; i++) ...[ upItemBuild(upList[i], i) ], @@ -123,7 +132,8 @@ class _UpPanelState extends State { double itemWidth = contentWidth + itemPadding.horizontal; double screenWidth = MediaQuery.of(context).size.width; double moveDistance = 0.0; - if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) { + if (itemWidth * (upList.length + liveList.length) <= screenWidth) { + } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) { moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2; } else { diff --git a/lib/pages/fan/controller.dart b/lib/pages/fan/controller.dart index cfbbc90c..4f389b92 100644 --- a/lib/pages/fan/controller.dart +++ b/lib/pages/fan/controller.dart @@ -5,19 +5,22 @@ import 'package:pilipala/models/fans/result.dart'; import 'package:pilipala/utils/storage.dart'; class FansController extends GetxController { - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; int pn = 1; int total = 0; RxList fansList = [FansItemModel()].obs; late int mid; late String name; + var userInfo; @override void onInit() { super.onInit(); - mid = int.parse( - Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString()); - name = Get.parameters['name'] ?? user.get(UserBoxKey.userName); + userInfo = userInfoCache.get('userInfoCache'); + mid = Get.parameters['mid'] != null + ? int.parse(Get.parameters['mid']!) + : userInfo.mid; + name = Get.parameters['name'] ?? userInfo.uname; } Future queryFans(type) async { diff --git a/lib/pages/fan/view.dart b/lib/pages/fan/view.dart index e17d87d2..a0b42528 100644 --- a/lib/pages/fan/view.dart +++ b/lib/pages/fan/view.dart @@ -37,6 +37,12 @@ class _FansPageState extends State { ); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart index 80162297..41923449 100644 --- a/lib/pages/fav/controller.dart +++ b/lib/pages/fav/controller.dart @@ -1,16 +1,24 @@ import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/utils/storage.dart'; class FavController extends GetxController { Rx favFolderData = FavFolderData().obs; + Box userInfoCache = GStrorage.userInfo; + UserInfoData? userInfo; Future queryFavFolder() async { + userInfo = userInfoCache.get('userInfoCache'); + if (userInfo == null) { + return {'status': false, 'msg': '账号未登录'}; + } var res = await await UserHttp.userfavFolder( pn: 1, ps: 10, - mid: GStrorage.user.get(UserBoxKey.userMid) ?? 0, + mid: userInfo!.mid!, ); if (res['status']) { favFolderData.value = res['data']; diff --git a/lib/pages/favDetail/view.dart b/lib/pages/favDetail/view.dart index 88c2cb5f..d90d4f11 100644 --- a/lib/pages/favDetail/view.dart +++ b/lib/pages/favDetail/view.dart @@ -61,6 +61,7 @@ class _FavDetailPageState extends State { SliverAppBar( expandedHeight: 260 - MediaQuery.of(context).padding.top, pinned: true, + titleSpacing: 0, title: StreamBuilder( stream: titleStreamC.stream, initialData: false, diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart index 6b8d80c2..a64e20f6 100644 --- a/lib/pages/follow/controller.dart +++ b/lib/pages/follow/controller.dart @@ -5,19 +5,22 @@ import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/utils/storage.dart'; class FollowController extends GetxController { - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; int pn = 1; int total = 0; RxList followList = [FollowItemModel()].obs; late int mid; late String name; + var userInfo; @override void onInit() { super.onInit(); - mid = int.parse( - Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString()); - name = Get.parameters['name'] ?? user.get(UserBoxKey.userName); + userInfo = userInfoCache.get('userInfoCache'); + mid = Get.parameters['mid'] != null + ? int.parse(Get.parameters['mid']!) + : userInfo.mid; + name = Get.parameters['name'] ?? userInfo.uname; } Future queryFollowings(type) async { diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index ec46c0c5..d2a4b423 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -37,6 +37,12 @@ class _FollowPageState extends State { ); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/pages/history/controller.dart b/lib/pages/history/controller.dart index 7dc8e3a5..ae897499 100644 --- a/lib/pages/history/controller.dart +++ b/lib/pages/history/controller.dart @@ -9,9 +9,10 @@ import 'package:pilipala/utils/storage.dart'; class HistoryController extends GetxController { final ScrollController scrollController = ScrollController(); RxList historyList = [HisListItem()].obs; - bool isLoadingMore = false; + RxBool isLoadingMore = false.obs; RxBool pauseStatus = false.obs; Box localCache = GStrorage.localCache; + RxBool isLoading = false.obs; @override void onInit() { @@ -26,9 +27,9 @@ class HistoryController extends GetxController { max = historyList.last.history!.oid!; viewAt = historyList.last.viewAt!; } - isLoadingMore = true; + isLoadingMore.value = true; var res = await UserHttp.historyList(max, viewAt); - isLoadingMore = false; + isLoadingMore.value = false; if (res['status']) { if (type == 'onload') { historyList.addAll(res['data'].list); diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index 3de41366..c0118819 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -1,7 +1,9 @@ +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; import 'package:pilipala/pages/history/index.dart'; import 'widgets/item.dart'; @@ -16,25 +18,33 @@ class HistoryPage extends StatefulWidget { class _HistoryPageState extends State { final HistoryController _historyController = Get.put(HistoryController()); Future? _futureBuilderFuture; + late ScrollController scrollController; @override void initState() { _futureBuilderFuture = _historyController.queryHistoryList(); super.initState(); - - _historyController.scrollController.addListener( + scrollController = _historyController.scrollController; + scrollController.addListener( () { - if (_historyController.scrollController.position.pixels >= - _historyController.scrollController.position.maxScrollExtent - - 300) { - if (!_historyController.isLoadingMore) { - _historyController.onLoad(); + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 300) { + if (!_historyController.isLoadingMore.value) { + EasyThrottle.throttle('history', const Duration(seconds: 1), () { + _historyController.onLoad(); + }); } } }, ); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -92,13 +102,8 @@ class _HistoryPageState extends State { Map data = snapshot.data; if (data['status']) { return Obx( - () => _historyController.historyList.isEmpty - ? const SliverToBoxAdapter( - child: Center( - child: Text('没数据'), - ), - ) - : SliverList( + () => _historyController.historyList.isNotEmpty + ? SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return HistoryItem( @@ -108,7 +113,12 @@ class _HistoryPageState extends State { }, childCount: _historyController.historyList.length), - ), + ) + : _historyController.isLoadingMore.value + ? const SliverToBoxAdapter( + child: Center(child: Text('加载中')), + ) + : const NoData(), ); } else { return HttpError( diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index b3dd7f0f..a368a978 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -37,20 +37,23 @@ class HistoryItem extends StatelessWidget { 'pageTitle': videoItem.title }, ); - } else if (videoItem.history.business == 'live' && - videoItem.liveStatus == 1) { - LiveItemModel liveItem = LiveItemModel.fromJson({ - 'face': videoItem.authorFace, - 'roomid': videoItem.history.oid, - 'pic': videoItem.cover, - 'title': videoItem.title, - 'uname': videoItem.authorName, - 'cover': videoItem.cover, - }); - Get.toNamed( - '/liveRoom?roomid=${videoItem.history.oid}', - arguments: {'liveItem': liveItem}, - ); + } else if (videoItem.history.business == 'live') { + if (videoItem.liveStatus == 1) { + LiveItemModel liveItem = LiveItemModel.fromJson({ + 'face': videoItem.authorFace, + 'roomid': videoItem.history.oid, + 'pic': videoItem.cover, + 'title': videoItem.title, + 'uname': videoItem.authorName, + 'cover': videoItem.cover, + }); + Get.toNamed( + '/liveRoom?roomid=${videoItem.history.oid}', + arguments: {'liveItem': liveItem}, + ); + } else { + SmartDialog.showToast('直播未开播'); + } } else if (videoItem.badge == '番剧' || videoItem.tagName.contains('动画')) { /// hack @@ -116,7 +119,7 @@ class HistoryItem extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.fromLTRB( - StyleString.cardSpace, 5, StyleString.cardSpace, 5), + StyleString.safeSpace, 5, StyleString.safeSpace, 5), child: LayoutBuilder( builder: (context, boxConstraints) { double width = diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 980d7381..727cdce3 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -11,16 +11,17 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { late TabController tabController; late List tabsCtrList; late List tabsPageList; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; RxBool userLogin = false.obs; RxString userFace = ''.obs; + var userInfo; @override void onInit() { super.onInit(); - - userLogin.value = user.get(UserBoxKey.userLogin) ?? false; - userFace.value = user.get(UserBoxKey.userFace) ?? ''; + userInfo = userInfoCache.get('userInfoCache'); + userLogin.value = userInfo != null; + userFace.value = userInfo != null ? userInfo.face : ''; // 进行tabs配置 tabs = tabsConfig; @@ -48,7 +49,8 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { // 更新登录状态 void updateLoginStatus(val) { + userInfo = userInfoCache.get('userInfoCache'); userLogin.value = val ?? false; - userFace.value = user.get(UserBoxKey.userFace) ?? ''; + userFace.value = userInfo != null ? userInfo.face : ''; } } diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index 198a9fa3..36920aef 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -6,13 +6,14 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/mine/view.dart'; import 'package:pilipala/utils/storage.dart'; -Box user = GStrorage.user; +Box userInfoCache = GStrorage.userInfo; class HomeAppBar extends StatelessWidget { const HomeAppBar({super.key}); @override Widget build(BuildContext context) { + var userInfo = userInfoCache.get('userInfoCache'); return SliverAppBar( // forceElevated: true, scrolledUnderElevation: 0, @@ -55,7 +56,7 @@ class HomeAppBar extends StatelessWidget { const SizedBox(width: 6), /// TODO - if (user.get(UserBoxKey.userLogin)) ...[ + if (userInfo != null) ...[ GestureDetector( onTap: () => showModalBottomSheet( context: context, @@ -70,7 +71,7 @@ class HomeAppBar extends StatelessWidget { type: 'avatar', width: 32, height: 32, - src: user.get(UserBoxKey.userMid), + src: userInfo.face, ), ), const SizedBox(width: 10), diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 191ebc0e..16ee4348 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -23,6 +23,7 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { final HotController _hotController = Get.put(HotController()); List videoList = []; Future? _futureBuilderFuture; + late ScrollController scrollController; @override bool get wantKeepAlive => true; @@ -31,7 +32,7 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { void initState() { super.initState(); _futureBuilderFuture = _hotController.queryHotFeed('init'); - ScrollController scrollController = _hotController.scrollController; + scrollController = _hotController.scrollController; StreamController mainStream = Get.find().bottomBarStream; scrollController.addListener( @@ -55,6 +56,12 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { ); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart index fa524157..7c04f8dc 100644 --- a/lib/pages/later/view.dart +++ b/lib/pages/later/view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/pages/later/index.dart'; @@ -85,13 +86,11 @@ class _LaterPageState extends State { ); }, childCount: _laterController.laterList.length), ) - : SliverToBoxAdapter( - child: Center( - child: Text(_laterController.isLoading.value - ? '加载中' - : '没有数据'), - ), - ), + : _laterController.isLoading.value + ? const SliverToBoxAdapter( + child: Center(child: Text('加载中')), + ) + : const NoData(), ); } else { return HttpError( diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index e07950ae..385d3272 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -23,22 +24,23 @@ class LivePage extends StatefulWidget { class _LivePageState extends State { final LiveController _liveController = Get.put(LiveController()); late Future _futureBuilderFuture; + late ScrollController scrollController; @override void initState() { super.initState(); _futureBuilderFuture = _liveController.queryLiveList('init'); - ScrollController scrollController = _liveController.scrollController; + scrollController = _liveController.scrollController; StreamController mainStream = Get.find().bottomBarStream; scrollController.addListener( () { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) { - if (!_liveController.isLoadingMore) { + EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () { _liveController.isLoadingMore = true; _liveController.onLoad(); - } + }); } final ScrollDirection direction = @@ -52,6 +54,12 @@ class _LivePageState extends State { ); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 1d2385e0..85150349 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -2,9 +2,12 @@ import 'dart:async'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/media/index.dart'; +import 'package:pilipala/utils/storage.dart'; +import 'package:pilipala/utils/utils.dart'; class MainController extends GetxController { List pages = [ @@ -49,4 +52,13 @@ class MainController extends GetxController { ].obs; final StreamController bottomBarStream = StreamController.broadcast(); + Box setting = GStrorage.setting; + + @override + void onInit() { + super.onInit(); + if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) { + Utils.checkUpdata(); + } + } } diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart index 88fef372..688b555c 100644 --- a/lib/pages/media/controller.dart +++ b/lib/pages/media/controller.dart @@ -8,7 +8,7 @@ import 'package:pilipala/utils/storage.dart'; class MediaController extends GetxController { Rx favFolderData = FavFolderData().obs; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; RxBool userLogin = false.obs; List list = [ { @@ -34,21 +34,23 @@ class MediaController extends GetxController { 'onTap': () => Get.toNamed('/later'), }, ]; + var userInfo; @override void onInit() { super.onInit(); - userLogin.value = user.get(UserBoxKey.userLogin) ?? false; + userInfo = userInfoCache.get('userInfoCache'); + userLogin.value = userInfo != null; } Future queryFavFolder() async { - if (!userLogin.value || GStrorage.user.get(UserBoxKey.userMid) == null) { + if (!userLogin.value || GStrorage.userInfo.get('userInfoCache') == null) { return {'status': false, 'data': [], 'msg': '未登录'}; } var res = await await UserHttp.userfavFolder( pn: 1, ps: 5, - mid: GStrorage.user.get(UserBoxKey.userMid), + mid: GStrorage.userInfo.get('userInfoCache').mid, ); favFolderData.value = res['data']; return res; diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index 13ab30bf..3657dbb5 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -161,11 +161,25 @@ class _MediaPageState extends State right: 14, bottom: 35), child: Center( child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.zero), + backgroundColor: + MaterialStateProperty.resolveWith( + (states) { + return Theme.of(context) + .colorScheme + .primaryContainer + .withOpacity(0.5); + }), + ), onPressed: () => Get.toNamed('/fav'), icon: Icon( Icons.arrow_forward_ios, size: 18, - color: Theme.of(context).primaryColor, + color: Theme.of(context) + .colorScheme + .primary, ), ), )); diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index eb3e4f33..db4deaae 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -14,16 +14,18 @@ class MemberController extends GetxController { Map? userStat; String? face; String? heroTag; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; late int ownerMid; // 投稿列表 RxList? archiveList = [VListItemModel()].obs; + var userInfo; @override void onInit() { super.onInit(); mid = int.parse(Get.parameters['mid']!); - ownerMid = user.get(UserBoxKey.userMid) ?? -1; + userInfo = userInfoCache.get('userInfoCache'); + ownerMid = userInfo != null ? userInfo.mid : -1; face = Get.arguments['face'] ?? ''; heroTag = Get.arguments['heroTag'] ?? ''; } @@ -57,7 +59,7 @@ class MemberController extends GetxController { // 关注/取关up Future actionRelationMod() async { - if (user.get(UserBoxKey.userMid) == null) { + if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 9bf4725c..55dad4f0 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -43,6 +43,12 @@ class _MemberPageState extends State ); } + @override + void dispose() { + _extendNestCtr.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index e4118154..66813091 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -13,9 +13,8 @@ class MineController extends GetxController { // 用户状态 动态、关注、粉丝 Rx userStat = UserStat().obs; RxBool userLogin = false.obs; - Box user = GStrorage.user; - Box setting = GStrorage.setting; Box userInfoCache = GStrorage.userInfo; + Box setting = GStrorage.setting; Rx themeType = ThemeType.system.obs; @override @@ -24,6 +23,7 @@ class MineController extends GetxController { if (userInfoCache.get('userInfoCache') != null) { userInfo.value = userInfoCache.get('userInfoCache'); + userLogin.value = true; } themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode, @@ -41,8 +41,8 @@ class MineController extends GetxController { }, ); } else { - int mid = user.get(UserBoxKey.userMid); - String face = user.get(UserBoxKey.userFace); + int mid = userInfo.value.mid!; + String face = userInfo.value.face!; Get.toNamed( '/member?mid=$mid', arguments: {'face': face}, @@ -51,7 +51,7 @@ class MineController extends GetxController { } Future queryUserInfo() async { - if (user.get(UserBoxKey.userLogin) == null) { + if (!userLogin.value) { return {'status': false}; } var res = await UserHttp.userInfo(); @@ -59,18 +59,12 @@ class MineController extends GetxController { if (res['data'].isLogin) { userInfo.value = res['data']; userInfoCache.put('userInfoCache', res['data']); - user.put(UserBoxKey.userName, res['data'].uname); - user.put(UserBoxKey.userFace, res['data'].face); - user.put(UserBoxKey.userMid, res['data'].mid); - user.put(UserBoxKey.userLogin, true); userLogin.value = true; - // Get.find().readuUserFace(); } else { resetUserInfo(); } } else { resetUserInfo(); - // SmartDialog.showToast(res['msg']); } await queryUserStatOwner(); return res; @@ -87,12 +81,8 @@ class MineController extends GetxController { Future resetUserInfo() async { userInfo.value = UserInfoData(); userStat.value = UserStat(); - await user.delete(UserBoxKey.userName); - await user.delete(UserBoxKey.userFace); - await user.delete(UserBoxKey.userMid); - await user.delete(UserBoxKey.userLogin); + userInfoCache.delete('userInfoCache'); userLogin.value = false; - // Get.find().resetLast(); } onChangeTheme() { diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index e084d96b..4c71869d 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/common/theme_type.dart'; +import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/utils/event_bus.dart'; import 'controller.dart'; @@ -160,10 +161,11 @@ class _MinePageState extends State { ])) ], ), - const SizedBox(height: 5), + const SizedBox(height: 25), if (_mineController.userInfo.value.levelInfo != null) ...[ LayoutBuilder( builder: (context, BoxConstraints box) { + LevelInfo levelInfo = _mineController.userInfo.value.levelInfo; return SizedBox( width: box.maxWidth, height: 24, @@ -172,48 +174,27 @@ class _MinePageState extends State { Positioned( top: 0, right: 0, - child: SizedBox( - height: 22, + bottom: 0, + child: Container( + color: Theme.of(context).colorScheme.primary, + height: 24, + constraints: + const BoxConstraints(minWidth: 100), // 设置最小宽度为100 width: box.maxWidth * - (1 - - (_mineController - .userInfo.value.levelInfo!.currentExp! / - _mineController - .userInfo.value.levelInfo!.nextExp!)), + (1 - (levelInfo.currentExp! / levelInfo.nextExp!)), child: Center( child: Text( - (_mineController - .userInfo.value.levelInfo!.nextExp! - - _mineController - .userInfo.value.levelInfo!.currentExp!) - .toString(), + '${levelInfo.currentExp!}/${levelInfo.nextExp!}', style: TextStyle( - color: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onPrimary, fontSize: 12, ), ), ), ), ), - ], - ), - ); - }, - ), - LayoutBuilder( - builder: (context, BoxConstraints box) { - return Container( - width: box.maxWidth, - height: 1, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Theme.of(context).colorScheme.onInverseSurface, - ), - child: Stack( - children: [ Positioned( - top: 0, + top: 23, left: 0, bottom: 0, child: Container( @@ -224,7 +205,6 @@ class _MinePageState extends State { .userInfo.value.levelInfo!.nextExp!), height: 1, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), color: Theme.of(context).colorScheme.primary, ), ), @@ -234,6 +214,36 @@ class _MinePageState extends State { ); }, ), + // LayoutBuilder( + // builder: (context, BoxConstraints box) { + // return Container( + // width: box.maxWidth, + // height: 1, + // color: Theme.of(context).colorScheme.onInverseSurface, + // child: Stack( + // children: [ + // Positioned( + // top: 0, + // left: 0, + // bottom: 0, + // child: Container( + // width: box.maxWidth * + // (_mineController + // .userInfo.value.levelInfo!.currentExp! / + // _mineController + // .userInfo.value.levelInfo!.nextExp!), + // height: 1, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(4), + // color: Theme.of(context).colorScheme.primary, + // ), + // ), + // ), + // ], + // ), + // ); + // }, + // ), ], const SizedBox(height: 30), Padding( diff --git a/lib/pages/preview/controller.dart b/lib/pages/preview/controller.dart index 8bc938f8..500f0b1d 100644 --- a/lib/pages/preview/controller.dart +++ b/lib/pages/preview/controller.dart @@ -2,11 +2,9 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:get/get.dart'; -import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:share_plus/share_plus.dart'; class PreviewController extends GetxController { @@ -17,7 +15,7 @@ class PreviewController extends GetxController { bool storage = true; bool videos = true; bool photos = true; - bool visiable = true; + String currentImgUrl = ''; @override void onInit() { @@ -26,6 +24,7 @@ class PreviewController extends GetxController { initialPage.value = Get.arguments['initialPage']!; currentPage.value = Get.arguments['initialPage']! + 1; imgList.value = Get.arguments['imgList']; + currentImgUrl = imgList[initialPage.value]; } } @@ -39,22 +38,6 @@ class PreviewController extends GetxController { // final photosInfo = statuses[Permission.photos].toString(); } - // 图片保存 - void onSaveImg() async { - var response = await Dio().get(imgList[initialPage.value], - options: Options(responseType: ResponseType.bytes)); - final result = await ImageGallerySaver.saveImage( - Uint8List.fromList(response.data), - quality: 100, - name: "pic_vvex${DateTime.now().toString().split('-').join()}"); - if (result != null) { - if (result['isSuccess']) { - // ignore: avoid_print - print('已保存到相册'); - } - } - } - // 图片分享 void onShareImg() async { requestPermission(); @@ -62,9 +45,15 @@ class PreviewController extends GetxController { options: Options(responseType: ResponseType.bytes)); final temp = await getTemporaryDirectory(); String imgName = - "pic_plpl${DateTime.now().toString().split('-').join()}.jpg"; + "plpl_pic_${DateTime.now().toString().split('-').join()}.jpg"; var path = '${temp.path}/$imgName'; File(path).writeAsBytesSync(response.data); Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]); } + + void onChange(int index) { + initialPage.value = index; + currentPage.value = index + 1; + currentImgUrl = imgList[index]; + } } diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart index 610a3ae2..42eb0b69 100644 --- a/lib/pages/preview/view.dart +++ b/lib/pages/preview/view.dart @@ -2,9 +2,12 @@ import 'package:dismissible_page/dismissible_page.dart'; 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:extended_image/extended_image.dart'; +import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/utils/download.dart'; import 'controller.dart'; typedef DoubleClickAnimationListener = void Function(); @@ -35,6 +38,56 @@ class _ImagePreviewState extends State duration: const Duration(milliseconds: 250), vsync: this); } + onOpenMenu() { + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + onTap: () { + _previewController.onShareImg(); + SmartDialog.dismiss(); + }, + dense: true, + title: const Text('分享', style: TextStyle(fontSize: 14)), + ), + ListTile( + onTap: () { + Clipboard.setData( + ClipboardData(text: _previewController.currentImgUrl)) + .then((value) { + SmartDialog.showToast('已复制到粘贴板'); + SmartDialog.dismiss(); + }).catchError((err) { + SmartDialog.showNotify( + msg: err.toString(), + notifyType: NotifyType.error, + ); + }); + }, + dense: true, + title: const Text('复制链接', style: TextStyle(fontSize: 14)), + ), + ListTile( + onTap: () { + DownloadUtils.downloadImg(_previewController.currentImgUrl); + }, + dense: true, + title: const Text('保存到手机', style: TextStyle(fontSize: 14)), + ), + ], + ), + ); + }, + ); + } + @override void dispose() { // animationController.dispose(); @@ -51,7 +104,7 @@ class _ImagePreviewState extends State primary: false, toolbarHeight: 0, backgroundColor: Colors.black, - systemOverlayStyle: SystemUiOverlayStyle.light, + systemOverlayStyle: SystemUiOverlayStyle.dark, ), body: Stack( children: [ @@ -69,19 +122,14 @@ class _ImagePreviewState extends State tag: _previewController .imgList[_previewController.initialPage.value], child: GestureDetector( - onTap: () { - _previewController.visiable = !_previewController.visiable; - setState(() {}); - }, + onLongPress: () => onOpenMenu(), child: ExtendedImageGesturePageView.builder( controller: ExtendedPageController( initialPage: _previewController.initialPage.value, pageSpacing: 0, ), - onPageChanged: (int index) { - _previewController.initialPage.value = index; - _previewController.currentPage.value = index + 1; - }, + onPageChanged: (int index) => + _previewController.onChange(index), canScrollPage: (GestureDetails? gestureDetails) => gestureDetails!.totalScale! <= 1.0, preloadPagesCount: 2, @@ -149,8 +197,10 @@ class _ImagePreviewState extends State children: [ SizedBox( width: 150.0, - child: - LinearProgressIndicator(value: progress), + child: LinearProgressIndicator( + value: progress, + color: Colors.white, + ), ), const SizedBox(height: 10.0), Text('${((progress ?? 0.0) * 100).toInt()}%'), @@ -179,7 +229,6 @@ class _ImagePreviewState extends State right: 0, bottom: 0, child: Container( - // height: 45, padding: EdgeInsets.only( bottom: MediaQuery.of(context).padding.bottom, top: 20), decoration: const BoxDecoration( @@ -193,36 +242,18 @@ class _ImagePreviewState extends State tileMode: TileMode.mirror, ), ), - child: Padding( - padding: const EdgeInsets.only(left: 20, right: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () => Text.rich( + child: Obx( + () => Text.rich( + textAlign: TextAlign.center, + TextSpan( + style: const TextStyle(color: Colors.white, fontSize: 15), + children: [ TextSpan( - style: const TextStyle( - color: Colors.white, fontSize: 18), - children: [ - TextSpan( - text: _previewController.currentPage - .toString()), - const TextSpan(text: ' / '), - TextSpan( - text: _previewController.imgList.length - .toString()), - ]), - ), - ), - const Spacer(), - ElevatedButton( - onPressed: () => _previewController.onShareImg(), - child: const Text('分享')), - const SizedBox(width: 10), - ElevatedButton( - onPressed: () => _previewController.onSaveImg(), - child: const Text('保存')) - ], + text: _previewController.currentPage.toString()), + const TextSpan(text: ' / '), + TextSpan( + text: _previewController.imgList.length.toString()), + ]), ), ), ), diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart index 6cead2df..d8d7c57a 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -40,12 +41,11 @@ class _RcmdPageState extends State () { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) { - if (!_rcmdController.isLoadingMore) { + EasyThrottle.throttle( + 'my-throttler', const Duration(milliseconds: 500), () { _rcmdController.isLoadingMore = true; - WidgetsBinding.instance.addPostFrameCallback((_) async { - _rcmdController.onLoad(); - }); - } + _rcmdController.onLoad(); + }); } final ScrollDirection direction = @@ -59,6 +59,12 @@ class _RcmdPageState extends State ); } + @override + void dispose() { + _rcmdController.scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 9b3f7dad..20277b89 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -12,8 +12,7 @@ class SSearchController extends GetxController { final FocusNode searchFocusNode = FocusNode(); RxString searchKeyWord = ''.obs; Rx controller = TextEditingController().obs; - List hotSearchList = []; - Box hotKeyword = GStrorage.hotKeyword; + RxList hotSearchList = [HotSearchItem()].obs; Box histiryWord = GStrorage.historyword; List historyCacheList = []; RxList historyList = [].obs; @@ -27,14 +26,6 @@ class SSearchController extends GetxController { void onInit() { super.onInit(); searchDefault(); - if (hotKeyword.get('cacheList') != null && - hotKeyword.get('cacheList').isNotEmpty) { - List list = []; - for (var i in hotKeyword.get('cacheList')) { - list.add(i); - } - hotSearchList = list; - } // 其他页面跳转过来 if (Get.parameters.keys.isNotEmpty) { if (Get.parameters['keyword'] != null) { @@ -89,8 +80,7 @@ class SSearchController extends GetxController { // 获取热搜关键词 Future queryHotSearchList() async { var result = await SearchHttp.hotSearchList(); - hotSearchList = result['data'].list; - hotKeyword.put('cacheList', result['data'].list); + hotSearchList.value = result['data'].list; return result; } diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index d4190e47..fdd18352 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -45,6 +45,11 @@ class _SearchPageState extends State with RouteAware { return OpenContainer( closedElevation: 0, openElevation: 0, + onClosed: (_) async { + // 在 openBuilder 关闭时触发的回调函数 + await Future.delayed(const Duration(milliseconds: 500)); + _searchController.onClear(); + }, openColor: Theme.of(context).colorScheme.background, middleColor: Theme.of(context).colorScheme.background, closedColor: Theme.of(context).colorScheme.background, @@ -145,7 +150,7 @@ class _SearchPageState extends State with RouteAware { // 搜索建议 _searchSuggest(), // 热搜 - hotSearch(), + hotSearch(_searchController), // 搜索历史 _history() ], @@ -176,25 +181,7 @@ class _SearchPageState extends State with RouteAware { // child: Text( // _searchController.searchSuggestList[index].term!, // ), - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: _searchController - .searchSuggestList[index].name![0]), - TextSpan( - text: _searchController - .searchSuggestList[index].name![1], - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold), - ), - TextSpan( - text: _searchController - .searchSuggestList[index].name![2]), - ], - ), - ), + child: _searchController.searchSuggestList[index].textRich, ), ); }, @@ -203,20 +190,37 @@ class _SearchPageState extends State with RouteAware { ); } - Widget hotSearch() { + Widget hotSearch(ctr) { return Padding( padding: const EdgeInsets.fromLTRB(10, 14, 4, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.fromLTRB(6, 0, 0, 6), - child: Text( - '大家都在搜', - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontWeight: FontWeight.bold), + padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '大家都在搜', + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.bold), + ), + SizedBox( + height: 34, + child: TextButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all(const EdgeInsets.only( + left: 10, top: 6, bottom: 6, right: 10)), + ), + onPressed: () => ctr.queryHotSearchList(), + icon: const Icon(Icons.refresh_outlined, size: 18), + label: const Text('刷新'), + ), + ), + ], ), ), LayoutBuilder( @@ -228,15 +232,17 @@ class _SearchPageState extends State with RouteAware { if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data as Map; if (data['status']) { - return HotKeyword( - width: width, - hotSearchList: _searchController.hotSearchList, - onClick: (keyword) async { - _searchController.searchFocusNode.unfocus(); - await Future.delayed( - const Duration(milliseconds: 150)); - _searchController.onClickKeyword(keyword); - }, + return Obx( + () => HotKeyword( + width: width, + hotSearchList: _searchController.hotSearchList.value, + onClick: (keyword) async { + _searchController.searchFocusNode.unfocus(); + await Future.delayed( + const Duration(milliseconds: 150)); + _searchController.onClickKeyword(keyword); + }, + ), ); } else { return HttpError( diff --git a/lib/pages/searchPanel/controller.dart b/lib/pages/searchPanel/controller.dart index b8e4a166..826fdacc 100644 --- a/lib/pages/searchPanel/controller.dart +++ b/lib/pages/searchPanel/controller.dart @@ -12,17 +12,25 @@ class SearchPanelController extends GetxController { SearchType? searchType; RxInt page = 1.obs; RxList resultList = [].obs; + // 结果排序方式 搜索类型为视频、专栏及相簿时 + RxString order = ''.obs; + // 视频时长筛选 仅用于搜索视频 + RxInt duration = 0.obs; Future onSearch({type = 'init'}) async { var result = await SearchHttp.searchByType( - searchType: searchType!, keyword: keyword!, page: page.value); + searchType: searchType!, + keyword: keyword!, + page: page.value, + order: searchType!.type != 'video' ? null : order.value, + duration: searchType!.type != 'video' ? null : duration.value); if (result['status']) { - if (type == 'init' || type == 'onLoad') { - page.value++; - resultList.addAll(result['data'].list); - } else if (type == 'onRefresh') { + if (type == 'onRefresh') { resultList.value = result['data'].list; + } else { + resultList.addAll(result['data'].list); } + page.value++; onPushDetail(keyword, resultList); } return result; @@ -30,7 +38,7 @@ class SearchPanelController extends GetxController { Future onRefresh() async { page.value = 1; - onSearch(type: 'onRefresh'); + await onSearch(type: 'onRefresh'); } // 返回顶部并刷新 diff --git a/lib/pages/searchPanel/view.dart b/lib/pages/searchPanel/view.dart index 8e1cf3c4..346e5048 100644 --- a/lib/pages/searchPanel/view.dart +++ b/lib/pages/searchPanel/view.dart @@ -1,3 +1,4 @@ +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/media_bangumi.dart'; @@ -27,8 +28,8 @@ class _SearchPanelState extends State with AutomaticKeepAliveClientMixin { late SearchPanelController _searchPanelController; - bool _isLoadingMore = false; late Future _futureBuilderFuture; + late ScrollController scrollController; @override bool get wantKeepAlive => true; @@ -43,20 +44,24 @@ class _SearchPanelState extends State ), tag: widget.searchType!.type, ); - ScrollController scrollController = _searchPanelController.scrollController; + scrollController = _searchPanelController.scrollController; scrollController.addListener(() async { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { - if (!_isLoadingMore) { - _isLoadingMore = true; - await _searchPanelController.onSearch(type: 'onLoad'); - _isLoadingMore = false; - } + EasyThrottle.throttle('history', const Duration(seconds: 1), () { + _searchPanelController.onSearch(type: 'onLoad'); + }); } }); _futureBuilderFuture = _searchPanelController.onSearch(); } + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); @@ -70,12 +75,15 @@ class _SearchPanelState extends State if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data; var ctr = _searchPanelController; - List list = ctr.resultList; + RxList list = ctr.resultList; if (data['status']) { return Obx(() { switch (widget.searchType) { case SearchType.video: - return searchVideoPanel(context, ctr, list); + return SearchVideoPanel( + ctr: _searchPanelController, + list: list.value, + ); case SearchType.media_bangumi: return searchMbangumiPanel(context, ctr, list); case SearchType.bili_user: diff --git a/lib/pages/searchPanel/widgets/video_panel.dart b/lib/pages/searchPanel/widgets/video_panel.dart index ca6b09fb..6cdc7868 100644 --- a/lib/pages/searchPanel/widgets/video_panel.dart +++ b/lib/pages/searchPanel/widgets/video_panel.dart @@ -1,15 +1,217 @@ import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; +import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/pages/searchPanel/index.dart'; -Widget searchVideoPanel(BuildContext context, ctr, list) { - return ListView.builder( - controller: ctr!.scrollController, - addAutomaticKeepAlives: false, - addRepaintBoundaries: false, - itemCount: list!.length, - itemBuilder: (context, index) { - var i = list![index]; - return VideoCardH(videoItem: i); - }, - ); +class SearchVideoPanel extends StatelessWidget { + SearchVideoPanel({ + this.ctr, + this.list, + Key? key, + }) : super(key: key); + + final SearchPanelController? ctr; + final List? list; + + final VideoPanelController controller = Get.put(VideoPanelController()); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.topCenter, + children: [ + Padding( + padding: const EdgeInsets.only(top: 36), + child: ListView.builder( + controller: ctr!.scrollController, + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + itemCount: list!.length, + itemBuilder: (context, index) { + var i = list![index]; + return Padding( + padding: index == 0 + ? const EdgeInsets.only(top: 2) + : EdgeInsets.zero, + child: VideoCardH(videoItem: i), + ); + }, + ), + ), + // 分类筛选 + Container( + width: double.infinity, + height: 36, + padding: const EdgeInsets.only(left: 8, top: 0, right: 12), + // decoration: BoxDecoration( + // border: Border( + // bottom: BorderSide( + // color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + // ), + // ), + // ), + child: Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Obx( + () => Wrap( + // spacing: , + children: [ + for (var i in controller.filterList) ...[ + CustomFilterChip( + label: i['label'], + type: i['type'], + selectedType: controller.selectedType.value, + callFn: (bool selected) async { + controller.selectedType.value = i['type']; + ctr!.order.value = + i['type'].toString().split('.').last; + SmartDialog.showLoading(msg: 'loooad'); + await ctr!.onRefresh(); + SmartDialog.dismiss(); + }, + ), + ] + ], + ), + ), + ), + ), + const VerticalDivider(indent: 7, endIndent: 8), + const SizedBox(width: 3), + SizedBox( + width: 32, + height: 32, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => controller.onShowFilterDialog(), + icon: Icon( + Icons.filter_list_outlined, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ), // 放置在ListView.builder()上方的组件 + ], + ); + } +} + +class CustomFilterChip extends StatelessWidget { + const CustomFilterChip({ + this.label, + this.type, + this.selectedType, + this.callFn, + Key? key, + }) : super(key: key); + + final String? label; + final ArchiveFilterType? type; + final ArchiveFilterType? selectedType; + final Function? callFn; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 34, + child: FilterChip( + padding: const EdgeInsets.only(left: 11, right: 11), + labelPadding: EdgeInsets.zero, + label: Text( + label!, + style: const TextStyle(fontSize: 13), + ), + labelStyle: TextStyle( + color: type == selectedType + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline), + selected: type == selectedType, + showCheckmark: false, + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + selectedColor: Colors.transparent, + // backgroundColor: + // Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), + backgroundColor: Colors.transparent, + side: BorderSide.none, + onSelected: (bool selected) => callFn!(selected), + ), + ); + } +} + +class VideoPanelController extends GetxController { + RxList filterList = [{}].obs; + Rx selectedType = ArchiveFilterType.values.first.obs; + List> timeFiltersList = [ + {'label': '全部时长', 'value': 0}, + {'label': '0-10分钟', 'value': 1}, + {'label': '10-30分钟', 'value': 2}, + {'label': '30-60分钟', 'value': 3}, + {'label': '60分钟+', 'value': 4}, + ]; + RxInt currentTimeFilterval = 0.obs; + + @override + void onInit() { + List> list = ArchiveFilterType.values + .map((type) => { + 'label': type.description, + 'type': type, + }) + .toList(); + filterList.value = list; + super.onInit(); + } + + onShowFilterDialog() { + SmartDialog.show( + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + TextStyle textStyle = Theme.of(context).textTheme.titleMedium!; + return AlertDialog( + title: const Text('时长筛选'), + contentPadding: const EdgeInsets.fromLTRB(0, 15, 0, 20), + content: StatefulBuilder(builder: (context, StateSetter setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i in timeFiltersList) ...[ + RadioListTile( + value: i['value'], + autofocus: true, + title: Text(i['label'], style: textStyle), + groupValue: currentTimeFilterval.value, + onChanged: (value) async { + currentTimeFilterval.value = value!; + setState(() {}); + SmartDialog.dismiss(); + SmartDialog.showToast("「${i['label']}」的筛选结果"); + SearchPanelController ctr = + Get.find(tag: 'video'); + ctr.duration.value = i['value']; + SmartDialog.showLoading(msg: 'loooad'); + await ctr.onRefresh(); + SmartDialog.dismiss(); + }, + ), + ], + ], + ); + }), + ); + }, + ); + } } diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart index 0766340b..2cf50ebf 100644 --- a/lib/pages/setting/controller.dart +++ b/lib/pages/setting/controller.dart @@ -11,19 +11,22 @@ import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; class SettingController extends GetxController { - Box user = GStrorage.user; - Box setting = GStrorage.setting; Box userInfoCache = GStrorage.userInfo; + Box setting = GStrorage.setting; + // Box userInfoCache = GStrorage.userInfo; + Box localCache = GStrorage.localCache; RxBool userLogin = false.obs; RxBool feedBackEnable = false.obs; RxInt picQuality = 10.obs; Rx themeType = ThemeType.system.obs; + var userInfo; @override void onInit() { super.onInit(); - userLogin.value = user.get(UserBoxKey.userLogin) ?? false; + userInfo = userInfoCache.get('userInfoCache'); + userLogin.value = userInfo != null; feedBackEnable.value = setting.get(SettingBoxKey.feedBackEnable, defaultValue: false); picQuality.value = @@ -53,7 +56,8 @@ class SettingController extends GetxController { // 清空本地存储的用户标识 userInfoCache.put('userInfoCache', null); - user.put(UserBoxKey.accessKey, {'mid': -1, 'value': ''}); + localCache + .put(LocalCacheKey.accessKey, {'mid': -1, 'value': ''}); // 更改我的页面登录状态 await Get.find().resetUserInfo(); diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart new file mode 100644 index 00000000..a46cefaf --- /dev/null +++ b/lib/pages/setting/extra_setting.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:pilipala/utils/storage.dart'; + +import 'widgets/switch_item.dart'; + +class ExtraSetting extends StatefulWidget { + const ExtraSetting({super.key}); + + @override + State createState() => _ExtraSettingState(); +} + +class _ExtraSettingState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + title: Text( + '其他设置', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: ListView( + children: const [ + SetSwitchItem( + title: '检查更新', + subTitle: '每次启动时检查是否需要更新', + setKey: SettingBoxKey.autoUpdate, + defaultVal: false, + ), + ], + ), + ); + } +} diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart index 6f160f08..524b014a 100644 --- a/lib/pages/setting/play_setting.dart +++ b/lib/pages/setting/play_setting.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/video/play/quality.dart'; -import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart'; +import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/utils/storage.dart'; import 'widgets/switch_item.dart'; @@ -19,6 +19,7 @@ class _PlaySettingState extends State { late dynamic defaultAudioQa; late dynamic defaultDecode; late int defaultFullScreenMode; + late int defaultBtmProgressBehavior; @override void initState() { @@ -31,6 +32,8 @@ class _PlaySettingState extends State { defaultValue: VideoDecodeFormats.values.last.code); defaultFullScreenMode = setting.get(SettingBoxKey.fullScreenMode, defaultValue: FullScreenMode.values.first.code); + defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior, + defaultValue: BtmProgresBehavior.values.first.code); } @override @@ -163,6 +166,31 @@ class _PlaySettingState extends State { ], ), ), + ListTile( + dense: false, + title: Text('底部进度条展示', style: titleStyle), + subtitle: Text( + '当前展示方式:${BtmProgresBehaviorCode.fromCode(defaultBtmProgressBehavior)!.description}', + style: subTitleStyle, + ), + trailing: PopupMenuButton( + initialValue: defaultBtmProgressBehavior, + icon: const Icon(Icons.more_vert_outlined, size: 22), + onSelected: (item) { + defaultBtmProgressBehavior = item; + setting.put(SettingBoxKey.btmProgressBehavior, item); + setState(() {}); + }, + itemBuilder: (BuildContext context) => [ + for (var i in BtmProgresBehavior.values) ...[ + PopupMenuItem( + value: i.code, + child: Text(i.description), + ), + ] + ], + ), + ), ], ), ); diff --git a/lib/pages/setting/privacy_setting.dart b/lib/pages/setting/privacy_setting.dart index 5594fc1e..a6d085ab 100644 --- a/lib/pages/setting/privacy_setting.dart +++ b/lib/pages/setting/privacy_setting.dart @@ -13,12 +13,14 @@ class PrivacySetting extends StatefulWidget { class _PrivacySettingState extends State { bool userLogin = false; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; + var userInfo; @override void initState() { super.initState(); - userLogin = user.get(UserBoxKey.userLogin) ?? false; + userInfo = userInfoCache.get('userInfoCache'); + userLogin = userInfo != null; } @override diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index 0bddb809..677a4546 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -34,11 +34,11 @@ class SettingPage extends StatelessWidget { dense: false, title: const Text('外观设置'), ), - // ListTile( - // onTap: () {}, - // dense: false, - // title: const Text('其他设置'), - // ), + ListTile( + onTap: () => Get.toNamed('/extraSetting'), + dense: false, + title: const Text('其他设置'), + ), Obx( () => Visibility( visible: settingController.userLogin.value, diff --git a/lib/pages/setting/widgets/switch_item.dart b/lib/pages/setting/widgets/switch_item.dart index 6ae642d3..3e41e9ee 100644 --- a/lib/pages/setting/widgets/switch_item.dart +++ b/lib/pages/setting/widgets/switch_item.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/utils/storage.dart'; +import 'package:pilipala/utils/utils.dart'; class SetSwitchItem extends StatefulWidget { final String? title; @@ -61,6 +62,9 @@ class _SetSwitchItemState extends State { onChanged: (value) { val = value; Setting.put(widget.setKey, value); + if (widget.setKey == SettingBoxKey.autoUpdate && value == true) { + Utils.checkUpdata(); + } setState(() {}); }), ), diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index fe19ae0d..a7ea10f4 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -38,7 +38,7 @@ class VideoDetailController extends GetxController /// 播放器配置 画质 音质 解码格式 late VideoQuality currentVideoQa; - late AudioQuality currentAudioQa; + AudioQuality? currentAudioQa; late VideoDecodeFormats currentDecodeFormats; // PlPlayerController plPlayerController = PlPlayerController(); // 是否开始自动播放 存在多p的情况下,第二p需要为true @@ -51,7 +51,7 @@ class VideoDetailController extends GetxController RxBool enableHA = true.obs; /// 本地存储 - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; Box localCache = GStrorage.localCache; Box setting = GStrorage.setting; @@ -70,11 +70,13 @@ class VideoDetailController extends GetxController late Duration defaultST; // 默认记录历史记录 bool enableHeart = true; + var userInfo; @override void onInit() { super.onInit(); Map argMap = Get.arguments; + userInfo = userInfoCache.get('userInfoCache'); var keys = argMap.keys.toList(); if (keys.isNotEmpty) { if (keys.contains('videoItem')) { @@ -92,7 +94,7 @@ class VideoDetailController extends GetxController setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true); enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true); - if (user.get(UserBoxKey.userMid) == null || + if (userInfo == null || localCache.get(LocalCacheKey.historyPause) == true) { enableHeart = false; } @@ -140,9 +142,11 @@ class VideoDetailController extends GetxController videoUrl = firstVideo.baseUrl!; /// 根据currentAudioQa 重新设置audioUrl - AudioItem firstAudio = - data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code); - audioUrl = firstAudio.baseUrl ?? ''; + if (currentAudioQa != null) { + AudioItem firstAudio = + data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa!.code); + audioUrl = firstAudio.baseUrl ?? ''; + } playerInit(); } @@ -224,11 +228,16 @@ class VideoDetailController extends GetxController // 根据画质选编码格式 List supportDecodeFormats = supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!; - + // 默认从设置中取AVC + currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get( + SettingBoxKey.defaultDecode, + defaultValue: VideoDecodeFormats.values.last.code))!; try { - currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get( - SettingBoxKey.defaultDecode, - defaultValue: supportDecodeFormats.first))!; + // 当前视频没有对应格式返回第一个 + currentDecodeFormats = + supportDecodeFormats.contains(supportDecodeFormats) + ? supportDecodeFormats + : supportDecodeFormats.first; } catch (_) {} /// 取出符合当前解码格式的videoItem @@ -270,6 +279,9 @@ class VideoDetailController extends GetxController // duration: data.timeLength ?? 0, // ); } else { + if (result['code'] == -404) { + isShowCover.value = false; + } SmartDialog.showToast(result['msg'].toString()); } return result; diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index aa5d41c4..0817a046 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -30,9 +30,6 @@ class VideoIntroController extends GetxController { // 视频详情 请求返回 Rx videoDetail = VideoDetailData().obs; - // 请求返回的信息 - String responseMsg = '请求异常'; - // up主粉丝数 Map userStat = {'follower': '-'}; @@ -42,7 +39,7 @@ class VideoIntroController extends GetxController { RxBool hasCoin = false.obs; // 是否收藏 RxBool hasFav = false.obs; - Box user = GStrorage.user; + Box userInfoCache = GStrorage.userInfo; bool userLogin = false; Rx favFolderData = FavFolderData().obs; List addMediaIdsNew = []; @@ -52,10 +49,12 @@ class VideoIntroController extends GetxController { int _tempThemeValue = -1; RxInt lastPlayCid = 0.obs; + var userInfo; @override void onInit() { super.onInit(); + userInfo = userInfoCache.get('userInfoCache'); if (Get.arguments.isNotEmpty) { if (Get.arguments.containsKey('videoItem')) { preRender = true; @@ -77,7 +76,7 @@ class VideoIntroController extends GetxController { videoItem!['owner'] = args.owner; } } - userLogin = user.get(UserBoxKey.userLogin) != null; + userLogin = userInfo != null; lastPlayCid.value = int.parse(Get.parameters['cid']!); } @@ -94,8 +93,6 @@ class VideoIntroController extends GetxController { .value = ['简介', '评论 ${result['data']!.stat!.reply}']; // 获取到粉丝数再返回 await queryUserStat(); - } else { - responseMsg = result['msg']; } if (userLogin) { // 获取点赞状态 @@ -143,7 +140,7 @@ class VideoIntroController extends GetxController { // 一键三连 Future actionOneThree() async { - if (user.get(UserBoxKey.userMid) == null) { + if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } @@ -206,7 +203,7 @@ class VideoIntroController extends GetxController { // 投币 Future actionCoinVideo() async { - if (user.get(UserBoxKey.userMid) == null) { + if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } @@ -302,7 +299,7 @@ class VideoIntroController extends GetxController { Future queryVideoInFolder() async { var result = await VideoHttp.videoInFolder( - mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid)); + mid: userInfo.mid, rid: IdUtils.bv2av(bvid)); if (result['status']) { favFolderData.value = result['data']; } @@ -327,6 +324,9 @@ class VideoIntroController extends GetxController { // 查询关注状态 Future queryFollowStatus() async { + if (videoDetail.value.owner == null) { + return; + } var result = await VideoHttp.hasFollow(mid: videoDetail.value.owner!.mid!); if (result['status']) { followStatus.value = result['data']; @@ -337,7 +337,7 @@ class VideoIntroController extends GetxController { // 关注/取关up Future actionRelationMod() async { feedBack(); - if (user.get(UserBoxKey.userMid) == null) { + if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 2528ca18..5f99dd9b 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -71,6 +71,10 @@ class _VideoIntroPanelState extends State // 请求错误 return HttpError( errMsg: snapshot.data['msg'], + btnText: snapshot.data['code'] == -404 || + snapshot.data['code'] == 62002 + ? '返回上一页' + : null, fn: () => Get.back(), ); } @@ -127,7 +131,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 收藏 showFavBottomSheet() { - if (videoIntroController.user.get(UserBoxKey.userMid) == null) { + if (videoIntroController.userInfo == null) { SmartDialog.showToast('账号未登录'); return; } diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index 5bde42ba..a81674af 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -37,6 +37,9 @@ class VideoReplyController extends GetxController { if (type == 'init') { currentPage = 0; } + if (noMore.value == '没有更多了') { + return; + } var res = await ReplyHttp.replyList( oid: aid!, pageNum: currentPage + 1, diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 83744c47..149a2f30 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -33,6 +34,7 @@ class _VideoReplyPanelState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { late VideoReplyController _videoReplyController; late AnimationController fabAnimationCtr; + late ScrollController scrollController; Future? _futureBuilderFuture; bool _isFabVisible = true; @@ -60,18 +62,18 @@ class _VideoReplyPanelState extends State vsync: this, duration: const Duration(milliseconds: 300)); _futureBuilderFuture = _videoReplyController.queryReplyList(); - _videoReplyController.scrollController.addListener( + scrollController = _videoReplyController.scrollController; + scrollController.addListener( () { - if (_videoReplyController.scrollController.position.pixels >= - _videoReplyController.scrollController.position.maxScrollExtent - - 300) { - if (!_videoReplyController.isLoadingMore) { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 300) { + EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { _videoReplyController.onLoad(); - } + }); } final ScrollDirection direction = - _videoReplyController.scrollController.position.userScrollDirection; + scrollController.position.userScrollDirection; if (direction == ScrollDirection.forward) { _showFab(); } else if (direction == ScrollDirection.reverse) { @@ -112,7 +114,7 @@ class _VideoReplyPanelState extends State void dispose() { super.dispose(); fabAnimationCtr.dispose(); - _videoReplyController.scrollController.dispose(); + scrollController.dispose(); } @override diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 7aab2a13..abebf5cb 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -657,11 +657,13 @@ InlineSpan buildContent( ); if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) { - spanChilds.add(TextSpan( - text: str, - recognizer: TapGestureRecognizer() - ..onTap = () => - replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); + if (str != '') { + spanChilds.add(TextSpan( + text: str, + recognizer: TapGestureRecognizer() + ..onTap = () => + replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); + } } return str; }, diff --git a/lib/pages/video/detail/replyReply/view.dart b/lib/pages/video/detail/replyReply/view.dart index bad46268..0d86cb5b 100644 --- a/lib/pages/video/detail/replyReply/view.dart +++ b/lib/pages/video/detail/replyReply/view.dart @@ -38,6 +38,7 @@ class _VideoReplyReplyPanelState extends State { Box localCache = GStrorage.localCache; late double sheetHeight; Future? _futureBuilderFuture; + late ScrollController scrollController; @override void initState() { @@ -48,12 +49,11 @@ class _VideoReplyReplyPanelState extends State { super.initState(); // 上拉加载更多 - _videoReplyReplyController.scrollController.addListener( + scrollController = _videoReplyReplyController.scrollController; + scrollController.addListener( () { - if (_videoReplyReplyController.scrollController.position.pixels >= - _videoReplyReplyController - .scrollController.position.maxScrollExtent - - 300) { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 300) { if (!_videoReplyReplyController.isLoadingMore) { _videoReplyReplyController.onLoad(); } @@ -69,7 +69,7 @@ class _VideoReplyReplyPanelState extends State { @override void dispose() { - // _videoReplyReplyController.scrollController.dispose(); + // scrollController.dispose(); super.dispose(); } diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 353371cd..58281b69 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -120,15 +120,16 @@ class _HeaderControlState extends State { '当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}', style: subTitleStyle), ), - ListTile( - onTap: () => {Get.back(), showSetAudioQa()}, - dense: true, - leading: const Icon(Icons.album_outlined, size: 20), - title: Text('选择音质', style: titleStyle), - subtitle: Text( - '当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}', - style: subTitleStyle), - ), + if (widget.videoDetailCtr!.currentAudioQa != null) + ListTile( + onTap: () => {Get.back(), showSetAudioQa()}, + dense: true, + leading: const Icon(Icons.album_outlined, size: 20), + title: Text('选择音质', style: titleStyle), + subtitle: Text( + '当前音质 ${widget.videoDetailCtr!.currentAudioQa!.description}', + style: subTitleStyle), + ), ListTile( onTap: () => {Get.back(), showSetDecodeFormats()}, dense: true, @@ -319,7 +320,7 @@ class _HeaderControlState extends State { /// 选择音质 void showSetAudioQa() { - AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa; + AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa!; List audio = videoInfo.dash!.audio!; showModalBottomSheet( diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index b5b50152..03b99ee8 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_print 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:hive/hive.dart'; @@ -14,7 +15,7 @@ import 'package:webview_flutter/webview_flutter.dart'; class WebviewController extends GetxController { String url = ''; - String type = ''; + RxString type = ''.obs; String pageTitle = ''; final WebViewController controller = WebViewController(); RxInt loadProgress = 0.obs; @@ -25,10 +26,10 @@ class WebviewController extends GetxController { void onInit() { super.onInit(); url = Get.parameters['url']!; - type = Get.parameters['type']!; + type.value = Get.parameters['type']!; pageTitle = Get.parameters['pageTitle']!; - if (type == 'login') { + if (type.value == 'login') { controller.clearCache(); controller.clearLocalStorage(); WebViewCookieManager().clearCookies(); @@ -52,54 +53,11 @@ class WebviewController extends GetxController { onUrlChange: (UrlChange urlChange) async { loadShow.value = false; String url = urlChange.url ?? ''; - if (type == 'login' && + if (type.value == 'login' && (url.startsWith( 'https://passport.bilibili.com/web/sso/exchange_cookie') || url.startsWith('https://m.bilibili.com/'))) { - try { - await SetCookie.onSet(); - var result = await UserHttp.userInfo(); - UserHttp.thirdLogin(); - print('网页登录: $result'); - if (result['status'] && result['data'].isLogin) { - SmartDialog.showToast('登录成功'); - try { - Box user = GStrorage.user; - user.put(UserBoxKey.userLogin, true); - user.put(UserBoxKey.userName, result['data'].uname); - user.put(UserBoxKey.userFace, result['data'].face); - user.put(UserBoxKey.userMid, result['data'].mid); - - Box userInfoCache = GStrorage.userInfo; - userInfoCache.put('userInfoCache', result['data']); - - // 通知更新 - eventBus.emit(EventName.loginEvent, {'status': true}); - - HomeController homeCtr = Get.find(); - homeCtr.updateLoginStatus(true); - } catch (err) { - SmartDialog.show(builder: (context) { - return AlertDialog( - title: const Text('登录遇到问题'), - content: Text(err.toString()), - actions: [ - TextButton( - onPressed: () => controller.reload(), - child: const Text('确认'), - ) - ], - ); - }); - } - Get.back(); - } else { - // 获取用户信息失败 - SmartDialog.showToast(result.msg); - } - } catch (e) { - print(e); - } + confirmLogin(url); } }, onWebResourceError: (WebResourceError error) {}, @@ -113,4 +71,51 @@ class WebviewController extends GetxController { ) ..loadRequest(Uri.parse(url)); } + + confirmLogin(url) async { + var content = ''; + if (url != null) { + content = '${content + url}; \n'; + } + try { + await SetCookie.onSet(); + var result = await UserHttp.userInfo(); + UserHttp.thirdLogin(); + if (result['status'] && result['data'].isLogin) { + SmartDialog.showToast('登录成功'); + try { + Box userInfoCache = GStrorage.userInfo; + await userInfoCache.put('userInfoCache', result['data']); + + // 通知更新 + eventBus.emit(EventName.loginEvent, {'status': true}); + + HomeController homeCtr = Get.find(); + homeCtr.updateLoginStatus(true); + } catch (err) { + SmartDialog.show(builder: (context) { + return AlertDialog( + title: const Text('登录遇到问题'), + content: Text(err.toString()), + actions: [ + TextButton( + onPressed: () => controller.reload(), + child: const Text('确认'), + ) + ], + ); + }); + } + Get.back(); + } else { + // 获取用户信息失败 + SmartDialog.showToast(result.msg); + Clipboard.setData(ClipboardData(text: result.msg.toString())); + } + } catch (e) { + SmartDialog.showNotify(msg: e.toString(), notifyType: NotifyType.warning); + content = content + e.toString(); + } + Clipboard.setData(ClipboardData(text: content)); + } } diff --git a/lib/pages/webview/view.dart b/lib/pages/webview/view.dart index 71cc84fd..301b1dfa 100644 --- a/lib/pages/webview/view.dart +++ b/lib/pages/webview/view.dart @@ -18,19 +18,25 @@ class _WebviewPageState extends State { return Scaffold( appBar: AppBar( centerTitle: false, + titleSpacing: 0, title: Text( _webviewController.pageTitle, style: Theme.of(context).textTheme.titleMedium, ), actions: [ - IconButton( + TextButton( onPressed: () { _webviewController.controller.reload(); }, - icon: const Icon( - Icons.refresh, - size: 22, - ), + child: const Text('刷新'), + ), + Obx( + () => _webviewController.type.value == 'login' + ? TextButton( + onPressed: () => _webviewController.confirmLogin(null), + child: const Text('刷新登录状态'), + ) + : const SizedBox(), ), const SizedBox(width: 10) ], @@ -48,6 +54,14 @@ class _WebviewPageState extends State { ), ), ), + if (_webviewController.type.value == 'login') + Container( + width: double.infinity, + color: Theme.of(context).colorScheme.onInverseSurface, + padding: const EdgeInsets.only( + left: 12, right: 12, top: 6, bottom: 6), + child: const Text('登录成功未自动跳转? 请点击右上角「刷新登录状态」'), + ), Expanded( child: WebViewWidget(controller: _webviewController.controller), ), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 10e8f7dd..a72e0df2 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -46,6 +46,7 @@ class PlPlayerController { // 播放位置 final Rx _position = Rx(Duration.zero); final Rx _sliderPosition = Rx(Duration.zero); + // 展示使用 final Rx _sliderTempPosition = Rx(Duration.zero); final Rx _duration = Rx(Duration.zero); final Rx _buffered = Rx(Duration.zero); @@ -450,7 +451,7 @@ class PlPlayerController { } /// 跳转至指定位置 - Future seekTo(Duration position) async { + Future seekTo(Duration position, {type = 'seek'}) async { // if (position >= duration.value) { // position = duration.value - const Duration(milliseconds: 100); // } @@ -459,7 +460,10 @@ class PlPlayerController { } _position.value = position; if (duration.value.inSeconds != 0) { - await _videoPlayerController!.stream.buffer.first; + if (type != 'slider') { + /// 拖动进度条调节时,不等待第一帧,防止抖动 + await _videoPlayerController!.stream.buffer.first; + } await _videoPlayerController?.seek(position); // if (playerStatus.stopped) { // play(); diff --git a/lib/plugin/pl_player/index.dart b/lib/plugin/pl_player/index.dart index 05cdffad..9d711264 100644 --- a/lib/plugin/pl_player/index.dart +++ b/lib/plugin/pl_player/index.dart @@ -7,6 +7,8 @@ export './models/play_status.dart'; export './models/data_status.dart'; export './widgets/common_btn.dart'; export './models/play_speed.dart'; +export './models/fullscreen_mode.dart'; +export './models/bottom_progress_behavior.dart'; export './widgets/app_bar_ani.dart'; export './utils/fullscreen.dart'; export './utils.dart'; diff --git a/lib/plugin/pl_player/models/bottom_progress_behavior.dart b/lib/plugin/pl_player/models/bottom_progress_behavior.dart new file mode 100644 index 00000000..c632669c --- /dev/null +++ b/lib/plugin/pl_player/models/bottom_progress_behavior.dart @@ -0,0 +1,23 @@ +// ignore: camel_case_types +enum BtmProgresBehavior { + alwaysShow, + alwaysHide, + onlyShowFullScreen, +} + +extension BtmProgresBehaviorDesc on BtmProgresBehavior { + String get description => ['始终展示', '始终隐藏', '仅全屏时展示'][index]; +} + +extension BtmProgresBehaviorCode on BtmProgresBehavior { + static final List _codeList = [0, 1, 2]; + int get code => _codeList[index]; + + static BtmProgresBehavior? fromCode(int code) { + final index = _codeList.indexOf(code); + if (index != -1) { + return BtmProgresBehavior.values[index]; + } + return null; + } +} diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index cfb9dad8..7ba5de18 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -18,6 +18,7 @@ import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import 'models/bottom_progress_behavior.dart'; import 'utils/fullscreen.dart'; import 'widgets/app_bar_ani.dart'; import 'widgets/backward_seek.dart'; @@ -67,6 +68,7 @@ class _PLVideoPlayerState extends State Box setting = GStrorage.setting; late FullScreenMode mode; + late int defaultBtmProgressBehavior; void onDoubleTapSeekBackward() { setState(() { @@ -87,6 +89,8 @@ class _PLVideoPlayerState extends State vsync: this, duration: const Duration(milliseconds: 300)); videoController = widget.controller.videoController!; widget.controller.headerControl = widget.headerControl; + defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior, + defaultValue: BtmProgresBehavior.values.first.code); Future.microtask(() async { try { @@ -203,7 +207,7 @@ class _PLVideoPlayerState extends State systemOverlayStyle: SystemUiOverlayStyle.light, ), body: SafeArea( - bottom: true, + bottom: false, child: PLVideoPlayer( controller: _, headerControl: _.headerControl, @@ -229,6 +233,7 @@ class _PLVideoPlayerState extends State @override void dispose() { animationController.dispose(); + FlutterVolumeController.removeListener(); super.dispose(); } @@ -253,13 +258,16 @@ class _PLVideoPlayerState extends State clipBehavior: Clip.hardEdge, fit: StackFit.passthrough, children: [ - Video( - controller: videoController, - controls: NoVideoControls, - subtitleViewConfiguration: SubtitleViewConfiguration( - style: subTitleStyle, - textAlign: TextAlign.center, - padding: const EdgeInsets.all(24.0), + Obx( + () => Video( + controller: videoController, + controls: NoVideoControls, + subtitleViewConfiguration: SubtitleViewConfiguration( + style: subTitleStyle, + textAlign: TextAlign.center, + padding: const EdgeInsets.all(24.0), + ), + fit: _.videoFit.value, ), ), @@ -312,38 +320,40 @@ class _PLVideoPlayerState extends State curve: Curves.easeInOut, opacity: _.isSliderMoving.value ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: const Color(0x88000000), - borderRadius: BorderRadius.circular(64.0), - ), - height: 34.0, - width: 100.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Obx(() { - return Text( - _.sliderTempPosition.value.inMinutes >= 60 - ? printDurationWithHours( - _.sliderTempPosition.value) - : printDuration(_.sliderTempPosition.value), - style: textStyle, - ); - }), - const SizedBox(width: 2), - const Text('/', style: textStyle), - const SizedBox(width: 2), - Obx( - () => Text( - _.duration.value.inMinutes >= 60 - ? printDurationWithHours(_.duration.value) - : printDuration(_.duration.value), - style: textStyle, + child: IntrinsicWidth( + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0x88000000), + borderRadius: BorderRadius.circular(64.0), + ), + height: 34.0, + padding: const EdgeInsets.only(left: 10, right: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Obx(() { + return Text( + _.sliderTempPosition.value.inMinutes >= 60 + ? printDurationWithHours( + _.sliderTempPosition.value) + : printDuration(_.sliderTempPosition.value), + style: textStyle, + ); + }), + const SizedBox(width: 2), + const Text('/', style: textStyle), + const SizedBox(width: 2), + Obx( + () => Text( + _.duration.value.inMinutes >= 60 + ? printDurationWithHours(_.duration.value) + : printDuration(_.duration.value), + style: textStyle, + ), ), - ), - ], + ], + ), ), ), ), @@ -539,7 +549,7 @@ class _PLVideoPlayerState extends State return; } _.onChangedSliderEnd(); - _.seekTo(_.sliderPosition.value); + _.seekTo(_.sliderPosition.value, type: 'slider'); }, // 垂直方向 音量/亮度调节 onVerticalDragUpdate: (DragUpdateDetails details) async { @@ -620,6 +630,15 @@ class _PLVideoPlayerState extends State final int value = _.sliderPosition.value.inSeconds; final int max = _.duration.value.inSeconds; final int buffer = _.buffered.value.inSeconds; + if (defaultBtmProgressBehavior == + BtmProgresBehavior.alwaysHide.code) { + return Container(); + } + if (defaultBtmProgressBehavior == + BtmProgresBehavior.onlyShowFullScreen.code && + !_.isFullScreen.value) { + return Container(); + } if (value > max || max <= 0) { return Container(); } @@ -695,9 +714,19 @@ class _PLVideoPlayerState extends State Obx(() { if (_.dataStatus.loading || _.isBuffering.value) { return Center( - child: Image.asset( - 'assets/images/loading.gif', - height: 25, + child: Container( + padding: const EdgeInsets.all(30), + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + center: Alignment.center, + colors: [Colors.black26, Colors.transparent], + ), + ), + child: Image.asset( + 'assets/images/loading.gif', + height: 25, + ), ), ); } else { diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index 1cd4c20d..995aef59 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -44,29 +44,33 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { if (value > max || max <= 0) { return Container(); } - return ProgressBar( - progress: Duration(seconds: value), - buffered: Duration(seconds: buffer), - total: Duration(seconds: max), - progressBarColor: colorTheme, - baseBarColor: Colors.white.withOpacity(0.2), - bufferedBarColor: colorTheme.withOpacity(0.4), - timeLabelLocation: TimeLabelLocation.none, - thumbColor: colorTheme, - barHeight: 3.0, - thumbRadius: 5.5, - onDragStart: (duration) { - feedBack(); - _.onChangedSliderStart(); - }, - onDragUpdate: (duration) { - _.onUodatedSliderProgress(duration.timeStamp); - }, - onSeek: (duration) { - _.onChangedSliderEnd(); - _.onChangedSlider(duration.inSeconds.toDouble()); - _.seekTo(Duration(seconds: duration.inSeconds)); - }, + return Padding( + padding: const EdgeInsets.only(left: 5, right: 5, bottom: 5), + child: ProgressBar( + progress: Duration(seconds: value), + buffered: Duration(seconds: buffer), + total: Duration(seconds: max), + progressBarColor: colorTheme, + baseBarColor: Colors.white.withOpacity(0.2), + bufferedBarColor: colorTheme.withOpacity(0.4), + timeLabelLocation: TimeLabelLocation.none, + thumbColor: colorTheme, + barHeight: 3.0, + thumbRadius: 5.5, + onDragStart: (duration) { + feedBack(); + _.onChangedSliderStart(); + }, + onDragUpdate: (duration) { + _.onUodatedSliderProgress(duration.timeStamp); + }, + onSeek: (duration) { + _.onChangedSliderEnd(); + _.onChangedSlider(duration.inSeconds.toDouble()); + _.seekTo(Duration(seconds: duration.inSeconds), + type: 'slider'); + }, + ), ); }, ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index c1482f1a..f525e4e5 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -16,6 +16,7 @@ import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/searchResult/index.dart'; +import 'package:pilipala/pages/setting/extra_setting.dart'; import 'package:pilipala/pages/setting/play_setting.dart'; import 'package:pilipala/pages/setting/privacy_setting.dart'; import 'package:pilipala/pages/setting/style_setting.dart'; @@ -80,7 +81,8 @@ class Routes { GetPage(name: '/styleSetting', page: () => const StyleSetting()), // 隐私设置 GetPage(name: '/privacySetting', page: () => const PrivacySetting()), - + // 其他设置 + GetPage(name: '/extraSetting', page: () => const ExtraSetting()), // GetPage(name: '/blackListPage', page: () => const BlackListPage()), // 关于 diff --git a/lib/utils/data.dart b/lib/utils/data.dart index e4361f92..7cf00bae 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -10,8 +10,8 @@ class Data { static Future historyStatus() async { Box localCache = GStrorage.localCache; - Box user = GStrorage.user; - if (user.get(UserBoxKey.userMid) == null) { + Box userInfoCache = GStrorage.userInfo; + if (userInfoCache.get('userInfoCache') == null) { return; } var res = await UserHttp.historyStatus(); diff --git a/lib/utils/download.dart b/lib/utils/download.dart index 830464b2..ae89ef04 100644 --- a/lib/utils/download.dart +++ b/lib/utils/download.dart @@ -6,7 +6,7 @@ import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:permission_handler/permission_handler.dart'; class DownloadUtils { - // 获取存储全县 + // 获取存储权限 static requestStoragePer() async { Map statuses = await [ Permission.storage, diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 7fd8b3a5..b70589a5 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -7,10 +7,8 @@ import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/user/info.dart'; class GStrorage { - static late final Box user; static late final Box recVideo; static late final Box userInfo; - static late final Box hotKeyword; static late final Box historyword; static late final Box localCache; static late final Box setting; @@ -21,28 +19,24 @@ class GStrorage { final path = dir.path; await Hive.initFlutter('$path/hive'); regAdapter(); - // 用户信息 - user = await Hive.openBox('user'); // 首页推荐视频 recVideo = await Hive.openBox( 'recVideo', compactionStrategy: (entries, deletedEntries) { - return deletedEntries > 20; + return deletedEntries > 12; }, ); // 登录用户信息 - userInfo = await Hive.openBox('userInfo'); + userInfo = await Hive.openBox( + 'userInfo', + compactionStrategy: (entries, deletedEntries) { + return deletedEntries > 2; + }, + ); // 本地缓存 localCache = await Hive.openBox('localCache'); // 设置 setting = await Hive.openBox('setting'); - // 热搜关键词 - hotKeyword = await Hive.openBox( - 'hotKeyword', - compactionStrategy: (entries, deletedEntries) { - return deletedEntries > 10; - }, - ); // 搜索历史 historyword = await Hive.openBox( 'historyWord', @@ -70,14 +64,12 @@ class GStrorage { } static Future close() async { - user.compact(); - user.close(); + // user.compact(); + // user.close(); recVideo.compact(); recVideo.close(); userInfo.compact(); userInfo.close(); - hotKeyword.compact(); - hotKeyword.close(); historyword.compact(); historyword.close(); localCache.compact(); @@ -89,19 +81,6 @@ class GStrorage { } } -// 约定 key -class UserBoxKey { - static const String userName = 'userName'; - // 头像 - static const String userFace = 'userFace'; - // mid - static const String userMid = 'userMid'; - // 登录状态 - static const String userLogin = 'userLogin'; - // 凭证 - static const String accessKey = 'accessKey'; -} - class SettingBoxKey { static const String themeMode = 'themeMode'; static const String feedBackEnable = 'feedBackEnable'; @@ -119,11 +98,19 @@ class SettingBoxKey { static const String fullScreenMode = 'fullScreenMode'; static const String blackMidsList = 'blackMidsList'; + static const String autoUpdate = 'autoUpdate'; + static const String btmProgressBehavior = 'btmProgressBehavior'; } class LocalCacheKey { // 历史记录暂停状态 默认false 记录 static const String historyPause = 'historyPause'; + // access_key + static const String accessKey = 'accessKey'; + + // + static const String wbiKeys = 'wbiKeys'; + static const String timeStamp = 'timeStamp'; } class VideoBoxKey { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 4a539888..cb32b38e 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -4,8 +4,14 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get_utils/get_utils.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:pilipala/http/index.dart'; +import 'package:pilipala/models/github/latest.dart'; +import 'package:url_launcher/url_launcher.dart'; class Utils { static Future getCookiePath() async { @@ -194,4 +200,55 @@ class Utils { } return false; } + + // 检查更新 + static Future checkUpdata() async { + SmartDialog.dismiss(); + var currentInfo = await PackageInfo.fromPlatform(); + var result = await Request().get(Api.latestApp); + LatestDataModel data = LatestDataModel.fromJson(result.data); + bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!); + if (isUpdate) { + SmartDialog.show( + builder: (context) { + return AlertDialog( + title: const Text('🎉 发现新版本 '), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.tagName!, + style: const TextStyle(fontSize: 20), + ), + const SizedBox(height: 8), + Text(data.body!), + ], + ), + actions: [ + TextButton( + onPressed: () => SmartDialog.dismiss(), + child: Text( + '稍后', + style: + TextStyle(color: Theme.of(context).colorScheme.outline), + )), + TextButton( + onPressed: () async { + await SmartDialog.dismiss(); + launchUrl( + Uri.parse( + 'https://github.com/guozhigq/pilipala/releases'), + mode: LaunchMode.externalApplication, + ); + }, + child: const Text('去下载')), + ], + ); + }, + ); + } + return true; + } } diff --git a/lib/utils/wbi_sign.dart b/lib/utils/wbi_sign.dart index 24fd9e7c..84065964 100644 --- a/lib/utils/wbi_sign.dart +++ b/lib/utils/wbi_sign.dart @@ -109,8 +109,10 @@ class WbiSign { // 获取最新的 img_key 和 sub_key 可以从缓存中获取 static Future> getWbiKeys() async { DateTime nowDate = DateTime.now(); - if (localCache.get('wbiKeys') != null && - DateTime.fromMillisecondsSinceEpoch(localCache.get('timeStamp')).day == + if (localCache.get(LocalCacheKey.wbiKeys) != null && + DateTime.fromMillisecondsSinceEpoch( + localCache.get(LocalCacheKey.timeStamp)) + .day == nowDate.day) { Map cacheWbiKeys = localCache.get('wbiKeys'); return Map.from(cacheWbiKeys); @@ -129,8 +131,8 @@ class WbiSign { .substring(subUrl.lastIndexOf('/') + 1, subUrl.length) .split('.')[0] }; - localCache.put('wbiKeys', wbiKeys); - localCache.put('timeStamp', nowDate.millisecondsSinceEpoch); + localCache.put(LocalCacheKey.wbiKeys, wbiKeys); + localCache.put(LocalCacheKey.timeStamp, nowDate.millisecondsSinceEpoch); return wbiKeys; } diff --git a/pubspec.lock b/pubspec.lock index 198c430f..dd13be73 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,6 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.6" + easy_debounce: + dependency: "direct main" + description: + name: easy_debounce + sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236 + url: "https://pub.dev" + source: hosted + version: "2.0.3" extended_image: dependency: "direct main" description: @@ -467,6 +475,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.3+2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter @@ -813,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: "direct main" description: @@ -1282,6 +1306,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + url: "https://pub.dev" + source: hosted + version: "1.1.7" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0d53ab33..5d6fde5d 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.0+1 +version: 1.0.3 environment: sdk: ">=2.19.6 <3.0.0" @@ -114,6 +114,9 @@ dependencies: # 获取appx信息 package_info_plus: ^4.1.0 url_launcher: ^6.1.12 + flutter_svg: ^2.0.7 + # 防抖节流 + easy_debounce: ^2.0.3 dev_dependencies: flutter_test: