diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f119eb1e..c948bc8f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -47,13 +47,14 @@ + + + + + + + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a400600f..86cb7071 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - app_links (0.0.2): + - Flutter - appscheme (1.0.4): - Flutter - audio_service (0.0.1): @@ -27,6 +29,8 @@ PODS: - Flutter - GT3Captcha-iOS - GT3Captcha-iOS (0.15.8.3) + - image_picker_ios (0.0.1): + - Flutter - media_kit_libs_ios_video (1.0.4): - Flutter - media_kit_native_event_loop (1.0.0): @@ -66,6 +70,7 @@ PODS: - Flutter DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) - appscheme (from `.symlinks/plugins/appscheme/ios`) - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) @@ -77,6 +82,7 @@ DEPENDENCIES: - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) @@ -102,6 +108,8 @@ SPEC REPOS: - Toast EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" appscheme: :path: ".symlinks/plugins/appscheme/ios" audio_service: @@ -124,6 +132,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" gt3_flutter_plugin: :path: ".symlinks/plugins/gt3_flutter_plugin/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" media_kit_libs_ios_video: :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" media_kit_native_event_loop: @@ -160,6 +170,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 @@ -173,6 +184,7 @@ SPEC CHECKSUMS: FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1e7d9fed..24dceb17 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -65,44 +65,29 @@ CFBundleURLName - + bilibili CFBundleURLSchemes http https - - CFBundleURLTypes - - - CFBundleURLName - - CFBundleURLSchemes - - m.bilibili.com - bilibili.com - www.bilibili.com - bangumi.bilibili.com - bilibili.cn - www.bilibili.cn - bangumi.bilibili.cn - bilibili.tv - www.bilibili.tv - bangumi.bilibili.tv - miniapp.bilibili.com - live.bilibili.com - - - - - - - - CFBundleURLName - bilibili - CFBundleURLSchemes - bilibili + m.bilibili.com + bilibili.com + www.bilibili.com + bangumi.bilibili.com + bilibili.cn + www.bilibili.cn + bangumi.bilibili.cn + bilibili.tv + www.bilibili.tv + bangumi.bilibili.tv + miniapp.bilibili.com + live.bilibili.com + pili + pilipala + FlutterDeepLinkingEnabled + UIBackgroundModes diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 378c9f75..8cec3523 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -282,9 +282,10 @@ class VideoStat extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - StatView(view: videoItem.stat.view), + if (videoItem.stat.view != null) StatView(view: videoItem.stat.view), const SizedBox(width: 8), - StatDanMu(danmu: videoItem.stat.danmu), + if (videoItem.stat.danmu != null) + StatDanMu(danmu: videoItem.stat.danmu), if (videoItem is RecVideoItemModel) ...[ crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), RichText( diff --git a/lib/http/api.dart b/lib/http/api.dart index 3d2a7efa..3d292345 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -593,6 +593,15 @@ class Api { static const String liveRoomEntry = '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction'; + /// 用户信息 + static const String accountInfo = '/x/member/web/account'; + + /// 更新用户信息 + static const String updateAccountInfo = '/x/member/web/update'; + /// 删除评论 static const String replyDel = '/x/v2/reply/del'; + + /// 图片上传 + static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs'; } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 4b8061ad..c37a92cf 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -1,5 +1,8 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:image_picker/image_picker.dart'; import '../models/video/reply/data.dart'; import '../models/video/reply/emote.dart'; import 'api.dart'; @@ -131,4 +134,44 @@ class ReplyHttp { return {'status': false, 'msg': res.data['message']}; } } + + // 图片上传 + static Future uploadImage({required XFile xFile, String type = 'im'}) async { + var formData = FormData.fromMap({ + 'file_up': await xFileToMultipartFile(xFile), + 'biz': type, + 'csrf': await Request.getCsrf(), + 'build': 0, + 'mobi_app': 'web', + }); + var res = await Request().post( + Api.uploadImage, + data: formData, + ); + if (res.data['code'] == 0) { + var data = res.data['data']; + data['img_src'] = data['image_url']; + data['img_width'] = data['image_width']; + data['img_height'] = data['image_height']; + data.remove('image_url'); + data.remove('image_width'); + data.remove('image_height'); + return { + 'status': true, + 'data': data, + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } + + static Future xFileToMultipartFile(XFile xFile) async { + var file = File(xFile.path); + var bytes = await file.readAsBytes(); + return MultipartFile.fromBytes(bytes, filename: xFile.name); + } } diff --git a/lib/http/user.dart b/lib/http/user.dart index b4e52cde..63f6bf66 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,4 +1,6 @@ import 'dart:convert'; + +import 'package:dio/dio.dart'; import 'package:html/parser.dart'; import 'package:pilipala/models/video/later.dart'; import '../models/model_hot_video_item.dart'; @@ -465,4 +467,53 @@ class UserHttp { .toList() }; } + + static Future getAccountInfo() async { + var res = await Request().get( + Api.accountInfo, + data: {'web_location': 333.33}, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': {}, + 'mag': res.data['message'], + }; + } + } + + static Future updateAccountInfo({ + required String uname, + required String sign, + required String sex, + required String birthday, + }) async { + var res = await Request().post( + Api.updateAccountInfo, + data: { + 'uname': uname, + 'usersign': sign, + 'sex': sex, + 'birthday': birthday, + 'csrf': await Request.getCsrf(), + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'msg': '更新成功', + }; + } else { + return { + 'status': false, + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/video.dart b/lib/http/video.dart index 406e59f6..b3970a14 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,4 +1,6 @@ +import 'dart:convert'; import 'dart:developer'; +import 'package:dio/dio.dart'; import 'package:hive/hive.dart'; import '../common/constants.dart'; import '../models/common/reply_type.dart'; @@ -346,20 +348,34 @@ class VideoHttp { required String message, int? root, int? parent, + List>? pictures, }) async { if (message == '') { return {'status': false, 'data': [], 'msg': '请输入评论内容'}; } + var params = { + 'plat': 1, + 'oid': oid, + 'type': type.index, + // 'root': root == null || root == 0 ? '' : root, + // 'parent': parent == null || parent == 0 ? '' : parent, + 'message': message, + 'at_name_to_mid': {}, + if (pictures != null) 'pictures': jsonEncode(pictures), + 'gaia_source': 'main_web', + 'csrf': await Request.getCsrf(), + }; + Map sign = await WbiSign().makSign(params); + params.remove('wts'); + params.remove('w_rid'); + FormData formData = FormData.fromMap({...params}); var res = await Request().post( Api.replyAdd, - data: { - 'type': type.index, - 'oid': oid, - 'root': root == null || root == 0 ? '' : root, - 'parent': parent == null || parent == 0 ? '' : parent, - 'message': message, - 'csrf': await Request.getCsrf(), + queryParameters: { + 'w_rid': sign['w_rid'], + 'wts': sign['wts'], }, + data: formData, ); log(res.toString()); if (res.data['code'] == 0) { diff --git a/lib/main.dart b/lib/main.dart index fe93da22..3ac97b25 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -60,6 +60,7 @@ void main() async { systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent, statusBarColor: Colors.transparent, + systemNavigationBarContrastEnforced: false, )); } diff --git a/lib/models/common/nav_bar_config.dart b/lib/models/common/nav_bar_config.dart index 64cebafb..1c3a3438 100644 --- a/lib/models/common/nav_bar_config.dart +++ b/lib/models/common/nav_bar_config.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; - +import 'package:pilipala/pages/mine/index.dart'; import '../../pages/dynamics/index.dart'; import '../../pages/home/index.dart'; -import '../../pages/media/index.dart'; import '../../pages/rank/index.dart'; List defaultNavigationBars = [ @@ -51,15 +50,15 @@ List defaultNavigationBars = [ { 'id': 3, 'icon': const Icon( - Icons.video_collection_outlined, + Icons.person_outline, size: 20, ), 'selectIcon': const Icon( - Icons.video_collection, + Icons.person, size: 21, ), - 'label': "媒体库", + 'label': "我的", 'count': 0, - 'page': const MediaPage(), - } + 'page': const MinePage(), + }, ]; diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index f197fdfa..ea6e72ba 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -8,7 +8,7 @@ import 'package:pilipala/utils/storage.dart'; import '../../http/index.dart'; class HomeController extends GetxController with GetTickerProviderStateMixin { - bool flag = false; + bool flag = true; late RxList tabs = [].obs; RxInt initialIndex = 1.obs; late TabController tabController; diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 187ed6c3..2c32aa0d 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; -import 'package:pilipala/pages/mine/index.dart'; +import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/utils/feed_back.dart'; import './controller.dart'; @@ -19,6 +19,8 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { final HomeController _homeController = Get.put(HomeController()); + final MainController mainController = Get.put(MainController()); + List videoList = []; late Stream stream; @@ -33,15 +35,14 @@ class _HomePageState extends State showUserBottomSheet() { feedBack(); - showModalBottomSheet( - context: context, - builder: (_) => const SizedBox( - height: 450, - child: MinePage(), - ), - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - ); + final MainController mainController = Get.put(MainController()); + int mineItemIndex = mainController.navigationBars + .indexWhere((item) => item['label'] == "我的"); + if (mineItemIndex != -1) { + mainController.pageController.jumpToPage(mineItemIndex); + } else { + Get.toNamed('/mine'); + } } @override diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index 36920aef..35cb9d4e 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; -import 'package:pilipala/pages/mine/view.dart'; import 'package:pilipala/utils/storage.dart'; Box userInfoCache = GStrorage.userInfo; @@ -54,19 +53,9 @@ class HomeAppBar extends StatelessWidget { // icon: const Icon(CupertinoIcons.bell, size: 22), // ), const SizedBox(width: 6), - - /// TODO if (userInfo != null) ...[ GestureDetector( - onTap: () => showModalBottomSheet( - context: context, - builder: (_) => const SizedBox( - height: 450, - child: MinePage(), - ), - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - ), + onTap: () => Get.toNamed('/mine'), child: NetworkImgLayer( type: 'avatar', width: 32, @@ -77,15 +66,7 @@ class HomeAppBar extends StatelessWidget { const SizedBox(width: 10), ] else ...[ IconButton( - onPressed: () => showModalBottomSheet( - context: context, - builder: (_) => const SizedBox( - height: 450, - child: MinePage(), - ), - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - ), + onPressed: () => Get.toNamed('/mine'), icon: const Icon(CupertinoIcons.person, size: 22), ), ], diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index fd18d728..37c8d174 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -5,6 +5,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/http/common.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; @@ -25,6 +26,7 @@ class MainController extends GetxController { late PageController pageController; int selectedIndex = 0; Box userInfoCache = GStrorage.userInfo; + dynamic userInfo; RxBool userLogin = false.obs; late Rx dynamicBadgeType = DynamicBadgeMode.number.obs; late bool enableGradientBg; @@ -38,7 +40,7 @@ class MainController extends GetxController { } hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: false); - var userInfo = userInfoCache.get('userInfoCache'); + userInfo = userInfoCache.get('userInfoCache'); userLogin.value = userInfo != null; dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( SettingBoxKey.dynamicBadgeMode, @@ -73,12 +75,20 @@ class MainController extends GetxController { } int dynamicItemIndex = navigationBars.indexWhere((item) => item['label'] == "动态"); + int mineItemIndex = + navigationBars.indexWhere((item) => item['label'] == "我的"); var res = await CommonHttp.unReadDynamic(); var data = res['data']; if (dynamicItemIndex != -1) { navigationBars[dynamicItemIndex]['count'] = data == null ? 0 : data.length; // 修改 count 属性为新的值 } + if (mineItemIndex != -1 && userInfo != null) { + Widget avatar = NetworkImgLayer( + width: 28, height: 28, src: userInfo.face, type: 'avatar'); + navigationBars[mineItemIndex]['icon'] = avatar; + navigationBars[mineItemIndex]['selectIcon'] = avatar; + } navigationBars.refresh(); } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 6d194960..82df1af9 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/common/dynamic_badge_mode.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/index.dart'; -import 'package:pilipala/pages/media/index.dart'; +import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/rank/index.dart'; import 'package:pilipala/utils/event_bus.dart'; import 'package:pilipala/utils/feed_back.dart'; @@ -26,7 +27,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { late HomeController _homeController; RankController? _rankController; late DynamicsController _dynamicController; - late MediaController _mediaController; + late MineController _mineController; int? _lastSelectTime; //上次点击时间 Box setting = GStrorage.setting; @@ -92,24 +93,22 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { _dynamicController.flag = false; } - if (currentPage is MediaPage) { - _mediaController.queryFavFolder(); + if (currentPage is MinePage) { + _mineController.queryFavFolder(); + _mineController.queryUserInfo(); } } void controllerInit() { _homeController = Get.put(HomeController()); _dynamicController = Get.put(DynamicsController()); - _mediaController = Get.put(MediaController()); + _mineController = Get.put(MineController()); if (_mainController.pagesIds.contains(1)) { _rankController = Get.put(RankController()); } if (_mainController.pagesIds.contains(2)) { _dynamicController = Get.put(DynamicsController()); } - if (_mainController.pagesIds.contains(3)) { - _mediaController = Get.put(MediaController()); - } } @override @@ -129,6 +128,14 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { GlobalDataCache().sheetHeight = sheetHeight; localCache.put('sheetHeight', sheetHeight); localCache.put('statusBarHeight', statusBarHeight); + + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: + Get.isDarkMode ? Brightness.light : Brightness.dark, + ), + ); return PopScope( canPop: false, onPopInvoked: (bool didPop) async { @@ -198,20 +205,21 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { destinations: [ ..._mainController.navigationBars.map((e) { return NavigationDestination( - icon: Badge( - label: _mainController - .dynamicBadgeType.value == - DynamicBadgeMode.number - ? Text(e['count'].toString()) - : null, - padding: - const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: _mainController - .dynamicBadgeType.value != - DynamicBadgeMode.hidden && - e['count'] > 0, - child: e['icon'], - ), + icon: _mainController + .dynamicBadgeType.value == + DynamicBadgeMode.number + ? Badge( + label: Text(e['count'].toString()), + padding: const EdgeInsets.fromLTRB( + 6, 0, 6, 0), + isLabelVisible: _mainController + .dynamicBadgeType + .value != + DynamicBadgeMode.hidden && + e['count'] > 0, + child: e['icon'], + ) + : e['icon'], selectedIcon: e['selectIcon'], label: e['label'], ); diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart deleted file mode 100644 index 757d5ac9..00000000 --- a/lib/pages/media/controller.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; -import 'package:hive/hive.dart'; -import 'package:pilipala/http/user.dart'; -import 'package:pilipala/models/user/fav_folder.dart'; -import 'package:pilipala/utils/storage.dart'; - -class MediaController extends GetxController { - Rx favFolderData = FavFolderData().obs; - Box userInfoCache = GStrorage.userInfo; - RxBool userLogin = false.obs; - List list = [ - { - 'icon': Icons.file_download_outlined, - 'title': '离线缓存', - 'onTap': () { - SmartDialog.showToast('功能开发中'); - }, - }, - { - 'icon': Icons.history, - 'title': '观看记录', - 'onTap': () => Get.toNamed('/history'), - }, - { - 'icon': Icons.star_border, - 'title': '我的收藏', - 'onTap': () => Get.toNamed('/fav'), - }, - { - 'icon': Icons.subscriptions_outlined, - 'title': '我的订阅', - 'onTap': () => Get.toNamed('/subscription'), - }, - { - 'icon': Icons.watch_later_outlined, - 'title': '稍后再看', - 'onTap': () => Get.toNamed('/later'), - }, - ]; - var userInfo; - int? mid; - final ScrollController scrollController = ScrollController(); - - @override - void onInit() { - super.onInit(); - userInfo = userInfoCache.get('userInfoCache'); - userLogin.value = userInfo != null; - } - - Future queryFavFolder() async { - if (!userLogin.value) { - return {'status': false, 'data': [], 'msg': '未登录'}; - } - var res = await await UserHttp.userfavFolder( - pn: 1, - ps: 5, - mid: 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 deleted file mode 100644 index 965628c4..00000000 --- a/lib/pages/media/view.dart +++ /dev/null @@ -1,296 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:pilipala/common/widgets/network_img_layer.dart'; -import 'package:pilipala/models/user/fav_folder.dart'; -import 'package:pilipala/pages/media/index.dart'; -import 'package:pilipala/utils/utils.dart'; - -class MediaPage extends StatefulWidget { - const MediaPage({super.key}); - - @override - State createState() => _MediaPageState(); -} - -class _MediaPageState extends State - with AutomaticKeepAliveClientMixin { - late MediaController mediaController; - late Future _futureBuilderFuture; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - mediaController = Get.put(MediaController()); - _futureBuilderFuture = mediaController.queryFavFolder(); - mediaController.userLogin.listen((status) { - setState(() { - _futureBuilderFuture = mediaController.queryFavFolder(); - }); - }); - } - - @override - void dispose() { - mediaController.scrollController.removeListener(() {}); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - Color primary = Theme.of(context).colorScheme.primary; - return Scaffold( - appBar: AppBar(toolbarHeight: 30), - body: SingleChildScrollView( - controller: mediaController.scrollController, - child: Column( - children: [ - ListTile( - leading: null, - title: Padding( - padding: const EdgeInsets.only(left: 20), - child: Text( - '媒体库', - style: TextStyle( - fontSize: Theme.of(context).textTheme.titleLarge!.fontSize, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - for (var i in mediaController.list) ...[ - ListTile( - onTap: () => i['onTap'](), - dense: true, - leading: Padding( - padding: const EdgeInsets.only(left: 15), - child: Icon( - i['icon'], - color: primary, - ), - ), - contentPadding: - const EdgeInsets.only(left: 15, top: 2, bottom: 2), - minLeadingWidth: 0, - title: Text( - i['title'], - style: const TextStyle(fontSize: 15), - ), - ), - ], - Obx(() => mediaController.userLogin.value - ? favFolder(mediaController, context) - : const SizedBox()), - SizedBox( - height: MediaQuery.of(context).padding.bottom + - kBottomNavigationBarHeight, - ) - ], - ), - ), - ); - } - - Widget favFolder(mediaController, context) { - return Column( - children: [ - Divider( - height: 35, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ), - ListTile( - onTap: () => Get.toNamed('/fav'), - leading: null, - dense: true, - title: Padding( - padding: const EdgeInsets.only(left: 10), - child: Obx( - () => Text.rich( - TextSpan( - children: [ - TextSpan( - text: '收藏夹 ', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.titleMedium!.fontSize, - fontWeight: FontWeight.bold), - ), - if (mediaController.favFolderData.value.count != null) - TextSpan( - text: mediaController.favFolderData.value.count - .toString(), - style: TextStyle( - fontSize: - Theme.of(context).textTheme.titleSmall!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - ), - ), - ), - trailing: IconButton( - onPressed: () { - setState(() { - _futureBuilderFuture = mediaController.queryFavFolder(); - }); - }, - icon: const Icon( - Icons.refresh, - size: 20, - ), - ), - ), - // const SizedBox(height: 10), - SizedBox( - width: double.infinity, - height: MediaQuery.textScalerOf(context).scale(200), - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data == null) { - return const SizedBox(); - } - Map data = snapshot.data as Map; - if (data['status']) { - List favFolderList = - mediaController.favFolderData.value.list!; - int favFolderCount = - mediaController.favFolderData.value.count!; - bool flag = favFolderCount > favFolderList.length; - return Obx(() => ListView.builder( - itemCount: - mediaController.favFolderData.value.list!.length + - (flag ? 1 : 0), - itemBuilder: (context, index) { - if (flag && index == favFolderList.length) { - return Padding( - padding: const EdgeInsets.only( - 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) - .colorScheme - .primary, - ), - ), - )); - } else { - return FavFolderItem( - item: mediaController - .favFolderData.value.list![index], - index: index); - } - }, - scrollDirection: Axis.horizontal, - )); - } else { - return SizedBox( - height: 160, - child: Center(child: Text(data['msg'])), - ); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }), - ), - ], - ); - } -} - -class FavFolderItem extends StatelessWidget { - const FavFolderItem({super.key, this.item, this.index}); - final FavFolderItemData? item; - final int? index; - @override - Widget build(BuildContext context) { - String heroTag = Utils.makeHeroTag(item!.fid); - - return Container( - margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14), - child: GestureDetector( - onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: { - 'mediaId': item!.id.toString(), - 'heroTag': heroTag, - 'isOwner': '1', - }), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Container( - width: 180, - height: 110, - margin: const EdgeInsets.only(bottom: 8), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Theme.of(context).colorScheme.onInverseSurface, - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onInverseSurface, - offset: const Offset(4, -12), // 阴影与容器的距离 - blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 - spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 - ), - ], - ), - child: LayoutBuilder( - builder: (context, BoxConstraints box) { - return Hero( - tag: heroTag, - child: NetworkImgLayer( - src: item!.cover, - width: box.maxWidth, - height: box.maxHeight, - ), - ); - }, - ), - ), - Text( - ' ${item!.title}', - overflow: TextOverflow.fade, - maxLines: 1, - ), - Text( - ' 共${item!.mediaCount}条视频', - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith(color: Theme.of(context).colorScheme.outline), - ) - ], - ), - ), - ); - } -} diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 8454cebe..6987337b 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -32,7 +32,7 @@ class MemberController extends GetxController { @override void onInit() { super.onInit(); - mid = int.parse(Get.parameters['mid']!); + mid = int.tryParse(Get.parameters['mid']!) ?? -2; userInfo = userInfoCache.get('userInfoCache'); ownerMid = userInfo != null ? userInfo.mid : -1; isOwner.value = mid == ownerMid; @@ -43,6 +43,11 @@ class MemberController extends GetxController { // 获取用户信息 Future> getInfo() async { + if (mid == -2) { + SmartDialog.showToast('用户ID获取异常'); + return {'status': false, 'msg': '用户ID获取异常'}; + } + await getMemberStat(); await getMemberView(); var res = await MemberHttp.memberInfo(mid: mid); diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index df501253..4ebc6153 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; @@ -36,7 +37,7 @@ class _MemberPageState extends State @override void initState() { super.initState(); - mid = int.parse(Get.parameters['mid']!); + mid = int.tryParse(Get.parameters['mid']!) ?? -1; heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid); _memberController = Get.put(MemberController(), tag: heroTag); _futureBuilderFuture = _memberController.getInfo(); @@ -100,8 +101,14 @@ class _MemberPageState extends State ), actions: [ IconButton( - onPressed: () => Get.toNamed( - '/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'), + onPressed: () { + if (mid == -1) { + SmartDialog.showToast('用户ID获取异常'); + return; + } + Get.toNamed( + '/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name ?? ''}'); + }, icon: const Icon(Icons.search_outlined), ), PopupMenuButton( diff --git a/lib/pages/member/widgets/profile.dart b/lib/pages/member/widgets/profile.dart index 7001c886..e06309d0 100644 --- a/lib/pages/member/widgets/profile.dart +++ b/lib/pages/member/widgets/profile.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/member/info.dart'; +import 'package:pilipala/plugin/pl_gallery/index.dart'; import 'package:pilipala/utils/utils.dart'; class ProfilePanel extends StatelessWidget { @@ -71,11 +71,27 @@ class ProfilePanel extends StatelessWidget { tag: ctr.heroTag!, child: Stack( children: [ - NetworkImgLayer( - width: 90, - height: 90, - type: 'avatar', - src: !loadingStatus ? memberInfo.face : ctr.face.value, + InkWell( + onTap: () { + Navigator.of(context).push( + HeroDialogRoute( + builder: (BuildContext context) => + InteractiveviewerGallery( + sources: [ + !loadingStatus ? memberInfo.face : ctr.face.value + ], + initIndex: 0, + onPageChanged: (int pageIndex) {}, + ), + ), + ); + }, + child: NetworkImgLayer( + width: 90, + height: 90, + type: 'avatar', + src: !loadingStatus ? memberInfo.face : ctr.face.value, + ), ), if (!loadingStatus && memberInfo.liveRoom != null && @@ -237,7 +253,7 @@ class ProfilePanel extends StatelessWidget { Widget buildEditProfileButton(BuildContext context) { return TextButton( onPressed: () { - SmartDialog.showToast('功能开发中 💪'); + Get.toNamed('/mineEdit'); }, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 80), diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index e682a7e5..24401711 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -4,22 +4,45 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/models/common/theme_type.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/utils/storage.dart'; class MineController extends GetxController { - // 用户信息 头像、昵称、lv - Rx userInfo = UserInfoData().obs; + RxBool userLogin = false.obs; // 用户状态 动态、关注、粉丝 Rx userStat = UserStat().obs; - RxBool userLogin = false.obs; - Box userInfoCache = GStrorage.userInfo; - Box setting = GStrorage.setting; + // 用户信息 头像、昵称、lv + Rx userInfo = UserInfoData().obs; Rx themeType = ThemeType.system.obs; - + Rx favFolderData = FavFolderData().obs; + Box setting = GStrorage.setting; + Box userInfoCache = GStrorage.userInfo; + List menuList = [ + { + 'icon': Icons.history, + 'title': '观看记录', + 'onTap': () => Get.toNamed('/history'), + }, + { + 'icon': Icons.star_border, + 'title': '我的收藏', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.subscriptions_outlined, + 'title': '我的订阅', + 'onTap': () => Get.toNamed('/subscription'), + }, + { + 'icon': Icons.watch_later_outlined, + 'title': '稍后再看', + 'onTap': () => Get.toNamed('/later'), + }, + ]; @override - onInit() { + void onInit() { super.onInit(); if (userInfoCache.get('userInfoCache') != null) { @@ -109,7 +132,10 @@ class MineController extends GetxController { SmartDialog.showToast('账号未登录'); return; } - Get.toNamed('/follow?mid=${userInfo.value.mid}', preventDuplicates: false); + Get.toNamed( + '/follow?mid=${userInfo.value.mid}', + preventDuplicates: false, + ); } pushFans() { @@ -117,7 +143,10 @@ class MineController extends GetxController { SmartDialog.showToast('账号未登录'); return; } - Get.toNamed('/fan?mid=${userInfo.value.mid}', preventDuplicates: false); + Get.toNamed( + '/fan?mid=${userInfo.value.mid}', + preventDuplicates: false, + ); } pushDynamic() { @@ -125,7 +154,22 @@ class MineController extends GetxController { SmartDialog.showToast('账号未登录'); return; } - Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}', - preventDuplicates: false); + Get.toNamed( + '/memberDynamics?mid=${userInfo.value.mid}', + preventDuplicates: false, + ); + } + + Future queryFavFolder() async { + if (!userLogin.value) { + return {'status': false, 'data': [], 'msg': '未登录'}; + } + var res = await await UserHttp.userfavFolder( + pn: 1, + ps: 5, + mid: userInfo.value.mid!, + ); + favFolderData.value = res['data']; + return res; } } diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 091b2149..b96c095a 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -1,12 +1,13 @@ -// ignore_for_file: no_leading_underscores_for_local_identifiers - -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 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/fav_folder.dart'; import 'package:pilipala/models/user/info.dart'; +import 'package:pilipala/models/user/stat.dart'; +import 'package:pilipala/utils/utils.dart'; import 'controller.dart'; class MinePage extends StatefulWidget { @@ -16,19 +17,23 @@ class MinePage extends StatefulWidget { State createState() => _MinePageState(); } -class _MinePageState extends State { - final MineController mineController = Get.put(MineController()); +class _MinePageState extends State + with AutomaticKeepAliveClientMixin { + final MineController ctr = Get.put(MineController()); late Future _futureBuilderFuture; + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); - _futureBuilderFuture = mineController.queryUserInfo(); - - mineController.userLogin.listen((status) { + _futureBuilderFuture = ctr.queryUserInfo(); + ctr.queryFavFolder(); + ctr.userLogin.listen((status) { if (mounted) { setState(() { - _futureBuilderFuture = mineController.queryUserInfo(); + _futureBuilderFuture = ctr.queryUserInfo(); }); } }); @@ -36,50 +41,44 @@ class _MinePageState extends State { @override Widget build(BuildContext context) { + super.build(context); return Scaffold( appBar: AppBar( - automaticallyImplyLeading: false, scrolledUnderElevation: 0, elevation: 0, - toolbarHeight: kTextTabBarHeight + 20, - backgroundColor: Colors.transparent, - centerTitle: false, - title: const Text( - 'PLPL', - style: TextStyle( - height: 2.8, - fontSize: 17, - fontWeight: FontWeight.bold, - fontFamily: 'Jura-Bold', - ), - ), actions: [ IconButton( - onPressed: () => mineController.onChangeTheme(), - icon: Icon( - mineController.themeType.value == ThemeType.dark - ? CupertinoIcons.sun_max - : CupertinoIcons.moon, - size: 22, - ), + icon: const Icon(Icons.search_outlined), + onPressed: () => Get.toNamed('/search'), ), IconButton( - onPressed: () => Get.toNamed('/setting', preventDuplicates: false), - icon: const Icon( - CupertinoIcons.slider_horizontal_3, + icon: Icon( + ctr.themeType.value == ThemeType.dark + ? Icons.wb_sunny_outlined + : Icons.dark_mode_outlined, ), + onPressed: () => ctr.onChangeTheme(), ), - const SizedBox(width: 10), + IconButton( + icon: const Icon(Icons.settings_outlined), + onPressed: () => Get.toNamed('/setting', preventDuplicates: false), + ), + const SizedBox(width: 22), ], ), - body: LayoutBuilder( - builder: (context, constraint) { - return SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: SizedBox( - height: constraint.maxHeight, + body: RefreshIndicator( + onRefresh: () async { + await ctr.queryUserInfo(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics()), + child: Padding( + padding: const EdgeInsets.only(bottom: 110), + child: Expanded( child: Column( children: [ + Obx(() => _buildProfileSection(context, ctr.userInfo.value)), const SizedBox(height: 10), FutureBuilder( future: _futureBuilderFuture, @@ -90,275 +89,424 @@ class _MinePageState extends State { } if (snapshot.data['status']) { return Obx( - () => userInfoBuild(mineController, context)); + () => _buildStatsSection( + context, + ctr.userStat.value, + ), + ); } else { - return userInfoBuild(mineController, context); + return _buildStatsSection( + context, + ctr.userStat.value, + ); } } else { - return userInfoBuild(mineController, context); + return _buildStatsSection( + context, + ctr.userStat.value, + ); } }, ), + _buildMenuSection(context), + Obx( + () => Visibility( + visible: ctr.userLogin.value, + child: Divider( + height: 25, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + ), + Obx( + () => ctr.userLogin.value + ? _buildFavoritesSection(context) + : const SizedBox(), + ), + SizedBox( + height: MediaQuery.of(context).padding.bottom + + kBottomNavigationBarHeight, + ) ], ), ), + ), + ), + ), + ); + } + + Widget _buildProfileSection(BuildContext context, UserInfoData userInfo) { + return InkWell( + onTap: () => ctr.onLogin(), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 10, 30, 10), + child: Row( + children: [ + userInfo.face != null + ? NetworkImgLayer( + src: userInfo.face, + width: 85, + height: 85, + type: 'avatar', + ) + : ClipOval( + child: SizedBox( + width: 85, + height: 85, + child: Image.asset('assets/images/noface.jpeg'), + ), + ), + const SizedBox(width: 12), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + userInfo.uname ?? '去登录', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: userInfo.vipStatus == 1 + ? const Color.fromARGB(255, 251, 100, 163) + : null, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${userInfo.levelInfo != null ? userInfo.levelInfo!.currentLevel : '0'}.png', + height: 12, + ), + ], + ), + const SizedBox(height: 2), + if (userInfo.vipType != 0 && userInfo.vipStatus == 1) ...[ + Image.network( + userInfo.vipLabel!['img_label_uri_hans_static'], + height: 22, + ), + const SizedBox(height: 2), + ], + Text.rich( + TextSpan(children: [ + TextSpan( + text: '硬币: ', + style: TextStyle( + color: Theme.of(context).colorScheme.outline)), + TextSpan( + text: (userInfo.money ?? '-').toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.primary)), + ]), + ) + ], + ), + const Spacer(), + Icon( + Icons.keyboard_arrow_right_rounded, + size: 28, + color: Theme.of(context).colorScheme.outline, + ), + ], + ), + ), + ); + } + + Widget _buildStatsSection(BuildContext context, UserStat userStat) { + return Padding( + padding: const EdgeInsets.only(left: 30, right: 30), + child: LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + height: constraints.maxWidth / 3 * 0.6, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 3, + childAspectRatio: 1.67, + children: [ + _buildStatItem( + context, + (userStat.dynamicCount ?? '-').toString(), + '动态', + ctr.pushDynamic, + ), + _buildStatItem( + context, + (userStat.following ?? '-').toString(), + '关注', + ctr.pushFollow, + ), + _buildStatItem( + context, + (userStat.follower ?? '-').toString(), + '粉丝', + ctr.pushFans, + ), + ], + ), ); }, ), ); } - Widget userInfoBuild(_mineController, context) { - return Column( - children: [ - const SizedBox(height: 5), - GestureDetector( - onTap: () => _mineController.onLogin(), - child: ClipOval( - child: Container( - width: 85, - height: 85, - color: Theme.of(context).colorScheme.onInverseSurface, - child: Center( - child: _mineController.userInfo.value.face != null - ? NetworkImgLayer( - src: _mineController.userInfo.value.face, - width: 85, - height: 85) - : Image.asset('assets/images/noface.jpeg'), - ), - ), - ), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - _mineController.userInfo.value.uname ?? '点击头像登录', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(width: 4), - Image.asset( - 'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png', - height: 10, - ), - ], - ), - const SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich(TextSpan(children: [ - TextSpan( - text: '硬币: ', - style: - TextStyle(color: Theme.of(context).colorScheme.outline)), - TextSpan( - text: (_mineController.userInfo.value.money ?? 'pilipala') - .toString(), - style: - TextStyle(color: Theme.of(context).colorScheme.primary)), - ])) - ], - ), - 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, - child: Stack( - children: [ - Positioned( - top: 0, - right: 0, - bottom: 0, - child: Container( - color: Theme.of(context).colorScheme.primary, - height: 24, - constraints: - const BoxConstraints(minWidth: 100), // 设置最小宽度为100 - width: box.maxWidth * - (1 - (levelInfo.currentExp! / levelInfo.nextExp!)), - child: Center( - child: Text( - '${levelInfo.currentExp!}/${levelInfo.nextExp!}', - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontSize: 12, - ), - ), - ), - ), - ), - Positioned( - top: 23, - left: 0, - bottom: 0, - child: Container( - width: box.maxWidth * - (_mineController - .userInfo.value.levelInfo!.currentExp! / - _mineController - .userInfo.value.levelInfo!.nextExp!), - height: 1, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], + Widget _buildStatItem( + BuildContext context, + String count, + String label, + Function onTap, + ) { + TextStyle style = TextStyle( + fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, + fontWeight: FontWeight.bold); + return InkWell( + onTap: () => onTap(), + // onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(count, style: style), + const SizedBox(height: 2), + Text( + label, + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.outline, ), - ); - }, ), ], - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.only(left: 12, right: 12), - child: LayoutBuilder( - builder: (context, constraints) { - TextStyle style = TextStyle( - fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold); - return SizedBox( - height: constraints.maxWidth / 3 * 0.6, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 3, - childAspectRatio: 1.67, - children: [ - InkWell( - onTap: () => _mineController.pushDynamic(), - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition( - scale: animation, child: child); - }, - child: Text( - (_mineController.userStat.value.dynamicCount ?? - '-') - .toString(), - key: ValueKey(_mineController - .userStat.value.dynamicCount - .toString()), - style: style), + ), + ); + } + + Widget _buildMenuSection(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return SizedBox( + height: constraints.maxWidth / 4 * 0.85, + child: GridView.count( + primary: false, + crossAxisCount: 4, + padding: const EdgeInsets.all(0), + childAspectRatio: 1.2, + children: [ + ...ctr.menuList.map((element) { + return InkWell( + onTap: () { + if (!ctr.userLogin.value) { + SmartDialog.showToast('账号未登录'); + } else { + element['onTap'](); + } + }, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Icon( + element['icon'], + size: 21, + color: Theme.of(context).colorScheme.primary, ), - const SizedBox(height: 8), - Text( - '动态', - style: Theme.of(context).textTheme.labelMedium, - ), - ], - ), + ), + const SizedBox(height: 4), + Text(element['title']) + ], ), - InkWell( - onTap: () => _mineController.pushFollow(), - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition( - scale: animation, child: child); - }, - child: Text( - (_mineController.userStat.value.following ?? - '-') - .toString(), - key: ValueKey(_mineController - .userStat.value.following - .toString()), - style: style), - ), - const SizedBox(height: 8), - Text( - '关注', - style: Theme.of(context).textTheme.labelMedium, - ), - ], - ), + ); + }).toList(), + ], + ), + ); + }, + ), + ); + } + + Widget _buildFavoritesSection(context) { + return Column( + children: [ + _buildFavoritesTitle(context, 'fav', '收藏夹'), + const SizedBox(height: 4), + SizedBox( + width: double.infinity, + height: MediaQuery.textScalerOf(context).scale(180), + child: FutureBuilder( + future: ctr.queryFavFolder(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map? data = snapshot.data; + if (data != null && data['status']) { + List favFolderList = ctr.favFolderData.value.list!; + int favFolderCount = ctr.favFolderData.value.count!; + bool flag = favFolderCount > favFolderList.length; + return Obx( + () => ListView.builder( + itemCount: + ctr.favFolderData.value.list!.length + (flag ? 1 : 0), + itemBuilder: (context, index) { + if (flag && index == favFolderList.length) { + return Padding( + padding: const EdgeInsets.only(right: 14), + 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).colorScheme.primary, + ), + ), + ), + ); + } else { + return FavFolderItem( + item: ctr.favFolderData.value.list![index], + index: index, + ); + } + }, + scrollDirection: Axis.horizontal, ), - InkWell( - onTap: () => _mineController.pushFans(), - borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition( - scale: animation, child: child); - }, - child: Text( - (_mineController.userStat.value.follower ?? '-') - .toString(), - key: ValueKey(_mineController - .userStat.value.follower - .toString()), - style: style), - ), - const SizedBox(height: 8), - Text( - '粉丝', - style: Theme.of(context).textTheme.labelMedium, - ), - ], - ), - ), - ], - ), - ); + ); + } else { + return SizedBox( + height: 110, + child: Center(child: Text(data?['msg'] ?? '')), + ); + } + } else { + // 骨架屏 + return Obx( + () => ctr.favFolderData.value.list != null + ? ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: ctr.favFolderData.value.list!.length, + itemBuilder: (context, index) { + return FavFolderItem( + item: ctr.favFolderData.value.list![index], + index: index, + ); + }, + ) + : const SizedBox(), + ); + } }, ), ), ], ); } + + Widget _buildFavoritesTitle( + BuildContext context, + String type, + String title, + ) { + return ListTile( + onTap: () => Get.toNamed('/fav'), + leading: null, + dense: true, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: title, + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, + fontWeight: FontWeight.bold), + ), + const TextSpan(text: ' '), + TextSpan( + text: '20', + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + // trailing: IconButton( + // onPressed: () {}, + // icon: const Icon( + // Icons.refresh, + // size: 20, + // ), + // ), + ); + } } -class ActionItem extends StatelessWidget { - final Icon? icon; - final Function? onTap; - final String? text; - - const ActionItem({ - Key? key, - this.icon, - this.onTap, - this.text, - }) : super(key: key); - +class FavFolderItem extends StatelessWidget { + const FavFolderItem({super.key, this.item, this.index}); + final FavFolderItemData? item; + final int? index; @override Widget build(BuildContext context) { - return InkWell( - onTap: () {}, - borderRadius: StyleString.mdRadius, + String heroTag = Utils.makeHeroTag(item!.fid); + return Container( + margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon!.icon!), + InkWell( + onTap: () => + Get.toNamed('/favDetail', arguments: item, parameters: { + 'mediaId': item!.id.toString(), + 'heroTag': heroTag, + 'isOwner': '1', + }), + borderRadius: StyleString.mdRadius, + child: Hero( + tag: heroTag, + child: NetworkImgLayer( + src: item!.cover, + width: 180, + height: 110, + ), + ), + ), const SizedBox(height: 8), Text( - text!, - style: Theme.of(context).textTheme.labelMedium, + ' ${item!.title}', + overflow: TextOverflow.fade, + maxLines: 1, ), + Text( + ' 共${item!.mediaCount}条视频', + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Theme.of(context).colorScheme.outline), + ) ], ), ); diff --git a/lib/pages/mine_edit/controller.dart b/lib/pages/mine_edit/controller.dart new file mode 100644 index 00000000..9bc43246 --- /dev/null +++ b/lib/pages/mine_edit/controller.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/utils/storage.dart'; + +class MineEditController extends GetxController { + Box userInfoCache = GStrorage.userInfo; + final formKey = GlobalKey(); + final TextEditingController unameCtr = TextEditingController(); + final TextEditingController useridCtr = TextEditingController(); + final TextEditingController signCtr = TextEditingController(); + final TextEditingController birthdayCtr = TextEditingController(); + String? sex; + dynamic userInfo; + + @override + void onInit() { + super.onInit(); + userInfo = userInfoCache.get('userInfoCache'); + } + + Future getAccountInfo() async { + var res = await UserHttp.getAccountInfo(); + if (res['status']) { + unameCtr.text = res['data']['uname']; + useridCtr.text = res['data']['userid']; + signCtr.text = res['data']['sign']; + birthdayCtr.text = res['data']['birthday']; + sex = res['data']['sex']; + } + return res; + } + + Future updateAccountInfo() async { + var res = await UserHttp.updateAccountInfo( + uname: unameCtr.text, + sign: signCtr.text, + sex: sex!, + birthday: birthdayCtr.text, + ); + SmartDialog.showToast(res['status'] ? res['msg'] : "更新失败:${res['msg']}"); + } +} diff --git a/lib/pages/media/index.dart b/lib/pages/mine_edit/index.dart similarity index 72% rename from lib/pages/media/index.dart rename to lib/pages/mine_edit/index.dart index 8fae4891..88806448 100644 --- a/lib/pages/media/index.dart +++ b/lib/pages/mine_edit/index.dart @@ -1,4 +1,4 @@ -library media; +library mine_edit; export './controller.dart'; export './view.dart'; diff --git a/lib/pages/mine_edit/view.dart b/lib/pages/mine_edit/view.dart new file mode 100644 index 00000000..700c707e --- /dev/null +++ b/lib/pages/mine_edit/view.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'controller.dart'; + +class MineEditPage extends StatefulWidget { + const MineEditPage({super.key}); + + @override + State createState() => _MineEditPageState(); +} + +class _MineEditPageState extends State { + final MineEditController ctr = Get.put(MineEditController()); + late Future _futureBuilderFuture; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = ctr.getAccountInfo(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('编辑资料'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: FutureBuilder( + future: _futureBuilderFuture, + builder: ((context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + if (snapshot.data['status']) { + return Form( + key: ctr.formKey, + child: Column( + children: [ + // 用户头像 + // InkWell( + // onTap: () {}, + // child: CircleAvatar( + // radius: 50, + // backgroundColor: Colors.transparent, + // backgroundImage: + // NetworkImage(ctr.userInfo.face), // 替换为实际的头像路径 + // ), + // ), + const SizedBox(height: 24.0), + // 昵称 + TextFormField( + controller: ctr.unameCtr, + decoration: const InputDecoration( + labelText: '昵称', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入昵称'; + } + return null; + }, + ), + const SizedBox(height: 20.0), + // 用户名 + TextFormField( + controller: ctr.useridCtr, + decoration: const InputDecoration( + labelText: '用户名', + border: OutlineInputBorder(), + enabled: false, + ), + readOnly: true, + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入用户名'; + } + return null; + }, + ), + const SizedBox(height: 20.0), + // 签名 + TextFormField( + controller: ctr.signCtr, + decoration: const InputDecoration( + labelText: '签名', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入签名'; + } + return null; + }, + ), + const SizedBox(height: 20.0), + // 性别 + DropdownButtonFormField( + value: ctr.sex, + decoration: const InputDecoration( + labelText: '性别', + border: OutlineInputBorder(), + ), + items: ['男', '女', '保密'].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (newValue) { + ctr.sex = newValue; + }, + validator: (value) { + if (value == null || value.isEmpty) { + return '请选择性别'; + } + return null; + }, + ), + const SizedBox(height: 20.0), + // 出生日期 + TextFormField( + controller: ctr.birthdayCtr, + decoration: const InputDecoration( + labelText: '出生日期', + border: OutlineInputBorder(), + ), + enabled: false, + readOnly: true, + onTap: () async { + // DateTime? pickedDate = await showDatePicker( + // context: context, + // initialDate: DateTime(1995, 12, 23), + // firstDate: DateTime(1900), + // lastDate: DateTime(2100), + // ); + // if (pickedDate != null) { + // ctr.birthdayCtr.text = + // "${pickedDate.toLocal()}".split(' ')[0]; + // } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return '请选择出生日期'; + } + return null; + }, + ), + const SizedBox(height: 30.0), + // 提交按钮 + ElevatedButton( + onPressed: () { + if (ctr.formKey.currentState!.validate()) { + // 处理表单提交 + ctr.updateAccountInfo(); + } + }, + child: const Text('提交'), + ), + ], + ), + ); + } else { + return const SizedBox(); + } + } else { + return const SizedBox(); + } + })), + ), + ); + } +} diff --git a/lib/pages/setting/pages/navigation_bar_set.dart b/lib/pages/setting/pages/navigation_bar_set.dart index 05407c16..49540163 100644 --- a/lib/pages/setting/pages/navigation_bar_set.dart +++ b/lib/pages/setting/pages/navigation_bar_set.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:hive/hive.dart'; -import 'package:pilipala/models/common/tab_type.dart'; import 'package:pilipala/utils/storage.dart'; import '../../../models/common/nav_bar_config.dart'; @@ -23,7 +22,7 @@ class _NavigationbarSetPageState extends State { super.initState(); defaultNavTabs = defaultNavigationBars; navBarSort = settingStorage - .get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]); + .get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3, 4]); // 对 tabData 进行排序 defaultNavTabs.sort((a, b) { int indexA = navBarSort.indexOf(a['id']); diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index a57ec1de..3fead9c7 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -675,7 +675,6 @@ class VideoDetailController extends GetxController @override void onClose() { super.onClose(); - plPlayerController.dispose(); tabCtr.removeListener(() { onTabChanged(); }); diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index de4584ed..051c6075 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -773,7 +773,7 @@ InlineSpan buildContent( // source: '', // dataString: matchStr, ); - PiliSchame.fullPathPush(scheme); + PiliSchame.httpsScheme(scheme); } } else { if (appUrlSchema.startsWith('bilibili://search')) { diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index 0f04455f..22f9e0ed 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -1,13 +1,18 @@ import 'dart:async'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:pilipala/http/dynamics.dart'; +import 'package:pilipala/http/reply.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/reply/emote.dart'; import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/pages/emote/index.dart'; +import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart'; +import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'toolbar_icon_button.dart'; @@ -44,6 +49,9 @@ class _VideoReplyNewDialogState extends State RxBool isForward = false.obs; RxBool showForward = false.obs; RxString message = ''.obs; + final ImagePicker _picker = ImagePicker(); + RxList imageList = [''].obs; + List> pictures = []; @override void initState() { @@ -60,6 +68,7 @@ class _VideoReplyNewDialogState extends State if (routePath.startsWith('/video')) { showForward.value = true; } + imageList.clear(); } _autoFocus() async { @@ -90,6 +99,7 @@ class _VideoReplyNewDialogState extends State message: widget.replyItem != null && widget.replyItem!.root != 0 ? ' 回复 @${widget.replyItem!.member!.uname!} : ${message.value}' : message.value, + pictures: pictures, ); if (result['status']) { SmartDialog.showToast(result['data']['success_toast']); @@ -125,6 +135,59 @@ class _VideoReplyNewDialogState extends State ); } + void onChooseImage() async { + if (mounted) { + try { + final XFile? pickedFile = + await _picker.pickImage(source: ImageSource.gallery); + var res = await ReplyHttp.uploadImage(xFile: pickedFile!); + if (res['status']) { + imageList.add(res['data']['img_src']); + pictures.add(res['data']); + } + } catch (e) { + debugPrint('选择图片失败: $e'); + } + } + } + + void onPreviewImg(picList, initIndex, context) { + Navigator.of(context).push( + HeroDialogRoute( + builder: (BuildContext context) => InteractiveviewerGallery( + sources: picList, + initIndex: initIndex, + itemBuilder: ( + BuildContext context, + int index, + bool isFocus, + bool enablePageView, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (enablePageView) { + Navigator.of(context).pop(); + } + }, + child: Center( + child: Hero( + tag: picList[index], + child: CachedNetworkImage( + fadeInDuration: const Duration(milliseconds: 0), + imageUrl: picList[index], + fit: BoxFit.contain, + ), + ), + ), + ); + }, + onPageChanged: (int pageIndex) {}, + ), + ), + ); + } + @override void didChangeMetrics() { super.didChangeMetrics(); @@ -175,10 +238,11 @@ class _VideoReplyNewDialogState extends State ), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( constraints: const BoxConstraints( - maxHeight: 200, + maxHeight: 250, minHeight: 120, ), child: Container( @@ -209,6 +273,65 @@ class _VideoReplyNewDialogState extends State ), ), ), + Obx( + () => Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), + child: SizedBox( + height: 65, // 固定高度以避免无限扩展 + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: imageList.length, + itemBuilder: (context, index) { + final url = imageList[index]; + return url != '' + ? Container( + width: 65, + height: 65, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + borderRadius: + const BorderRadius.all(Radius.circular(6))), + child: InkWell( + onTap: () => + onPreviewImg(imageList, index, context), + onLongPress: () { + feedBack(); + imageList.removeAt(index); + }, + child: CachedNetworkImage( + imageUrl: url, + width: 65, + height: 65, + fit: BoxFit.cover, + ), + ), + ) + : const SizedBox(); + }, + separatorBuilder: (context, index) => + const SizedBox(width: 8.0), + ), + ), + ), + ), + Obx( + () => Visibility( + visible: imageList.isNotEmpty, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), + child: Text( + '点击预览,长按删除', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: 12, + ), + ), + ), + ), + ), Divider( height: 1, color: Theme.of(context).dividerColor.withOpacity(0.1), @@ -240,7 +363,7 @@ class _VideoReplyNewDialogState extends State toolbarType: toolbarType, selected: toolbarType == 'input', ), - const SizedBox(width: 20), + const SizedBox(width: 10), ToolbarIconButton( onPressed: () { if (toolbarType == 'input') { @@ -254,6 +377,15 @@ class _VideoReplyNewDialogState extends State toolbarType: toolbarType, selected: toolbarType == 'emote', ), + if (widget.root != null && widget.root == 0) ...[ + const SizedBox(width: 10), + ToolbarIconButton( + onPressed: onChooseImage, + icon: const Icon(Icons.photo, size: 22), + toolbarType: toolbarType, + selected: toolbarType == 'picture', + ), + ], const SizedBox(width: 6), Obx( () => showForward.value diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index d3afdf1d..7fe76745 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -511,6 +511,14 @@ class _VideoDetailPageState extends State exitFullScreen(); } + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: + Get.isDarkMode ? Brightness.light : Brightness.dark, + ), + ); + Widget buildLoadingWidget() { return Center(child: Lottie.asset('assets/loading.json', width: 200)); } @@ -605,11 +613,6 @@ class _VideoDetailPageState extends State backgroundColor: Colors.black, elevation: 0, scrolledUnderElevation: 0, - systemOverlayStyle: Get.isDarkMode - ? SystemUiOverlayStyle.light - : snapshot.data!.toDouble() > kToolbarHeight - ? SystemUiOverlayStyle.dark - : SystemUiOverlayStyle.light, ); }), ), diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index e97aa79b..cbe4d938 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -228,7 +228,7 @@ class SessionItem extends StatelessWidget { parameters: { 'talkerId': sessionItem.talkerId.toString(), 'name': sessionItem.accountInfo.name, - 'face': sessionItem.accountInfo.face, + 'face': sessionItem.accountInfo.face ?? '', 'mid': (sessionItem.accountInfo?.mid ?? 0).toString(), 'heroTag': heroTag, }, @@ -244,7 +244,7 @@ class SessionItem extends StatelessWidget { width: 45, height: 45, type: 'avatar', - src: sessionItem.accountInfo.face, + src: sessionItem.accountInfo.face ?? '', ), ), ), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 8dff954a..0bd7950c 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1045,6 +1045,14 @@ class PlPlayerController { /// 缓存本次弹幕选项 cacheDanmakuOption() { + final cache = GlobalDataCache(); + cache.blockTypes = blockTypes; + cache.showArea = showArea; + cache.opacityVal = opacityVal; + cache.fontSizeVal = fontSizeVal; + cache.danmakuDurationVal = danmakuDurationVal; + cache.strokeWidth = strokeWidth; + localCache.put(LocalCacheKey.danmakuBlockType, blockTypes); localCache.put(LocalCacheKey.danmakuShowArea, showArea); localCache.put(LocalCacheKey.danmakuOpacity, opacityVal); diff --git a/lib/plugin/pl_player/panels/seek_panel.dart b/lib/plugin/pl_player/panels/seek_panel.dart new file mode 100644 index 00000000..0d5e7c9e --- /dev/null +++ b/lib/plugin/pl_player/panels/seek_panel.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../widgets/seek_indicator.dart'; + +class SeekPanel extends StatelessWidget { + const SeekPanel({ + required this.mountSeekBackwardButton, + required this.mountSeekForwardButton, + required this.hideSeekBackwardButton, + required this.hideSeekForwardButton, + required this.onSubmittedcb, + Key? key, + }) : super(key: key); + + final RxBool mountSeekBackwardButton; + final RxBool mountSeekForwardButton; + final RxBool hideSeekBackwardButton; + final RxBool hideSeekForwardButton; + final void Function(String, Duration) onSubmittedcb; + + @override + Widget build(BuildContext context) { + return Obx( + () => Visibility( + visible: mountSeekBackwardButton.value || mountSeekForwardButton.value, + child: Positioned.fill( + child: Row( + children: [ + _buildSeekIndicator( + mountSeekBackwardButton, + hideSeekBackwardButton, + 'backward', + SeekIndicator( + direction: SeekDirection.backward, + onSubmitted: (Duration value) { + onSubmittedcb.call('backward', value); + }, + ), + ), + Expanded(child: Container()), + _buildSeekIndicator( + mountSeekForwardButton, + hideSeekForwardButton, + 'forward', + SeekIndicator( + direction: SeekDirection.forward, + onSubmitted: (Duration value) { + onSubmittedcb.call('forward', value); + }, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildSeekIndicator( + RxBool mountSeekButton, + RxBool hideSeekButton, + String direction, + Widget seekIndicator, + ) { + return Expanded( + child: mountSeekButton.value + ? AnimatedOpacity( + opacity: hideSeekButton.value ? 0.0 : 1.0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + child: seekIndicator, + ) + : const SizedBox.shrink(), + ); + } +} diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 6b57a8e1..a88bc86f 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -22,12 +22,11 @@ import 'package:screen_brightness/screen_brightness.dart'; import '../../utils/global_data_cache.dart'; import 'models/bottom_control_type.dart'; import 'models/bottom_progress_behavior.dart'; +import 'panels/seek_panel.dart'; import 'widgets/app_bar_ani.dart'; -import 'widgets/backward_seek.dart'; import 'widgets/bottom_control.dart'; import 'widgets/common_btn.dart'; import 'widgets/control_bar.dart'; -import 'widgets/forward_seek.dart'; import 'widgets/play_pause_btn.dart'; class PLVideoPlayer extends StatefulWidget { @@ -69,8 +68,8 @@ class _PLVideoPlayerState extends State final RxBool _mountSeekBackwardButton = false.obs; final RxBool _mountSeekForwardButton = false.obs; - final RxBool _hideSeekBackwardButton = false.obs; - final RxBool _hideSeekForwardButton = false.obs; + final RxBool _hideSeekBackwardButton = true.obs; + final RxBool _hideSeekForwardButton = true.obs; final RxDouble _brightnessValue = 0.0.obs; final RxBool _brightnessIndicator = false.obs; @@ -97,10 +96,12 @@ class _PLVideoPlayerState extends State void onDoubleTapSeekBackward() { _mountSeekBackwardButton.value = true; + _hideSeekBackwardButton.value = false; } void onDoubleTapSeekForward() { _mountSeekForwardButton.value = true; + _hideSeekForwardButton.value = false; } // 双击播放、暂停 @@ -375,6 +376,29 @@ class _PLVideoPlayerState extends State return list; } + void _handleSubmittedCallback(String type, Duration value) { + final PlPlayerController _ = widget.controller; + final Player player = + _.videoPlayerController ?? widget.controller.videoPlayerController!; + late Duration result; + + switch (type) { + case 'backward': + _hideSeekBackwardButton.value = true; + result = player.state.position - value; + break; + case 'forward': + _hideSeekForwardButton.value = true; + result = player.state.position + value; + break; + } + _mountSeekBackwardButton.value = false; + _mountSeekForwardButton.value = false; + result = result.clamp(Duration.zero, player.state.duration); + player.seek(result); + _.play(); + } + @override Widget build(BuildContext context) { final PlPlayerController _ = widget.controller; @@ -865,99 +889,13 @@ class _PLVideoPlayerState extends State } }), - /// 点击 快进/快退 - Obx( - () => Visibility( - visible: - _mountSeekBackwardButton.value || _mountSeekForwardButton.value, - child: Positioned.fill( - child: Row( - children: [ - Expanded( - child: _mountSeekBackwardButton.value - ? TweenAnimationBuilder( - tween: Tween( - begin: 0.0, - end: _hideSeekBackwardButton.value ? 0.0 : 1.0, - ), - duration: const Duration(milliseconds: 150), - builder: (BuildContext context, double value, - Widget? child) => - Opacity( - opacity: value, - child: child, - ), - onEnd: () { - if (_hideSeekBackwardButton.value) { - _hideSeekBackwardButton.value = false; - _mountSeekBackwardButton.value = false; - } - }, - child: BackwardSeekIndicator( - onChanged: (Duration value) => {}, - onSubmitted: (Duration value) { - _hideSeekBackwardButton.value = true; - final Player player = - widget.controller.videoPlayerController!; - Duration result = player.state.position - value; - result = result.clamp( - Duration.zero, - player.state.duration, - ); - player.seek(result); - widget.controller.play(); - }, - ), - ) - : const SizedBox(), - ), - Expanded( - child: SizedBox( - width: MediaQuery.sizeOf(context).width / 4, - ), - ), - Expanded( - child: _mountSeekForwardButton.value - ? TweenAnimationBuilder( - tween: Tween( - begin: 0.0, - end: _hideSeekForwardButton.value ? 0.0 : 1.0, - ), - duration: const Duration(milliseconds: 150), - builder: (BuildContext context, double value, - Widget? child) => - Opacity( - opacity: value, - child: child, - ), - onEnd: () { - if (_hideSeekForwardButton.value) { - _hideSeekForwardButton.value = false; - _mountSeekForwardButton.value = false; - } - }, - child: ForwardSeekIndicator( - onChanged: (Duration value) => {}, - onSubmitted: (Duration value) { - _hideSeekForwardButton.value = true; - final Player player = - widget.controller.videoPlayerController!; - Duration result = player.state.position + value; - result = result.clamp( - Duration.zero, - player.state.duration, - ); - player.seek(result); - widget.controller.play(); - }, - ), - ) - : const SizedBox(), - ), - ], - ), - ), - ), + /// 快进/快退面板 + SeekPanel( + mountSeekBackwardButton: _mountSeekBackwardButton, + mountSeekForwardButton: _mountSeekForwardButton, + hideSeekBackwardButton: _hideSeekBackwardButton, + hideSeekForwardButton: _hideSeekForwardButton, + onSubmittedcb: _handleSubmittedCallback, ), ], ); diff --git a/lib/plugin/pl_player/widgets/backward_seek.dart b/lib/plugin/pl_player/widgets/backward_seek.dart deleted file mode 100644 index 8289d77c..00000000 --- a/lib/plugin/pl_player/widgets/backward_seek.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -class BackwardSeekIndicator extends StatefulWidget { - final void Function(Duration) onChanged; - final void Function(Duration) onSubmitted; - const BackwardSeekIndicator({ - Key? key, - required this.onChanged, - required this.onSubmitted, - }) : super(key: key); - - @override - State createState() => BackwardSeekIndicatorState(); -} - -class BackwardSeekIndicatorState extends State { - Duration value = const Duration(seconds: 10); - - Timer? timer; - - @override - void setState(VoidCallback fn) { - if (mounted) { - super.setState(fn); - } - } - - @override - void initState() { - super.initState(); - timer = Timer(const Duration(milliseconds: 200), () { - widget.onSubmitted.call(value); - }); - } - - void increment() { - timer?.cancel(); - timer = Timer(const Duration(milliseconds: 200), () { - widget.onSubmitted.call(value); - }); - widget.onChanged.call(value); - // 重复点击 快退秒数累加10 - setState(() { - value += const Duration(seconds: 10); - }); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0x88767676), - Color(0x00767676), - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - ), - child: InkWell( - splashColor: const Color(0x44767676), - onTap: increment, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.fast_rewind, - size: 24.0, - color: Color(0xFFFFFFFF), - ), - const SizedBox(height: 8.0), - Text( - '快退${value.inSeconds}秒', - style: const TextStyle( - fontSize: 12.0, - color: Color(0xFFFFFFFF), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/plugin/pl_player/widgets/forward_seek.dart b/lib/plugin/pl_player/widgets/forward_seek.dart deleted file mode 100644 index 3f68fe0d..00000000 --- a/lib/plugin/pl_player/widgets/forward_seek.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -class ForwardSeekIndicator extends StatefulWidget { - final void Function(Duration) onChanged; - final void Function(Duration) onSubmitted; - const ForwardSeekIndicator({ - Key? key, - required this.onChanged, - required this.onSubmitted, - }) : super(key: key); - - @override - State createState() => ForwardSeekIndicatorState(); -} - -class ForwardSeekIndicatorState extends State { - Duration value = const Duration(seconds: 10); - - Timer? timer; - - @override - void setState(VoidCallback fn) { - if (mounted) { - super.setState(fn); - } - } - - @override - void initState() { - super.initState(); - timer = Timer(const Duration(milliseconds: 200), () { - widget.onSubmitted.call(value); - }); - } - - void increment() { - timer?.cancel(); - timer = Timer(const Duration(milliseconds: 200), () { - widget.onSubmitted.call(value); - }); - widget.onChanged.call(value); - // 重复点击 快进秒数累加10 - setState(() { - value += const Duration(seconds: 10); - }); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0x00767676), - Color(0x88767676), - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - ), - child: InkWell( - splashColor: const Color(0x44767676), - onTap: increment, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.fast_forward, - size: 24.0, - color: Color(0xFFFFFFFF), - ), - const SizedBox(height: 8.0), - Text( - '快进${value.inSeconds}秒', - style: const TextStyle( - fontSize: 12.0, - color: Color(0xFFFFFFFF), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/plugin/pl_player/widgets/seek_indicator.dart b/lib/plugin/pl_player/widgets/seek_indicator.dart new file mode 100644 index 00000000..45fa5a16 --- /dev/null +++ b/lib/plugin/pl_player/widgets/seek_indicator.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; + +enum SeekDirection { forward, backward } + +class SeekIndicator extends StatefulWidget { + final SeekDirection direction; + final void Function(Duration) onSubmitted; + + const SeekIndicator({ + Key? key, + required this.direction, + required this.onSubmitted, + }) : super(key: key); + + @override + State createState() => _SeekIndicatorState(); +} + +class _SeekIndicatorState extends State { + Timer? timer; + + @override + void initState() { + super.initState(); + _startTimer(); + } + + void _startTimer() { + timer?.cancel(); + timer = Timer(const Duration(milliseconds: 400), () { + widget.onSubmitted.call(const Duration(seconds: 10)); + timer = null; + }); + } + + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: widget.direction == SeekDirection.forward + ? [ + const Color(0x00767676), + const Color(0x88767676), + ] + : [ + const Color(0x88767676), + const Color(0x00767676), + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + widget.direction == SeekDirection.forward + ? Icons.fast_forward + : Icons.fast_rewind, + size: 24.0, + color: const Color(0xFFFFFFFF), + ), + const SizedBox(height: 8.0), + Text( + widget.direction == SeekDirection.forward ? '快进10秒' : '快退10秒', + style: const TextStyle( + fontSize: 12.0, + color: Color(0xFFFFFFFF), + shadows: [ + Shadow( + color: Color(0xFF000000), + offset: Offset(0, 2), + blurRadius: 4, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 25f42d56..7a14b499 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -10,6 +10,8 @@ import 'package:pilipala/pages/message/at/index.dart'; import 'package:pilipala/pages/message/like/index.dart'; import 'package:pilipala/pages/message/reply/index.dart'; import 'package:pilipala/pages/message/system/index.dart'; +import 'package:pilipala/pages/mine/index.dart'; +import 'package:pilipala/pages/mine_edit/index.dart'; import 'package:pilipala/pages/opus/index.dart'; import 'package:pilipala/pages/read/index.dart'; import 'package:pilipala/pages/setting/pages/logs.dart'; @@ -31,7 +33,6 @@ import '../pages/html/index.dart'; import '../pages/later/index.dart'; import '../pages/live_room/view.dart'; import '../pages/login/index.dart'; -import '../pages/media/index.dart'; import '../pages/member/index.dart'; import '../pages/member_archive/index.dart'; import '../pages/member_coin/index.dart'; @@ -79,8 +80,6 @@ class Routes { // 设置 CustomGetPage(name: '/setting', page: () => const SettingPage()), // - CustomGetPage(name: '/media', page: () => const MediaPage()), - // CustomGetPage(name: '/fav', page: () => const FavPage()), // CustomGetPage(name: '/favDetail', page: () => const FavDetailPage()), @@ -187,6 +186,8 @@ class Routes { // 系统通知 CustomGetPage( name: '/messageSystem', page: () => const MessageSystemPage()), + // 我的 + CustomGetPage(name: '/mine', page: () => const MinePage()), // 收藏夹编辑 CustomGetPage(name: '/favEdit', page: () => const FavEditPage()), @@ -196,6 +197,8 @@ class Routes { // 用户专栏 CustomGetPage( name: '/memberArticle', page: () => const MemberArticlePage()), + // 用户信息编辑 + CustomGetPage(name: '/mineEdit', page: () => const MineEditPage()), ]; } diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 3d200351..3a69843c 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -1,3 +1,4 @@ +import 'package:app_links/app_links.dart'; import 'package:appscheme/appscheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,22 +11,29 @@ import 'url_utils.dart'; import 'utils.dart'; class PiliSchame { + static late AppLinks appLinks; static AppScheme appScheme = AppSchemeImpl.getInstance()!; static Future init() async { - /// - final SchemeEntity? value = await appScheme.getInitScheme(); - if (value != null) { - routePush(value); - } + appLinks = AppLinks(); + appLinks.uriLinkStream.listen((Uri uri) { + final String scheme = uri.scheme; + if (RegExp(r'^pili', caseSensitive: false).hasMatch(scheme)) { + piliScheme(uri); + } + }); + + appScheme.getInitScheme().then((SchemeEntity? value) { + if (value != null) { + routePush(value); + } + }); - /// 完整链接进入 b23.无效 appScheme.getLatestScheme().then((SchemeEntity? value) { if (value != null) { routePush(value); } }); - /// 注册从外部打开的Scheme监听信息 # appScheme.registerSchemeListener().listen((SchemeEntity? event) { if (event != null) { routePush(event); @@ -36,88 +44,11 @@ class PiliSchame { /// 路由跳转 static void routePush(value) async { final String scheme = value.scheme; - final String host = value.host; - final String path = value.path; if (scheme == 'bilibili') { - switch (host) { - case 'root': - Navigator.popUntil( - Get.context!, (Route route) => route.isFirst); - break; - case 'space': - final String mid = path.split('/').last; - Get.toNamed( - '/member?mid=$mid', - arguments: {'face': null}, - ); - break; - case 'video': - String pathQuery = path.split('/').last; - final numericRegex = RegExp(r'^[0-9]+$'); - if (numericRegex.hasMatch(pathQuery)) { - pathQuery = 'AV$pathQuery'; - } - Map map = IdUtils.matchAvorBv(input: pathQuery); - if (map.containsKey('AV')) { - _videoPush(map['AV'], null); - } else if (map.containsKey('BV')) { - _videoPush(null, map['BV']); - } else { - SmartDialog.showToast('投稿匹配失败'); - } - break; - case 'live': - final String roomId = path.split('/').last; - Get.toNamed( - '/liveRoom?roomid=$roomId', - arguments: {'liveItem': null, 'heroTag': roomId}, - ); - break; - case 'bangumi': - if (path.startsWith('/season')) { - final String seasonId = path.split('/').last; - RoutePush.bangumiPush(int.parse(seasonId), null); - } - break; - case 'opus': - if (path.startsWith('/detail')) { - var opusId = path.split('/').last; - Get.toNamed('/opus', parameters: { - 'title': '', - 'id': opusId, - 'articleType': 'opus', - }); - } - break; - case 'search': - Get.toNamed('/searchResult', parameters: {'keyword': ''}); - break; - case 'article': - final String id = path.split('/').last.split('?').first; - Get.toNamed( - '/read', - parameters: { - 'title': 'cv$id', - 'id': id, - 'dynamicType': 'read', - }, - ); - break; - case 'pgc': - if (path.contains('ep')) { - final String lastPathSegment = path.split('/').last; - RoutePush.bangumiPush( - null, int.parse(lastPathSegment.split('?').first)); - } - break; - default: - SmartDialog.showToast('未匹配地址,请联系开发者'); - Clipboard.setData(ClipboardData(text: value.toJson().toString())); - break; - } + biliScheme(value); } if (['http', 'https'].contains(scheme)) { - fullPathPush(value); + httpsScheme(value); } } @@ -148,7 +79,7 @@ class PiliSchame { } } - static Future fullPathPush(Uri value) async { + static Future httpsScheme(Uri value) async { // https://m.bilibili.com/bangumi/play/ss39708 // https | m.bilibili.com | /bangumi/play/ss39708 // final String scheme = value.scheme!; @@ -281,6 +212,153 @@ class PiliSchame { } } + static Future biliScheme(SchemeEntity value) async { + final String host = value.host!; + final String path = value.path!; + switch (host) { + case 'root': + Navigator.popUntil( + Get.context!, (Route route) => route.isFirst); + break; + case 'space': + final String mid = path.split('/').last; + Get.toNamed( + '/member?mid=$mid', + arguments: {'face': null}, + ); + break; + case 'video': + String pathQuery = path.split('/').last; + final numericRegex = RegExp(r'^[0-9]+$'); + if (numericRegex.hasMatch(pathQuery)) { + pathQuery = 'AV$pathQuery'; + } + Map map = IdUtils.matchAvorBv(input: pathQuery); + if (map.containsKey('AV')) { + _videoPush(map['AV'], null); + } else if (map.containsKey('BV')) { + _videoPush(null, map['BV']); + } else { + SmartDialog.showToast('投稿匹配失败'); + } + break; + case 'live': + final String roomId = path.split('/').last; + Get.toNamed( + '/liveRoom?roomid=$roomId', + arguments: {'liveItem': null, 'heroTag': roomId}, + ); + break; + case 'bangumi': + if (path.startsWith('/season')) { + final String seasonId = path.split('/').last; + RoutePush.bangumiPush(int.parse(seasonId), null); + } + break; + case 'opus': + if (path.startsWith('/detail')) { + var opusId = path.split('/').last; + Get.toNamed('/opus', parameters: { + 'title': '', + 'id': opusId, + 'articleType': 'opus', + }); + } + break; + case 'search': + Get.toNamed('/searchResult', parameters: {'keyword': ''}); + break; + case 'article': + final String id = path.split('/').last.split('?').first; + Get.toNamed( + '/read', + parameters: { + 'title': 'cv$id', + 'id': id, + 'dynamicType': 'read', + }, + ); + break; + case 'pgc': + if (path.contains('ep')) { + final String lastPathSegment = path.split('/').last; + RoutePush.bangumiPush( + null, int.parse(lastPathSegment.split('?').first)); + } + break; + case 'following': + if (path.startsWith('/detail')) { + var opusId = path.split('/').last; + Get.toNamed( + '/webview', + parameters: { + 'url': 'https://m.bilibili.com/opus/$opusId', + 'type': 'url', + 'pageTitle': '' + }, + ); + } + break; + default: + SmartDialog.showToast('未匹配地址,请联系开发者'); + Clipboard.setData(ClipboardData(text: value.toJson().toString())); + break; + } + } + + static Future piliScheme(Uri value) async { + final String host = value.host; + final String path = value.path; + final String arg = path.split('/').last; + switch (host) { + case 'home': + case 'root': + Get.toNamed('/'); + break; + case 'member': + if (arg != '') { + final int? mid = int.tryParse(arg); + if (mid == null) { + SmartDialog.showToast('用户id有误'); + return; + } + Get.toNamed( + '/member?mid=$mid', + arguments: {'face': null}, + ); + } else { + Get.toNamed('/mine'); + } + break; + case 'search': + if (arg != '') { + final String encodedArg = Uri.decodeComponent(arg); + Get.toNamed('/searchResult', parameters: {'keyword': encodedArg}); + } else { + Get.toNamed('/search'); + } + break; + case 'setting': + Get.toNamed('/setting'); + break; + case 'fav': + Get.toNamed('/fav'); + break; + case 'history': + Get.toNamed('/history'); + break; + case 'later': + Get.toNamed('/later'); + break; + case 'msg': + Get.toNamed('/whisper'); + break; + default: + Get.toNamed('/'); + break; + } + } + static void _handleEpisodePath(String lastPathSegment, String redirectUrl) { final String seasonId = _extractIdFromPath(lastPathSegment); RoutePush.bangumiPush(null, Utils.matchNum(seasonId).first); diff --git a/lib/utils/cache_manage.dart b/lib/utils/cache_manage.dart index d6bbb816..4603da9f 100644 --- a/lib/utils/cache_manage.dart +++ b/lib/utils/cache_manage.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -50,13 +51,17 @@ class CacheManage { Future getTotalSizeOfFilesInDir(final FileSystemEntity file) async { if (file is File) { int length = await file.length(); - return double.parse(length.toString()); + return length.toDouble(); } if (file is Directory) { - final List children = file.listSync(); double total = 0; - for (final FileSystemEntity child in children) { - total += await getTotalSizeOfFilesInDir(child); + try { + await for (final FileSystemEntity child in file.list()) { + total += await getTotalSizeOfFilesInDir(child); + } + } catch (e) { + // 处理错误,例如记录日志或显示错误消息 + print('读取目录时出错: $e'); } return total; } @@ -77,16 +82,17 @@ class CacheManage { // 清除缓存 Future clearCacheAll() async { - bool cleanStatus = await SmartDialog.show( - useSystem: true, - animationType: SmartAnimationType.centerFade_otherSlide, + bool? cleanStatus = await showDialog( + context: Get.context!, builder: (BuildContext context) { return AlertDialog( title: const Text('提示'), content: const Text('该操作将清除图片及网络请求缓存数据,确认清除?'), actions: [ TextButton( - onPressed: (() => {SmartDialog.dismiss()}), + onPressed: () { + Navigator.of(context).pop(false); + }, child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), @@ -94,40 +100,45 @@ class CacheManage { ), TextButton( onPressed: () async { - SmartDialog.dismiss(); - SmartDialog.showLoading(msg: '正在清除...'); - try { - // 清除缓存 图片缓存 - await clearLibraryCache(); - SmartDialog.dismiss().then((res) { - SmartDialog.showToast('清除完成'); - }); - } catch (err) { - SmartDialog.dismiss(); - SmartDialog.showToast(err.toString()); - } + Navigator.of(context).pop(true); }, child: const Text('确认'), - ) + ), ], ); }, - ).then((res) { - return true; - }); - return cleanStatus; + ); + if (cleanStatus != null && cleanStatus) { + SmartDialog.showLoading(msg: '正在清除...'); + try { + // 清除缓存 图片缓存 + await clearLibraryCache(); + SmartDialog.dismiss().then((res) { + SmartDialog.showToast('清除完成'); + }); + } catch (err) { + SmartDialog.dismiss(); + SmartDialog.showToast(err.toString()); + } + } + return cleanStatus!; } /// 清除 Documents 目录下的 DioCache.db - Future clearApplicationCache() async { - Directory directory = await getApplicationDocumentsDirectory(); - if (directory.existsSync()) { - String dioCacheFileName = - '${directory.path}${Platform.pathSeparator}DioCache.db'; - var dioCacheFile = File(dioCacheFileName); - if (dioCacheFile.existsSync()) { - dioCacheFile.delete(); + Future clearApplicationCache() async { + try { + Directory directory = await getApplicationDocumentsDirectory(); + if (directory.existsSync()) { + String dioCacheFileName = + '${directory.path}${Platform.pathSeparator}DioCache.db'; + File dioCacheFile = File(dioCacheFileName); + if (await dioCacheFile.exists()) { + await dioCacheFile.delete(); + } } + } catch (e) { + // 处理错误,例如记录日志或显示错误消息 + print('清除缓存时出错: $e'); } } diff --git a/lib/utils/login.dart b/lib/utils/login.dart index 2687a8c2..07afd2dc 100644 --- a/lib/utils/login.dart +++ b/lib/utils/login.dart @@ -10,7 +10,6 @@ import 'package:hive/hive.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/index.dart'; -import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/utils/cookie.dart'; import 'package:pilipala/utils/storage.dart'; @@ -31,9 +30,6 @@ class LoginUtils { DynamicsController dynamicsCtr = Get.find(); dynamicsCtr.userLogin.value = status; - - MediaController mediaCtr = Get.find(); - mediaCtr.userLogin.value = status; } catch (err) { SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}'); } @@ -84,8 +80,6 @@ class LoginUtils { final HomeController homeCtr = Get.find(); homeCtr.updateLoginStatus(true); homeCtr.userFace.value = result['data'].face; - final MediaController mediaCtr = Get.find(); - mediaCtr.mid = result['data'].mid; await LoginUtils.refreshLoginStatus(true); } catch (err) { SmartDialog.show(builder: (BuildContext context) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c3b56ecd..e42146fc 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,7 +7,9 @@ #include "generated_plugin_registrant.h" #include +#include #include +#include #include #include #include @@ -16,9 +18,15 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) flutter_volume_controller_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterVolumeControllerPlugin"); flutter_volume_controller_plugin_register_with_registrar(flutter_volume_controller_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 70cdeb4b..90fdffc0 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + file_selector_linux flutter_volume_controller + gtk media_kit_libs_linux media_kit_video url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8af2f922..ed9ce437 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import app_links import audio_service import audio_session import connectivity_plus import device_info_plus import dynamic_color +import file_selector_macos import flutter_volume_controller import media_kit_libs_macos_video import media_kit_video @@ -22,11 +24,13 @@ import url_launcher_macos import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index d9c6a1dd..8050b857 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.2.0" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.2" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" appscheme: dependency: "direct main" description: @@ -497,6 +529,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.4+1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.3+2" fixnum: dependency: transitive description: @@ -670,6 +734,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" hive: dependency: "direct main" description: @@ -742,6 +814,70 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.3" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.12+1" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.5" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1+1" intl: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c87c75fc..b9d29abf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,6 +112,7 @@ dependencies: flutter_displaymode: ^0.6.0 # scheme跳转 appscheme: ^1.0.8 + app_links: ^6.3.2 # 弹幕 ns_danmaku: git: @@ -150,6 +151,8 @@ dependencies: brotli: ^0.6.0 # 文本语法高亮 re_highlight: ^0.0.3 + # 图片选择器 + image_picker: ^1.1.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d2cff3b2..a3b120c5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,8 +6,10 @@ #include "generated_plugin_registrant.h" +#include #include #include +#include #include #include #include @@ -17,10 +19,14 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterVolumeControllerPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterVolumeControllerPluginCApi")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5d25e134..3399c82b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links connectivity_plus dynamic_color + file_selector_windows flutter_volume_controller media_kit_libs_windows_video media_kit_video