diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0faef731..94db6539 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png index df13b128..4f468fbe 100644 Binary files a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png index 8cdf4851..432b3425 100644 Binary files a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png index 1bdb8bb3..c37d4b82 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png index 140baca6..2c03aecb 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png index b1b58395..ffc6bc92 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index f606c4d8..5f349f7f 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,4 @@ - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 1cb4b0c4..1c01ae34 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 9eaf9392..740b2e82 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 6bf71b8f..efd04701 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 9c4d6c7a..64583ebb 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 59f64433..fc95b79a 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf b/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf deleted file mode 100755 index aff150a1..00000000 Binary files a/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf and /dev/null differ diff --git a/assets/images/logo/logo_android.png b/assets/images/logo/logo_android.png index 5e220fb4..db737743 100644 Binary files a/assets/images/logo/logo_android.png and b/assets/images/logo/logo_android.png differ diff --git a/assets/images/logo/logo_android_2.png b/assets/images/logo/logo_android_2.png new file mode 100644 index 00000000..9e018982 Binary files /dev/null and b/assets/images/logo/logo_android_2.png differ diff --git a/assets/images/logo/logo_big.png b/assets/images/logo/logo_big.png deleted file mode 100644 index 62370832..00000000 Binary files a/assets/images/logo/logo_big.png and /dev/null differ diff --git a/assets/images/logo/logo_ios.png b/assets/images/logo/logo_ios.png index a9992b4a..f1c73b96 100644 Binary files a/assets/images/logo/logo_ios.png and b/assets/images/logo/logo_ios.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 7f19eb8f..94704c86 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 4b8fadf7..c1a5949c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index bf7de64d..bc245a87 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 59305c75..fec4838b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index b22a8706..5d9f621b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 4eb2bd8d..8737ff38 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 8cfd292f..bd5f5489 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index bf7de64d..bc245a87 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index fd0c7eab..550144b6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index c2d7c252..7c5103f0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png index e1f6fde2..9a357161 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png index 48ce80ad..4e8ef248 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png index 0b3f7b66..a26b80c2 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png index ebc40996..b31b1777 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index c2d7c252..7c5103f0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 41f9638c..d12c2280 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png index 5ba23fa6..4bbe2f28 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index ccfaddf5..8edb5ca2 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 59436734..65617828 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 371d4763..8fd3384e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 04d36300..cccb9170 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 86182b8e..863a8ef8 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Pilipala + PiliPala CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index b07144e3..46683e96 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/utils/storage.dart'; + +Box setting = GStrorage.setting; class NetworkImgLayer extends StatelessWidget { final String? src; @@ -24,12 +28,14 @@ class NetworkImgLayer extends StatelessWidget { this.fadeOutDuration, this.fadeInDuration, // 图片质量 默认1% - this.quality = 1, + this.quality, }) : super(key: key); @override Widget build(BuildContext context) { double pr = MediaQuery.of(context).devicePixelRatio; + int picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); + // double pr = 2; return src != '' ? ClipRRect( @@ -41,7 +47,7 @@ class NetworkImgLayer extends StatelessWidget { : StyleString.imgRadius.x), child: CachedNetworkImage( imageUrl: - '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality}q.webp', + '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? picQuality}q.webp', width: width ?? double.infinity, height: height ?? double.infinity, alignment: Alignment.center, diff --git a/lib/http/api.dart b/lib/http/api.dart index d1dc350a..0ea3e5f4 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -253,4 +253,29 @@ class Api { // 取消追番 static const String bangumiDel = '/pgc/web/follow/del'; + + // 番剧列表 + // https://api.bilibili.com/pgc/season/index/result? + // st=1& + // order=3 + // season_version=-1 全部-1 正片1 电影2 其他3 + // spoken_language_type=-1 全部-1 原生1 中文配音2 + // area=-1& + // is_finish=-1& + // copyright=-1& + // season_status=-1& + // season_month=-1& + // year=-1& + // style_id=-1& + // sort=0& + // page=1& + // season_type=1& + // pagesize=20& + // type=1 + static const String bangumiList = + '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1©right=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1'; + + // 我的订阅 + static const String bangumiFollow = + '/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969'; } diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart new file mode 100644 index 00000000..bd20366c --- /dev/null +++ b/lib/http/bangumi.dart @@ -0,0 +1,36 @@ +import 'package:pilipala/http/index.dart'; +import 'package:pilipala/models/bangumi/list.dart'; + +class BangumiHttp { + static Future bangumiList({int? page}) async { + var res = await Request().get(Api.bangumiList, data: {'page': page}); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': BangumiListDataModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + static Future bangumiFollow({int? mid}) async { + var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid}); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': BangumiListDataModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } +} diff --git a/lib/http/init.dart b/lib/http/init.dart index 08be1514..b36ff57e 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -29,6 +29,7 @@ class Request { /// 设置cookie static setCookie() async { + Box user = GStrorage.user; var cookiePath = await Utils.getCookiePath(); var cookieJar = PersistCookieJar( ignoreExpires: true, @@ -38,8 +39,18 @@ class Request { dio.interceptors.add(cookieManager); var cookie = await cookieManager.cookieJar .loadForRequest(Uri.parse(HttpString.baseUrl)); - var cookie2 = await cookieManager.cookieJar - .loadForRequest(Uri.parse(HttpString.tUrl)); + if (user.get(UserBoxKey.userMid) != null) { + var cookie2 = await cookieManager.cookieJar + .loadForRequest(Uri.parse(HttpString.tUrl)); + if (cookie2.isEmpty) { + try { + await Request().get(HttpString.tUrl); + } catch (e) { + log("setCookie, ${e.toString()}"); + } + } + } + if (cookie.isEmpty) { try { await Request().get(HttpString.baseUrl); @@ -47,13 +58,6 @@ class Request { log("setCookie, ${e.toString()}"); } } - if (cookie2.isEmpty) { - try { - await Request().get(HttpString.tUrl); - } catch (e) { - log("setCookie, ${e.toString()}"); - } - } } // 移除cookie @@ -95,28 +99,29 @@ class Request { //Http请求头. headers: { // 'cookie': '', - "env": 'prod', - "app-key": 'android', - "x-bili-aurora-eid": 'UlMFQVcABlAH', - "x-bili-aurora-zone": 'sh001', - 'referer': 'https://www.bilibili.com/', }, ); Box user = GStrorage.user; if (user.get(UserBoxKey.userMid) != null) { options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString(); + options.headers['env'] = 'prod'; + options.headers['app-key'] = 'android64'; + options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH'; + options.headers['x-bili-aurora-zone'] = 'sh001'; + options.headers['referer'] = 'https://www.bilibili.com/'; } dio.options = options; //添加拦截器 - dio.interceptors - ..add(ApiInterceptor()) - // 日志拦截器 输出请求、响应内容 - ..add(LogInterceptor( - request: false, - requestHeader: false, - responseHeader: false, - )); + dio.interceptors.add(ApiInterceptor()); + + // 日志拦截器 输出请求、响应内容 + dio.interceptors.add(LogInterceptor( + request: false, + requestHeader: false, + responseHeader: false, + )); + dio.transformer = BackgroundTransformer(); dio.options.validateStatus = (status) { return status! >= 200 && status < 300 || status == 304 || status == 302; @@ -161,7 +166,7 @@ class Request { * post请求 */ post(url, {data, queryParameters, options, cancelToken, extra}) async { - print('post-data: $data'); + // print('post-data: $data'); Response response; try { response = await dio.post( @@ -171,7 +176,7 @@ class Request { options: options, cancelToken: cancelToken, ); - print('post success: ${response.data}'); + // print('post success: ${response.data}'); return response; } on DioException catch (e) { print('post error: $e'); diff --git a/lib/main.dart b/lib/main.dart index 50d686c2..d35d8399 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,8 +4,10 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/custom_toast.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/router/app_pages.dart'; @@ -33,19 +35,38 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + Color brandColor = const Color.fromARGB(255, 92, 182, 123); + Box setting = GStrorage.setting; + ThemeType currentThemeValue = ThemeType.values[setting + .get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)]; return DynamicColorBuilder( - builder: ((lightDynamic, darkDynamic) { + builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) { + ColorScheme? lightColorScheme; + ColorScheme? darkColorScheme; + if (lightDynamic != null && darkDynamic != null) { + // dynamic取色成功 + lightColorScheme = lightDynamic.harmonized(); + darkColorScheme = darkDynamic.harmonized(); + } else { + // dynamic取色失败,采用品牌色 + lightColorScheme = ColorScheme.fromSeed( + seedColor: brandColor, + brightness: Brightness.light, + ); + darkColorScheme = ColorScheme.fromSeed( + seedColor: brandColor, + brightness: Brightness.dark, + ); + } // 图片缓存 // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20; return GetMaterialApp( title: 'PiLiPaLa', theme: ThemeData( - fontFamily: 'HarmonyOS', - colorScheme: lightDynamic ?? - ColorScheme.fromSeed( - seedColor: Colors.green, - brightness: Brightness.light, - ), + // fontFamily: 'HarmonyOS', + colorScheme: currentThemeValue == ThemeType.dark + ? darkColorScheme + : lightColorScheme, useMaterial3: true, pageTransitionsTheme: const PageTransitionsTheme( builders: { @@ -56,12 +77,10 @@ class MyApp extends StatelessWidget { ), ), darkTheme: ThemeData( - fontFamily: 'HarmonyOS', - colorScheme: darkDynamic ?? - ColorScheme.fromSeed( - seedColor: Colors.green, - brightness: Brightness.dark, - ), + // fontFamily: 'HarmonyOS', + colorScheme: currentThemeValue == ThemeType.light + ? lightColorScheme + : darkColorScheme, useMaterial3: true, ), localizationsDelegates: const [ diff --git a/lib/models/bangumi/list.dart b/lib/models/bangumi/list.dart new file mode 100644 index 00000000..c15014d0 --- /dev/null +++ b/lib/models/bangumi/list.dart @@ -0,0 +1,90 @@ +class BangumiListDataModel { + BangumiListDataModel({ + this.hasNext, + this.list, + this.num, + this.size, + this.total, + }); + + int? hasNext; + List? list; + int? num; + int? size; + int? total; + + BangumiListDataModel.fromJson(Map json) { + hasNext = json['has_next']; + list = json['list'] != null + ? json['list'] + .map((e) => BangumiListItemModel.fromJson(e)) + .toList() + : []; + num = json['num']; + size = json['size']; + total = json['total']; + } +} + +class BangumiListItemModel { + BangumiListItemModel({ + this.badge, + this.badgeType, + this.cover, + // this.firstEp, + this.indexShow, + this.isFinish, + this.link, + this.mediaId, + this.order, + this.orderType, + this.score, + this.seasonId, + this.seaconStatus, + this.seasonType, + this.subTitle, + this.title, + this.titleIcon, + this.progress, + }); + + String? badge; + int? badgeType; + String? cover; + String? indexShow; + int? isFinish; + String? link; + int? mediaId; + String? order; + String? orderType; + String? score; + int? seasonId; + int? seaconStatus; + int? seasonType; + String? subTitle; + String? title; + String? titleIcon; + + String? progress; + + BangumiListItemModel.fromJson(Map json) { + badge = json['badge'] == '' ? null : json['badge']; + badgeType = json['badge_type']; + cover = json['cover']; + indexShow = json['index_show']; + isFinish = json['is_finish']; + link = json['link']; + mediaId = json['media_id']; + order = json['order']; + orderType = json['order_type']; + score = json['score']; + seasonId = json['season_id']; + seaconStatus = json['seacon_status']; + seasonType = json['season_type']; + subTitle = json['sub_title']; + title = json['title']; + titleIcon = json['title_icon']; + + progress = json['progress']; + } +} diff --git a/lib/models/common/tab_type.dart b/lib/models/common/tab_type.dart new file mode 100644 index 00000000..90d19029 --- /dev/null +++ b/lib/models/common/tab_type.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/bangumi/index.dart'; +import 'package:pilipala/pages/hot/index.dart'; +import 'package:pilipala/pages/live/index.dart'; +import 'package:pilipala/pages/rcmd/index.dart'; + +enum TabType { live, rcmd, hot, bangumi } + +extension TabTypeDesc on TabType { + String get description => ['直播', '推荐', '热门', '番剧'][index]; +} + +List tabsConfig = [ + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '直播', + 'type': TabType.live, + 'ctr': Get.find, + 'page': const LivePage(), + }, + { + 'icon': const Icon( + Icons.thumb_up_off_alt_outlined, + size: 15, + ), + 'label': '推荐', + 'type': TabType.rcmd, + 'ctr': Get.find, + 'page': const RcmdPage(), + }, + { + 'icon': const Icon( + Icons.whatshot_outlined, + size: 15, + ), + 'label': '热门', + 'type': TabType.hot, + 'ctr': Get.find, + 'page': const HotPage(), + }, + { + 'icon': const Icon( + Icons.play_circle_outlined, + size: 15, + ), + 'label': '番剧', + 'type': TabType.bangumi, + 'ctr': Get.find, + 'page': const BangumiPage(), + }, +]; diff --git a/lib/models/common/theme_type.dart b/lib/models/common/theme_type.dart new file mode 100644 index 00000000..d2dac752 --- /dev/null +++ b/lib/models/common/theme_type.dart @@ -0,0 +1,13 @@ +enum ThemeType { + light, + dark, + system, +} + +extension ThemeTypeDesc on ThemeType { + String get description => ['浅色', '深色', '跟随系统'][index]; +} + +extension ThemeTypeCode on ThemeType { + int get code => [0, 1, 2][index]; +} diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index af5e2cfb..91070215 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -224,10 +224,12 @@ class SearchLiveItemModel { this.liveTime, this.uname, this.uface, + this.face, this.userCover, this.type, this.title, this.cover, + this.pic, this.online, this.rankIndex, this.rankScore, @@ -242,16 +244,19 @@ class SearchLiveItemModel { String? liveTime; String? uname; String? uface; + String? face; String? userCover; String? type; List? title; String? cover; + String? pic; int? online; int? rankIndex; int? rankScore; int? roomid; int? attentions; String? cateName; + Map? watchedShow; SearchLiveItemModel.fromJson(Map json) { rankOffset = json['rank_offset']; @@ -260,10 +265,12 @@ class SearchLiveItemModel { liveTime = json['live_time']; uname = json['uname']; uface = json['uface']; + face = json['uface']; userCover = json['user_cover']; type = json['type']; title = Em.regTitle(json['title']); cover = json['cover']; + pic = json['cover']; online = json['online']; rankIndex = json['rank_index']; rankScore = json['rank_score']; diff --git a/lib/models/video/play/quality.dart b/lib/models/video/play/quality.dart index 7536b971..6b1daa89 100644 --- a/lib/models/video/play/quality.dart +++ b/lib/models/video/play/quality.dart @@ -89,3 +89,46 @@ extension AudioQualityDesc on AudioQuality { ]; get description => _descList[index]; } + +enum VideoDecodeFormats { + AV1, + HEVC, + AVC, +} + +extension VideoDecodeFormatsDesc on VideoDecodeFormats { + static final List _descList = [ + 'AV1', + 'HEVC', + 'AVC', + ]; + get description => _descList[index]; +} + +extension VideoDecodeFormatsCode on VideoDecodeFormats { + static final List _codeList = [ + 'av01', + 'hev1', + 'avc1', + ]; + get code => _codeList[index]; + + static VideoDecodeFormats? fromCode(String code) { + final index = _codeList.indexOf(code); + if (index != -1) { + return VideoDecodeFormats.values[index]; + } + return null; + } + + static VideoDecodeFormats? fromString(String val) { + var result = VideoDecodeFormats.values.first; + for (var i in _codeList) { + if (val.startsWith(i)) { + result = VideoDecodeFormats.values[_codeList.indexOf(i)]; + break; + } + } + return result; + } +} diff --git a/lib/models/video/play/url.dart b/lib/models/video/play/url.dart index 07dd684c..c3109467 100644 --- a/lib/models/video/play/url.dart +++ b/lib/models/video/play/url.dart @@ -29,7 +29,7 @@ class PlayUrlModel { int? timeLength; String? acceptFormat; List? acceptDesc; - List? acceptQuality; + List? acceptQuality; int? videoCodecid; String? seekParam; String? seekType; @@ -48,7 +48,7 @@ class PlayUrlModel { timeLength = json['timelength']; acceptFormat = json['accept_format']; acceptDesc = json['accept_description']; - acceptQuality = json['accept_quality']; + acceptQuality = json['accept_quality'].map((e) => e as int).toList(); videoCodecid = json['video_codecid']; seekParam = json['seek_param']; seekType = json['seek_type']; diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart index 277d1e30..38e0b877 100644 --- a/lib/models/video_detail_res.dart +++ b/lib/models/video_detail_res.dart @@ -580,9 +580,11 @@ class UgcSeason { intro = json['intro']; signState = json['sign_state']; attribute = json['attribute']; - sections = json['sections'] - .map((e) => SectionItem.fromJson(e)) - .toList(); + sections = json['sections'] != null + ? json['sections'] + .map((e) => SectionItem.fromJson(e)) + .toList() + : []; stat = Stat.fromJson(json['stat']); epCount = json['ep_count']; seasonType = json['season_type']; diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index cb02a3f7..7f9f6a61 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -1,3 +1,68 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/bangumi.dart'; +import 'package:pilipala/models/bangumi/list.dart'; +import 'package:pilipala/utils/storage.dart'; -class BangumiController extends GetxController {} +class BangumiController extends GetxController { + final ScrollController scrollController = ScrollController(); + RxList bangumiList = [BangumiListItemModel()].obs; + RxList bangumiFollowList = [BangumiListItemModel()].obs; + int _currentPage = 1; + bool isLoadingMore = true; + Box user = GStrorage.user; + RxBool userLogin = false.obs; + late int mid; + + @override + void onInit() { + super.onInit(); + if (user.get(UserBoxKey.userMid) != null) { + mid = int.parse(user.get(UserBoxKey.userMid).toString()); + } + userLogin.value = user.get(UserBoxKey.userLogin) != null; + } + + Future queryBangumiListFeed({type = 'init'}) async { + if (type == 'init') { + _currentPage = 1; + } + var result = await BangumiHttp.bangumiList(page: _currentPage); + if (result['status']) { + if (type == 'init') { + bangumiList.value = result['data'].list; + } else { + bangumiList.addAll(result['data'].list); + } + _currentPage += 1; + } else {} + isLoadingMore = false; + return result; + } + + // 上拉加载 + Future onLoad() async { + queryBangumiListFeed(type: 'onLoad'); + } + + // 我的订阅 + Future queryBangumiFollow() async { + var result = await BangumiHttp.bangumiFollow(mid: 17340771); + if (result['status']) { + bangumiFollowList.value = result['data'].list; + } else {} + return result; + } + + // 返回顶部并刷新 + void animateToTop() async { + if (scrollController.offset >= + MediaQuery.of(Get.context!).size.height * 5) { + scrollController.jumpTo(0); + } else { + await scrollController.animateTo(0, + duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); + } + } +} diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 1b1599cb..eeedfee6 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -36,7 +36,6 @@ class BangumiIntroController extends GetxController { RxBool isLoading = false.obs; // 视频详情 请求返回 - Rx videoDetail = VideoDetailData().obs; Rx bangumiDetail = BangumiInfoModel().obs; // 请求返回的信息 @@ -89,11 +88,6 @@ class BangumiIntroController extends GetxController { // 获取番剧简介&选集 Future queryBangumiIntro() async { - var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); - if (result['status']) { - bangumiDetail.value = result['data']; - epId = bangumiDetail.value.episodes!.first.id; - } if (userLogin) { // 获取点赞状态 queryHasLikeVideo(); @@ -102,6 +96,11 @@ class BangumiIntroController extends GetxController { // 获取收藏状态 queryHasFavVideo(); } + var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); + if (result['status']) { + bangumiDetail.value = result['data']; + epId = bangumiDetail.value.episodes!.first.id; + } return result; } @@ -132,15 +131,10 @@ class BangumiIntroController extends GetxController { Future actionLikeVideo() async { var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); if (result['status']) { - if (!hasLike.value) { - SmartDialog.showToast('点赞成功 👍'); - hasLike.value = true; - videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1; - } else if (hasLike.value) { - SmartDialog.showToast('取消赞'); - hasLike.value = false; - videoDetail.value.stat!.like = videoDetail.value.stat!.like! - 1; - } + SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞'); + hasLike.value = !hasLike.value; + bangumiDetail.value.stat!['likes'] = + bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1); hasLike.refresh(); } else { SmartDialog.showToast(result['msg']); @@ -193,8 +187,8 @@ class BangumiIntroController extends GetxController { if (res['status']) { SmartDialog.showToast('投币成功 👏'); hasCoin.value = true; - videoDetail.value.stat!.coin = - videoDetail.value.stat!.coin! + _tempThemeValue; + bangumiDetail.value.stat!['coins'] = + bangumiDetail.value.stat!['coins'] + _tempThemeValue; } else { SmartDialog.showToast(res['msg']); } @@ -287,4 +281,13 @@ class BangumiIntroController extends GetxController { await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId); SmartDialog.showToast(result['msg']); } + + Future queryVideoInFolder() async { + var result = await VideoHttp.videoInFolder( + mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid)); + if (result['status']) { + favFolderData.value = result['data']; + } + return result; + } } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index a8503152..96657398 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -33,6 +33,7 @@ class _BangumiIntroPanelState extends State final BangumiIntroController bangumiIntroController = Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']); BangumiInfoModel? bangumiDetail; + late Future _futureBuilderFuture; // 添加页面缓存 @override @@ -44,13 +45,14 @@ class _BangumiIntroPanelState extends State bangumiIntroController.bangumiDetail.listen((value) { bangumiDetail = value; }); + _futureBuilderFuture = bangumiIntroController.queryBangumiIntro(); } @override Widget build(BuildContext context) { super.build(context); return FutureBuilder( - future: bangumiIntroController.queryBangumiIntro(), + future: _futureBuilderFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data['status']) { @@ -89,20 +91,19 @@ class BangumiInfo extends StatefulWidget { } class _BangumiInfoState extends State { - late BangumiInfoModel? bangumiItem; - final BangumiIntroController bangumiIntroController = - Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']); - - late VideoDetailController? videoDetailCtr; + String heroTag = Get.arguments['heroTag']; + late final BangumiIntroController bangumiIntroController; + late final VideoDetailController videoDetailCtr; Box localCache = GStrorage.localCache; + late final BangumiInfoModel? bangumiItem; late double sheetHeight; @override void initState() { super.initState(); + bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); + videoDetailCtr = Get.find(tag: heroTag); bangumiItem = bangumiIntroController.bangumiItem; - videoDetailCtr = - Get.find(tag: Get.arguments['heroTag']); sheetHeight = localCache.get('sheetHeight'); } @@ -357,10 +358,10 @@ class _BangumiInfoState extends State { selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), onTap: () => bangumiIntroController.actionLikeVideo(), selectStatus: bangumiIntroController.hasLike.value, - loadingStatus: widget.loadingStatus, + loadingStatus: false, text: !widget.loadingStatus ? widget.bangumiDetail!.stat!['likes']!.toString() - : '-'), + : bangumiItem!.stat!['likes']!.toString()), ), Obx( () => ActionItem( @@ -368,10 +369,10 @@ class _BangumiInfoState extends State { selectIcon: const Icon(FontAwesomeIcons.b), onTap: () => bangumiIntroController.actionCoinVideo(), selectStatus: bangumiIntroController.hasCoin.value, - loadingStatus: widget.loadingStatus, + loadingStatus: false, text: !widget.loadingStatus ? widget.bangumiDetail!.stat!['coins']!.toString() - : '-'), + : bangumiItem!.stat!['coins']!.toString()), ), Obx( () => ActionItem( @@ -379,29 +380,29 @@ class _BangumiInfoState extends State { selectIcon: const Icon(FontAwesomeIcons.solidStar), onTap: () => showFavBottomSheet(), selectStatus: bangumiIntroController.hasFav.value, - loadingStatus: widget.loadingStatus, + loadingStatus: false, text: !widget.loadingStatus ? widget.bangumiDetail!.stat!['favorite']!.toString() - : '-'), + : bangumiItem!.stat!['favorite']!.toString()), ), ActionItem( icon: const Icon(FontAwesomeIcons.comment), selectIcon: const Icon(FontAwesomeIcons.reply), - onTap: () => videoDetailCtr!.tabCtr!.animateTo(1), + onTap: () => videoDetailCtr.tabCtr!.animateTo(1), selectStatus: false, - loadingStatus: widget.loadingStatus, + loadingStatus: false, text: !widget.loadingStatus ? widget.bangumiDetail!.stat!['reply']!.toString() - : '-', + : bangumiItem!.stat!['reply']!.toString(), ), ActionItem( icon: const Icon(FontAwesomeIcons.shareFromSquare), onTap: () => bangumiIntroController.actionShareVideo(), selectStatus: false, - loadingStatus: widget.loadingStatus, + loadingStatus: false, text: !widget.loadingStatus ? widget.bangumiDetail!.stat!['share']!.toString() - : '-'), + : bangumiItem!.stat!['share']!.toString()), ], ), ), @@ -465,9 +466,6 @@ class _BangumiInfoState extends State { onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, loadingStatus: widget.loadingStatus, - // text: !widget.loadingStatus - // ? widget.videoDetail!.stat!.share!.toString() - // : '-', text: '转发'), ]); } diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index cb1b1ddb..d357c7cd 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -1,4 +1,17 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/main/index.dart'; +import 'package:pilipala/pages/rcmd/view.dart'; +import 'package:pilipala/utils/storage.dart'; + +import 'controller.dart'; +import 'widgets/bangumu_card_v.dart'; class BangumiPage extends StatefulWidget { const BangumiPage({super.key}); @@ -7,12 +20,187 @@ class BangumiPage extends StatefulWidget { State createState() => _BangumiPageState(); } -class _BangumiPageState extends State { +class _BangumiPageState extends State + with AutomaticKeepAliveClientMixin { + final BangumiController _bangumidController = Get.put(BangumiController()); + late Future? _futureBuilderFuture; + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + ScrollController scrollController = _bangumidController.scrollController; + StreamController mainStream = + Get.find().bottomBarStream; + _futureBuilderFuture = _bangumidController.queryBangumiListFeed(); + scrollController.addListener( + () async { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + if (!_bangumidController.isLoadingMore) { + _bangumidController.isLoadingMore = true; + await _bangumidController.onLoad(); + } + } + + final ScrollDirection direction = + scrollController.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + mainStream.add(true); + } else if (direction == ScrollDirection.reverse) { + mainStream.add(false); + } + }, + ); + } + @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Text('还在开发中'), + super.build(context); + return Container( + clipBehavior: Clip.hardEdge, + margin: const EdgeInsets.only( + left: StyleString.safeSpace, right: StyleString.safeSpace), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(StyleString.imgRadius), + ), + child: RefreshIndicator( + onRefresh: () async { + await _bangumidController.queryBangumiListFeed(type: 'init'); + return _bangumidController.queryBangumiFollow(); + }, + child: CustomScrollView( + controller: _bangumidController.scrollController, + slivers: [ + SliverToBoxAdapter( + child: Obx( + () => Visibility( + visible: _bangumidController.userLogin.value, + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.only(top: 10, bottom: 10, left: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '最近追番', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + ), + SizedBox( + height: 254, + child: FutureBuilder( + future: _bangumidController.queryBangumiFollow(), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _bangumidController + .bangumiFollowList.length, + itemBuilder: (context, index) { + return Container( + width: Get.size.width / 3, + height: 254, + margin: EdgeInsets.only( + right: index < + _bangumidController + .bangumiFollowList + .length - + 1 + ? StyleString.safeSpace + : 0), + child: BangumiCardV( + bangumiItem: _bangumidController + .bangumiFollowList[index], + ), + ); + }, + ), + ); + } else { + return SizedBox(); + } + } else { + return SizedBox(); + } + }, + ), + ), + ], + ), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10, left: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '推荐', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + ), + ), + SliverPadding( + padding: EdgeInsets.zero, + sliver: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx(() => contentGrid(_bangumidController, + _bangumidController.bangumiList)); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => {}, + ); + } + } else { + return contentGrid(_bangumidController, []); + } + }, + ), + ), + const LoadingMore() + ], + ), + ), + ); + } + + Widget contentGrid(ctr, bangumiList) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + // 行间距 + mainAxisSpacing: StyleString.cardSpace - 2, + // 列间距 + crossAxisSpacing: StyleString.cardSpace, + // 列数 + crossAxisCount: 3, + mainAxisExtent: Get.size.width / 3 / 0.65 + 30, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return bangumiList!.isNotEmpty + ? BangumiCardV(bangumiItem: bangumiList[index]) + : const SizedBox(); + }, + childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10, ), ); } diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index 85e70d19..83e39d00 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -37,10 +37,10 @@ class _BangumiPanelState extends State { color: Theme.of(context).colorScheme.background, child: Column( children: [ - Container( - height: 45, - padding: const EdgeInsets.only(left: 14, right: 14), - child: Row( + AppBar( + toolbarHeight: 45, + automaticallyImplyLeading: false, + title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( @@ -53,10 +53,7 @@ class _BangumiPanelState extends State { ), ], ), - ), - Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), + titleSpacing: 10, ), Expanded( child: Material( @@ -66,8 +63,15 @@ class _BangumiPanelState extends State { return ListTile( onTap: () => changeFucCall(widget.pages[index], index), dense: false, + leading: index == currentIndex + ? Image.asset( + 'assets/images/live.gif', + color: Theme.of(context).colorScheme.primary, + height: 12, + ) + : null, title: Text( - widget.pages[index].longTitle!, + '第${index + 1}话 ${widget.pages[index].longTitle!}', style: TextStyle( fontSize: 14, color: index == currentIndex @@ -148,6 +152,7 @@ class _BangumiPanelState extends State { child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: widget.pages.length, + itemExtent: 150, itemBuilder: ((context, i) { return Container( width: 150, diff --git a/lib/pages/bangumi/widgets/bangumu_card_v.dart b/lib/pages/bangumi/widgets/bangumu_card_v.dart new file mode 100644 index 00000000..fdc67c1a --- /dev/null +++ b/lib/pages/bangumi/widgets/bangumu_card_v.dart @@ -0,0 +1,173 @@ +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/badge.dart'; +import 'package:pilipala/http/search.dart'; +import 'package:pilipala/models/bangumi/info.dart'; +import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; + +// 视频卡片 - 垂直布局 +class BangumiCardV extends StatelessWidget { + // ignore: prefer_typing_uninitialized_variables + final bangumiItem; + final Function()? longPress; + final Function()? longPressEnd; + + const BangumiCardV({ + Key? key, + required this.bangumiItem, + this.longPress, + this.longPressEnd, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(bangumiItem.mediaId); + return Card( + elevation: 0, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: StyleString.mdRadius, + ), + margin: EdgeInsets.zero, + child: GestureDetector( + // onLongPress: () { + // if (longPress != null) { + // longPress!(); + // } + // }, + // onLongPressEnd: (details) { + // if (longPressEnd != null) { + // longPressEnd!(); + // } + // }, + child: InkWell( + onTap: () async { + int seasonId = bangumiItem.seasonId; + SmartDialog.showLoading(msg: '获取中...'); + var res = await SearchHttp.bangumiInfo(seasonId: seasonId); + SmartDialog.dismiss().then((value) { + if (res['status']) { + if (res['data'].episodes.isEmpty) { + SmartDialog.showToast('资源加载失败'); + return; + } + EpisodeItem episode = res['data'].episodes.first; + String bvid = episode.bvid!; + int cid = episode.cid!; + String pic = episode.cover!; + String heroTag = Utils.makeHeroTag(cid); + Get.toNamed( + '/video?bvid=$bvid&cid=$cid&seasonId=$seasonId', + arguments: { + 'pic': pic, + 'heroTag': heroTag, + 'videoType': SearchType.media_bangumi, + 'bangumiItem': res['data'], + }, + ); + } + }); + }, + child: Column( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: StyleString.imgRadius, + topRight: StyleString.imgRadius, + bottomLeft: StyleString.imgRadius, + bottomRight: StyleString.imgRadius, + ), + child: AspectRatio( + aspectRatio: 0.65, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: bangumiItem.cover, + width: maxWidth, + height: maxHeight, + ), + ), + if (bangumiItem.badge != null) + pBadge(bangumiItem.badge, context, 6, 6, null, null), + if (bangumiItem.order != null) + pBadge(bangumiItem.order, context, null, null, 6, 6, + type: 'gray'), + ], + ); + }), + ), + ), + BangumiContent(bangumiItem: bangumiItem) + ], + ), + ), + ), + ); + } +} + +class BangumiContent extends StatelessWidget { + // ignore: prefer_typing_uninitialized_variables + final bangumiItem; + const BangumiContent({Key? key, required this.bangumiItem}) : super(key: key); + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + // 多列 + padding: const EdgeInsets.fromLTRB(4, 5, 0, 3), + // 单列 + // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Expanded( + child: Text( + bangumiItem.title, + textAlign: TextAlign.start, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + letterSpacing: 0.3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + )), + ], + ), + if (bangumiItem.indexShow != null) + Text( + bangumiItem.indexShow, + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + if (bangumiItem.progress != null) + Text( + bangumiItem.progress, + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index 2e01537b..5bfa08f6 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -29,7 +29,8 @@ class DynamicsPage extends StatefulWidget { class _DynamicsPageState extends State with AutomaticKeepAliveClientMixin { final DynamicsController _dynamicsController = Get.put(DynamicsController()); - Future? _futureBuilderFuture; + late Future _futureBuilderFuture; + late Future _futureBuilderFutureUp; bool _isLoadingMore = false; Box user = GStrorage.user; @@ -40,6 +41,7 @@ class _DynamicsPageState extends State void initState() { super.initState(); _futureBuilderFuture = _dynamicsController.queryFollowDynamic(); + _futureBuilderFutureUp = _dynamicsController.queryFollowUp(); ScrollController scrollController = _dynamicsController.scrollController; StreamController mainStream = Get.find().bottomBarStream; @@ -175,50 +177,6 @@ class _DynamicsPageState extends State icon: const Icon(Icons.history, size: 21), ), ), - Positioned( - left: 10, - top: 0, - bottom: 0, - child: Align( - alignment: Alignment.center, - child: user.get(UserBoxKey.userLogin) ?? false - ? GestureDetector( - onTap: () { - feedBack(); - showModalBottomSheet( - context: context, - builder: (_) => const SizedBox( - height: 450, - child: MinePage(), - ), - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - ); - }, - child: NetworkImgLayer( - type: 'avatar', - width: 30, - height: 30, - src: user.get(UserBoxKey.userFace), - ), - ) - : IconButton( - onPressed: () { - feedBack(); - showModalBottomSheet( - context: context, - builder: (_) => const SizedBox( - height: 450, - child: MinePage(), - ), - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - ); - }, - icon: const Icon(CupertinoIcons.person, size: 22), - ), - ), - ), ], ), ), @@ -229,7 +187,7 @@ class _DynamicsPageState extends State controller: _dynamicsController.scrollController, slivers: [ FutureBuilder( - future: _dynamicsController.queryFollowUp(), + future: _futureBuilderFutureUp, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data; diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index e1054ba0..2c0a63f7 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -40,7 +40,7 @@ class _UpPanelState extends State { 1, UpItem( face: user.get(UserBoxKey.userFace), - uname: '我的', + uname: '我', mid: user.get(UserBoxKey.userMid), ), ); diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index f190bc85..8c242862 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -13,6 +13,13 @@ class FavPage extends StatefulWidget { class _FavPageState extends State { final FavController _favController = Get.put(FavController()); + late Future _futureBuilderFuture; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = _favController.queryFavFolder(); + } @override Widget build(BuildContext context) { @@ -26,7 +33,7 @@ class _FavPageState extends State { ), ), body: FutureBuilder( - future: _favController.queryFavFolder(), + future: _futureBuilderFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data as Map; diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index fe185738..f5ce88ac 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -1,77 +1,51 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/index.dart'; -import 'package:pilipala/pages/bangumi/index.dart'; -import 'package:pilipala/pages/hot/index.dart'; -import 'package:pilipala/pages/live/index.dart'; -import 'package:pilipala/pages/rcmd/index.dart'; +import 'package:pilipala/models/common/tab_type.dart'; +import 'package:pilipala/utils/storage.dart'; class HomeController extends GetxController with GetTickerProviderStateMixin { bool flag = false; - List tabs = [ - { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), - 'label': '直播', - 'type': 'live' - }, - { - 'icon': const Icon( - Icons.thumb_up_off_alt_outlined, - size: 15, - ), - 'label': '推荐', - 'type': 'rcm' - }, - { - 'icon': const Icon( - Icons.whatshot_outlined, - size: 15, - ), - 'label': '热门', - 'type': 'hot' - }, - { - 'icon': const Icon( - Icons.play_circle_outlined, - size: 15, - ), - 'label': '番剧', - 'type': 'bangumi' - }, - ]; + late List tabs; int initialIndex = 1; late TabController tabController; - List ctrList = [ - Get.find, - Get.find, - Get.find, - Get.find, - ]; + late List tabsCtrList; + late List tabsPageList; RxString defaultSearch = '输入关键词搜索'.obs; + Box user = GStrorage.user; + RxBool userLogin = false.obs; + RxString userFace = ''.obs; @override void onInit() { super.onInit(); + + searchDefault(); + userLogin.value = user.get(UserBoxKey.userLogin) ?? false; + userFace.value = user.get(UserBoxKey.userFace) ?? ''; + + // 进行tabs配置 + tabs = tabsConfig; + tabsCtrList = tabsConfig.map((e) => e['ctr']).toList(); + tabsPageList = tabsConfig.map((e) => e['page']).toList(); + tabController = TabController( initialIndex: initialIndex, length: tabs.length, vsync: this, ); - searchDefault(); } void onRefresh() { int index = tabController.index; - var ctr = ctrList[index]; + var ctr = tabsCtrList[index]; ctr().onRefresh(); } void animateToTop() { int index = tabController.index; - var ctr = ctrList[index]; + var ctr = tabsCtrList[index]; ctr().animateToTop(); } @@ -81,4 +55,10 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { defaultSearch.value = res.data['data']['name']; } } + + // 更新登录状态 + void updateLoginStatus(val) { + userLogin.value = val ?? false; + userFace.value = user.get(UserBoxKey.userFace) ?? ''; + } } diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 6a71d664..8c65ebf9 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,7 +1,5 @@ -import 'package:flutter/cupertino.dart'; 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/bangumi/index.dart'; import 'package:pilipala/pages/hot/index.dart'; @@ -10,7 +8,6 @@ import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/rcmd/index.dart'; import 'package:pilipala/utils/feed_back.dart'; -import 'package:pilipala/utils/storage.dart'; import './controller.dart'; class HomePage extends StatefulWidget { @@ -29,6 +26,19 @@ class _HomePageState extends State @override bool get wantKeepAlive => true; + showUserBottonSheet() { + feedBack(); + showModalBottomSheet( + context: context, + builder: (_) => const SizedBox( + height: 450, + child: MinePage(), + ), + clipBehavior: Clip.hardEdge, + isScrollControlled: true, + ); + } + @override Widget build(BuildContext context) { super.build(context); @@ -38,73 +48,68 @@ class _HomePageState extends State appBar: AppBar(toolbarHeight: 0, elevation: 0), body: Column( children: [ - CustomAppBar(stream: stream, ctr: _homeController), - Container( + CustomAppBar( + stream: stream, + ctr: _homeController, + callback: showUserBottonSheet, + ), + Padding( padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4), - child: Stack( - children: [ - Align( - alignment: Alignment.center, - child: Theme( - data: ThemeData( - splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明 - highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明 - ), - child: Padding( - padding: const EdgeInsets.only(top: 2), - child: TabBar( - controller: _homeController.tabController, - tabs: [ - for (var i in _homeController.tabs) - // Tab(text: i['label']) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 0, vertical: 11), - child: Row( - children: [ - i['icon'], - const SizedBox(width: 4), - Text(i['label']) - ], - ), - ), - ], - isScrollable: true, - indicatorWeight: 0, - indicatorPadding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 5), - indicator: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.8), - borderRadius: - const BorderRadius.all(Radius.circular(20)), - ), - indicatorSize: TabBarIndicatorSize.tab, - labelColor: Theme.of(context).colorScheme.primary, - labelStyle: const TextStyle(fontSize: 13), - dividerColor: Colors.transparent, - unselectedLabelColor: - Theme.of(context).colorScheme.outline, - onTap: (value) => - {feedBack(), _homeController.initialIndex = value}, - ), - ), + child: Theme( + data: ThemeData( + splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明 + highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明 + ), + child: TabBar( + controller: _homeController.tabController, + tabs: [ + for (var i in _homeController.tabs) Tab(text: i['label']) + ], + isScrollable: true, + indicatorWeight: 0, + indicatorPadding: const EdgeInsets.only( + top: 37, left: 18, right: 18, bottom: 6), + indicatorColor: Colors.black, + indicator: BoxDecoration( + gradient: RadialGradient( + center: Alignment.centerLeft, + radius: 20.00, + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.background, + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(2), + bottomLeft: Radius.circular(2), + bottomRight: Radius.circular(4), ), ), - ], + indicatorSize: TabBarIndicatorSize.tab, + labelColor: Theme.of(context).colorScheme.primary, + labelStyle: + const TextStyle(fontSize: 13, fontWeight: FontWeight.bold), + dividerColor: Colors.transparent, + unselectedLabelStyle: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontWeight: FontWeight.normal, + ), + unselectedLabelColor: Theme.of(context).colorScheme.outline, + onTap: (value) { + feedBack(); + if (_homeController.initialIndex == value) { + _homeController.tabsCtrList[value]().animateToTop(); + } + _homeController.initialIndex = value; + }, + ), ), ), Expanded( child: TabBarView( controller: _homeController.tabController, - children: const [ - LivePage(), - RcmdPage(), - HotPage(), - BangumiPage(), - ], + children: _homeController.tabsPageList, ), ), ], @@ -116,13 +121,15 @@ class _HomePageState extends State class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { final double height; final Stream? stream; - final ctr; + final HomeController? ctr; + final Function? callback; const CustomAppBar({ super.key, this.height = kToolbarHeight, this.stream, this.ctr, + this.callback, }); @override @@ -130,8 +137,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - Box user = GStrorage.user; - return StreamBuilder( stream: stream, initialData: true, @@ -144,111 +149,100 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { child: AnimatedContainer( curve: Curves.linear, duration: const Duration(milliseconds: 300), - height: snapshot.data ? 94 : MediaQuery.of(context).padding.top, + height: snapshot.data + ? MediaQuery.of(context).padding.top + 42 + : MediaQuery.of(context).padding.top, child: Container( padding: EdgeInsets.only( left: 12, right: 12, - bottom: 4, + bottom: 0, top: MediaQuery.of(context).padding.top, ), - child: Row( - children: [ - const Text( - 'PLPL', - style: TextStyle( - height: 2.8, - fontSize: 17, - fontWeight: FontWeight.bold, - fontFamily: 'Jura-Bold', - ), - ), - const SizedBox(width: 10), - Expanded( - child: GestureDetector( - onTap: () { - Get.toNamed('/search', parameters: { - 'hintText': ctr.defaultSearch.value - }); - }, - child: Container( - width: 250, - height: 45, - clipBehavior: Clip.hardEdge, - padding: const EdgeInsets.only(left: 12, right: 22), - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(25)), - color: - Theme.of(context).colorScheme.onInverseSurface, - ), - child: Row( - children: [ - Icon( - Icons.search_outlined, - size: 23, - color: Theme.of(context).colorScheme.outline, - ), - const SizedBox(width: 7), - Expanded( - child: Obx( - () => Text( - ctr.defaultSearch.value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .outline), - ), + child: Row(children: [ + Image.asset( + 'assets/images/logo/logo_android_2.png', + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 4), + Expanded( + child: GestureDetector( + onTap: () { + Get.toNamed('/search', + parameters: {'hintText': ctr!.defaultSearch.value}); + }, + child: Container( + width: 250, + height: 40, + clipBehavior: Clip.hardEdge, + padding: const EdgeInsets.only(left: 12, right: 22), + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(25)), + color: Theme.of(context).colorScheme.onInverseSurface, + ), + child: Row( + children: [ + Icon( + Icons.search_outlined, + size: 21, + color: Theme.of(context).colorScheme.outline, + ), + const SizedBox(width: 6), + Expanded( + child: Obx( + () => Text( + ctr!.defaultSearch.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline), ), ), - ], - ), + ), + ], ), ), ), - const SizedBox(width: 12), - if (user.get(UserBoxKey.userLogin) ?? false) ...[ - GestureDetector( - onTap: () { - feedBack(); - showModalBottomSheet( - context: context, - builder: (_) => const SizedBox( - height: 450, - child: MinePage(), + ), + const SizedBox(width: 10), + Obx( + () => ctr!.userLogin.value + ? GestureDetector( + onTap: () => callback!(), + child: NetworkImgLayer( + type: 'avatar', + width: 38, + height: 38, + src: ctr!.userFace.value, ), - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - ); - }, - child: NetworkImgLayer( - type: 'avatar', - width: 34, - height: 34, - src: user.get(UserBoxKey.userFace), - ), - ) - ] else ...[ - IconButton( - onPressed: () { - feedBack(); - showModalBottomSheet( - context: context, - builder: (_) => const SizedBox( - height: 450, - child: MinePage(), + ) + : SizedBox( + width: 38, + height: 38, + child: IconButton( + style: ButtonStyle( + padding: + MaterialStateProperty.all(EdgeInsets.zero), + backgroundColor: + MaterialStateProperty.resolveWith((states) { + return Theme.of(context) + .colorScheme + .onInverseSurface; + }), + ), + onPressed: () => callback!(), + icon: Icon( + Icons.person_rounded, + size: 22, + color: Theme.of(context).colorScheme.primary, + ), ), - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - ); - }, - icon: const Icon(CupertinoIcons.person, size: 22), - ) - ], - ], - ), + ), + ), + ]), ), ), ), diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 3e26bafc..20eeb09c 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -22,10 +22,12 @@ class LivePage extends StatefulWidget { class _LivePageState extends State { final LiveController _liveController = Get.put(LiveController()); + late Future _futureBuilderFuture; @override void initState() { super.initState(); + _futureBuilderFuture = _liveController.queryLiveList('init'); ScrollController scrollController = _liveController.scrollController; StreamController mainStream = Get.find().bottomBarStream; @@ -52,47 +54,54 @@ class _LivePageState extends State { @override Widget build(BuildContext context) { - return RefreshIndicator( - onRefresh: () async { - return await _liveController.onRefresh(); - }, - child: CustomScrollView( - controller: _liveController.scrollController, - slivers: [ - SliverPadding( - // 单列布局 EdgeInsets.zero - padding: const EdgeInsets.fromLTRB( - StyleString.safeSpace, 0, StyleString.safeSpace, 0), - sliver: FutureBuilder( - future: _liveController.queryLiveList('init'), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (data['status']) { - return Obx(() => - contentGrid(_liveController, _liveController.liveList)); + return Container( + clipBehavior: Clip.hardEdge, + margin: const EdgeInsets.only( + left: StyleString.safeSpace, right: StyleString.safeSpace), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(StyleString.imgRadius), + ), + child: RefreshIndicator( + onRefresh: () async { + return await _liveController.onRefresh(); + }, + child: CustomScrollView( + controller: _liveController.scrollController, + slivers: [ + SliverPadding( + // 单列布局 EdgeInsets.zero + padding: EdgeInsets.zero, + sliver: FutureBuilder( + future: _liveController.queryLiveList('init'), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx(() => contentGrid( + _liveController, _liveController.liveList)); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => {}, + ); + } } else { - return HttpError( - errMsg: data['msg'], - fn: () => {}, - ); + // 缓存数据 + if (_liveController.liveList.length > 1) { + return contentGrid( + _liveController, _liveController.liveList); + } + // 骨架屏 + else { + return contentGrid(_liveController, []); + } } - } else { - // 缓存数据 - if (_liveController.liveList.length > 1) { - return contentGrid( - _liveController, _liveController.liveList); - } - // 骨架屏 - else { - return contentGrid(_liveController, []); - } - } - }, + }, + ), ), - ), - const LoadingMore() - ], + const LoadingMore() + ], + ), ), ); } diff --git a/lib/pages/liveRoom/controller.dart b/lib/pages/liveRoom/controller.dart index b4042e17..28d039d8 100644 --- a/lib/pages/liveRoom/controller.dart +++ b/lib/pages/liveRoom/controller.dart @@ -31,6 +31,9 @@ class LiveRoomController extends GetxController { liveItem = Get.arguments['liveItem']; heroTag = Get.arguments['heroTag'] ?? ''; if (liveItem.pic != null && liveItem.pic != '') { + cover = liveItem.pic; + } + if (liveItem.cover != null && liveItem.cover != '') { cover = liveItem.cover; } } diff --git a/lib/pages/liveRoom/view.dart b/lib/pages/liveRoom/view.dart index 1fe31c9e..8409d4a6 100644 --- a/lib/pages/liveRoom/view.dart +++ b/lib/pages/liveRoom/view.dart @@ -112,68 +112,67 @@ class _LiveRoomPageState extends State { ], ), ), - if (_liveRoomController.liveItem.watchedShow != null) - Container( - height: 45, - padding: const EdgeInsets.only(left: 12, right: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor.withOpacity(0.1)), + Container( + height: 45, + padding: const EdgeInsets.only(left: 12, right: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.1)), + ), + ), + child: Row(children: [ + SizedBox( + width: 38, + height: 38, + child: IconButton( + onPressed: () {}, + icon: const Icon( + Icons.subtitles_outlined, + size: 21, + ), ), ), - child: Row(children: [ - SizedBox( - width: 38, - height: 38, - child: IconButton( - onPressed: () {}, - icon: const Icon( - Icons.subtitles_outlined, - size: 21, - ), + const Spacer(), + SizedBox( + width: 38, + height: 38, + child: IconButton( + onPressed: () {}, + icon: const Icon( + Icons.hd_outlined, + size: 20, ), ), - const Spacer(), - SizedBox( - width: 38, - height: 38, - child: IconButton( - onPressed: () {}, - icon: const Icon( - Icons.hd_outlined, - size: 20, - ), + ), + SizedBox( + width: 38, + height: 38, + child: IconButton( + onPressed: () => _liveRoomController + .setVolumn(plPlayerController!.volume.value), + icon: Obx(() => Icon( + _liveRoomController.volumeOff.value + ? Icons.volume_off_outlined + : Icons.volume_up_outlined, + size: 21, + )), + ), + ), + SizedBox( + width: 38, + height: 38, + child: IconButton( + onPressed: () => {}, + // plPlayerController!.goToFullscreen(context), + icon: const Icon( + Icons.fullscreen, ), ), - SizedBox( - width: 38, - height: 38, - child: IconButton( - onPressed: () => _liveRoomController - .setVolumn(plPlayerController!.volume.value), - icon: Obx(() => Icon( - _liveRoomController.volumeOff.value - ? Icons.volume_off_outlined - : Icons.volume_up_outlined, - size: 21, - )), - ), - ), - SizedBox( - width: 38, - height: 38, - child: IconButton( - onPressed: () => {}, - // plPlayerController!.goToFullscreen(context), - icon: const Icon( - Icons.fullscreen, - ), - ), - ), - ]), - ), + ), + ]), + ), ], ), ); diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 67d49c82..4cec9ac8 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -143,7 +143,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { // type: BottomNavigationBarType.shifting, selectedItemColor: Theme.of(context).colorScheme.primary, unselectedItemColor: - Theme.of(context).colorScheme.onSurfaceVariant, + Theme.of(context).colorScheme.outline.withOpacity(0.5), selectedFontSize: 12.4, onTap: (value) => setIndex(value), items: [ diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index e1794d97..54b96ea7 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -14,13 +14,22 @@ class MediaPage extends StatefulWidget { 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(); + } + @override Widget build(BuildContext context) { super.build(context); - final MediaController mediaController = Get.put(MediaController()); Color primary = Theme.of(context).colorScheme.primary; return Scaffold( appBar: AppBar(toolbarHeight: 30), @@ -107,7 +116,7 @@ class _MediaPageState extends State ), ), trailing: IconButton( - onPressed: () => mediaController.queryFavFolder(), + onPressed: () => _futureBuilderFuture, icon: const Icon( Icons.refresh, size: 20, @@ -119,7 +128,7 @@ class _MediaPageState extends State width: double.infinity, height: 170, child: FutureBuilder( - future: mediaController.queryFavFolder(), + future: _futureBuilderFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data as Map; diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 6d515243..9562f488 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -1,6 +1,8 @@ +import 'package:flutter/material.dart'; 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/info.dart'; import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/utils/storage.dart'; @@ -10,9 +12,11 @@ class MineController extends GetxController { Rx userInfo = UserInfoData().obs; // 用户状态 动态、关注、粉丝 Rx userStat = UserStat().obs; - Box user = GStrorage.user; RxBool userLogin = false.obs; + Box user = GStrorage.user; + Box setting = GStrorage.setting; Box userInfoCache = GStrorage.userInfo; + Rx themeType = ThemeType.system.obs; @override onInit() { @@ -21,6 +25,9 @@ class MineController extends GetxController { if (userInfoCache.get('userInfoCache') != null) { userInfo.value = userInfoCache.get('userInfoCache'); } + + themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode, + defaultValue: ThemeType.system.code)]; } onLogin() async { @@ -90,4 +97,31 @@ class MineController extends GetxController { userLogin.value = false; // Get.find().resetLast(); } + + onChangeTheme() { + Brightness currentBrightness = + MediaQuery.of(Get.context!).platformBrightness; + ThemeType currentTheme = themeType.value; + switch (currentTheme) { + case ThemeType.dark: + setting.put(SettingBoxKey.themeMode, ThemeType.light.code); + themeType.value = ThemeType.light; + break; + case ThemeType.light: + setting.put(SettingBoxKey.themeMode, ThemeType.dark.code); + themeType.value = ThemeType.dark; + break; + case ThemeType.system: + // 判断当前的颜色模式 + if (currentBrightness == Brightness.light) { + setting.put(SettingBoxKey.themeMode, ThemeType.dark.code); + themeType.value = ThemeType.dark; + } else { + setting.put(SettingBoxKey.themeMode, ThemeType.light.code); + themeType.value = ThemeType.light; + } + break; + } + Get.forceAppUpdate(); + } } diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 3cf560dc..102e47bf 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.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/utils/storage.dart'; import 'controller.dart'; class MinePage extends StatelessWidget { @@ -21,16 +23,23 @@ class MinePage extends StatelessWidget { elevation: 0, toolbarHeight: kTextTabBarHeight + 20, backgroundColor: Colors.transparent, - title: null, + centerTitle: false, + title: const Text( + 'PLPL', + style: TextStyle( + height: 2.8, + fontSize: 17, + fontWeight: FontWeight.bold, + fontFamily: 'Jura-Bold', + ), + ), actions: [ IconButton( - onPressed: () { - Get.changeThemeMode(ThemeMode.dark); - }, + onPressed: () => mineController.onChangeTheme(), icon: Icon( - Get.theme == ThemeData.light() - ? CupertinoIcons.moon - : CupertinoIcons.sun_max, + mineController.themeType.value == ThemeType.dark + ? CupertinoIcons.sun_max + : CupertinoIcons.moon, size: 22, ), ), @@ -93,7 +102,7 @@ class MinePage extends StatelessWidget { src: _mineController.userInfo.value.face, width: 85, height: 85) - : Image.asset('assets/images/loading.png'), + : Image.asset('assets/images/noface.jpeg'), ), ), ), diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart index 1b22a2db..8fea2f79 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -23,6 +23,7 @@ class RcmdPage extends StatefulWidget { class _RcmdPageState extends State with AutomaticKeepAliveClientMixin { final RcmdController _rcmdController = Get.put(RcmdController()); + late Future _futureBuilderFuture; @override bool get wantKeepAlive => true; @@ -30,6 +31,7 @@ class _RcmdPageState extends State @override void initState() { super.initState(); + _futureBuilderFuture = _rcmdController.queryRcmdFeed('init'); ScrollController scrollController = _rcmdController.scrollController; StreamController mainStream = Get.find().bottomBarStream; @@ -57,49 +59,56 @@ class _RcmdPageState extends State @override Widget build(BuildContext context) { super.build(context); - return RefreshIndicator( - onRefresh: () async { - return await _rcmdController.onRefresh(); - }, - child: CustomScrollView( - controller: _rcmdController.scrollController, - slivers: [ - SliverPadding( - // 单列布局 EdgeInsets.zero - padding: _rcmdController.crossAxisCount == 1 - ? EdgeInsets.zero - : const EdgeInsets.fromLTRB( - StyleString.safeSpace, 0, StyleString.safeSpace, 0), - sliver: FutureBuilder( - future: _rcmdController.queryRcmdFeed('init'), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (data['status']) { - return Obx(() => contentGrid( - _rcmdController, _rcmdController.videoList)); + return Container( + clipBehavior: Clip.hardEdge, + margin: const EdgeInsets.only( + left: StyleString.safeSpace, right: StyleString.safeSpace), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(StyleString.imgRadius), + ), + child: RefreshIndicator( + onRefresh: () async { + return await _rcmdController.onRefresh(); + }, + child: CustomScrollView( + controller: _rcmdController.scrollController, + slivers: [ + SliverPadding( + // 单列布局 EdgeInsets.zero + padding: _rcmdController.crossAxisCount == 1 + ? EdgeInsets.zero + : const EdgeInsets.fromLTRB(0, 0, 0, 0), + sliver: FutureBuilder( + future: _rcmdController.queryRcmdFeed('init'), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx(() => contentGrid( + _rcmdController, _rcmdController.videoList)); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => {}, + ); + } } else { - return HttpError( - errMsg: data['msg'], - fn: () => {}, - ); + // 缓存数据 + if (_rcmdController.videoList.length > 1) { + return contentGrid( + _rcmdController, _rcmdController.videoList); + } + // 骨架屏 + else { + return contentGrid(_rcmdController, []); + } } - } else { - // 缓存数据 - if (_rcmdController.videoList.length > 1) { - return contentGrid( - _rcmdController, _rcmdController.videoList); - } - // 骨架屏 - else { - return contentGrid(_rcmdController, []); - } - } - }, + }, + ), ), - ), - const LoadingMore() - ], + const LoadingMore() + ], + ), ), ); } diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 5183c635..8fc33ffc 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -56,7 +56,7 @@ class SSearchController extends GetxController { } void onClear() { - if (searchKeyWord.value.isNotEmpty) { + if (searchKeyWord.value.isNotEmpty && controller.value.text != '') { controller.value.clear(); searchKeyWord.value = ''; searchSuggestList.value = []; diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 4a1f809d..726b0a0d 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -211,13 +211,13 @@ class _SearchPageState extends State with RouteAware { return Obx( () => Container( width: double.infinity, - padding: const EdgeInsets.fromLTRB(10, 25, 4, 0), + padding: const EdgeInsets.fromLTRB(10, 25, 6, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_searchController.historyList.isNotEmpty) Padding( - padding: const EdgeInsets.fromLTRB(6, 0, 1, 2), + padding: const EdgeInsets.fromLTRB(6, 0, 0, 2), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/pages/searchPanel/controller.dart b/lib/pages/searchPanel/controller.dart index 640ae246..b8e4a166 100644 --- a/lib/pages/searchPanel/controller.dart +++ b/lib/pages/searchPanel/controller.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/search.dart'; import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/utils/id_utils.dart'; +import 'package:pilipala/utils/utils.dart'; class SearchPanelController extends GetxController { SearchPanelController({this.keyword, this.searchType}); @@ -21,6 +23,7 @@ class SearchPanelController extends GetxController { } else if (type == 'onRefresh') { resultList.value = result['data'].list; } + onPushDetail(keyword, resultList); } return result; } @@ -40,4 +43,24 @@ class SearchPanelController extends GetxController { duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); } } + + void onPushDetail(keyword, resultList) async { + // 匹配输入内容,如果是AV、BV号且有结果 直接跳转详情页 + Map matchRes = IdUtils.matchAvorBv(input: keyword); + List matchKeys = matchRes.keys.toList(); + if (matchKeys.isNotEmpty && searchType == SearchType.video) { + String bvid = resultList.first.bvid; + int aid = resultList.first.aid; + String heroTag = Utils.makeHeroTag(bvid); + + int cid = await SearchHttp.ab2c(aid: aid, bvid: bvid); + if (matchKeys.first == 'BV' && matchRes[matchKeys.first] == bvid || + matchKeys.first == 'AV' && matchRes[matchKeys.first] == aid) { + Get.toNamed( + '/video?bvid=$bvid&cid=$cid', + arguments: {'videoItem': resultList.first, 'heroTag': heroTag}, + ); + } + } + } } diff --git a/lib/pages/searchPanel/view.dart b/lib/pages/searchPanel/view.dart index 5dd15b8f..560e9d73 100644 --- a/lib/pages/searchPanel/view.dart +++ b/lib/pages/searchPanel/view.dart @@ -25,7 +25,7 @@ class SearchPanel extends StatefulWidget { class _SearchPanelState extends State with AutomaticKeepAliveClientMixin { - late SearchPanelController? _searchPanelController; + late SearchPanelController _searchPanelController; bool _isLoadingMore = false; late Future _futureBuilderFuture; @@ -41,10 +41,9 @@ class _SearchPanelState extends State keyword: widget.keyword, searchType: widget.searchType, ), - tag: widget.searchType!.type + widget.tag!, + tag: widget.searchType!.type, ); - ScrollController scrollController = - _searchPanelController!.scrollController; + ScrollController scrollController = _searchPanelController.scrollController; scrollController.addListener(() async { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { @@ -55,7 +54,7 @@ class _SearchPanelState extends State } } }); - _futureBuilderFuture = _searchPanelController!.onSearch(); + _futureBuilderFuture = _searchPanelController.onSearch(); } @override @@ -63,7 +62,7 @@ class _SearchPanelState extends State super.build(context); return RefreshIndicator( onRefresh: () async { - await _searchPanelController!.onRefresh(); + await _searchPanelController.onRefresh(); }, child: FutureBuilder( future: _futureBuilderFuture, @@ -71,7 +70,7 @@ class _SearchPanelState extends State if (snapshot.connectionState == ConnectionState.done) { Map data = snapshot.data; var ctr = _searchPanelController; - List list = ctr!.resultList; + List list = ctr.resultList; if (data['status']) { return Obx(() { switch (widget.searchType) { diff --git a/lib/pages/searchPanel/widgets/live_panel.dart b/lib/pages/searchPanel/widgets/live_panel.dart index f00660e6..02b56d3a 100644 --- a/lib/pages/searchPanel/widgets/live_panel.dart +++ b/lib/pages/searchPanel/widgets/live_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/utils/utils.dart'; @@ -32,6 +33,7 @@ class LiveItem extends StatelessWidget { @override Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(liveItem.roomid); return Card( elevation: 0, clipBehavior: Clip.hardEdge, @@ -40,7 +42,10 @@ class LiveItem extends StatelessWidget { ), margin: EdgeInsets.zero, child: InkWell( - onTap: () {}, + onTap: () async { + Get.toNamed('/liveRoom?roomid=${liveItem.roomid}', + arguments: {'liveItem': liveItem, 'heroTag': heroTag}); + }, child: Column( children: [ ClipRRect( @@ -58,7 +63,7 @@ class LiveItem extends StatelessWidget { return Stack( children: [ Hero( - tag: Utils.makeHeroTag(liveItem.roomid), + tag: heroTag, child: NetworkImgLayer( src: liveItem.cover, type: 'emote', diff --git a/lib/pages/searchResult/view.dart b/lib/pages/searchResult/view.dart index 00c917f7..bbd89cd2 100644 --- a/lib/pages/searchResult/view.dart +++ b/lib/pages/searchResult/view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/pages/rcmd/index.dart'; import 'package:pilipala/pages/searchPanel/index.dart'; import 'controller.dart'; @@ -88,6 +89,7 @@ class _SearchResultPageState extends State tag: SearchType.values[index].type) .animateToTop(); } + _searchResultController!.tabIndex = index; }, ), diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart index 2a394587..70405e4b 100644 --- a/lib/pages/setting/controller.dart +++ b/lib/pages/setting/controller.dart @@ -1,16 +1,21 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/common/theme_type.dart'; +import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/mine/controller.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; class SettingController extends GetxController { Box user = GStrorage.user; - RxBool userLogin = false.obs; - Box userInfoCache = GStrorage.userInfo; Box setting = GStrorage.setting; + Box userInfoCache = GStrorage.userInfo; + + RxBool userLogin = false.obs; RxBool feedBackEnable = false.obs; + RxInt picQuality = 10.obs; + Rx themeType = ThemeType.system.obs; @override void onInit() { @@ -18,6 +23,10 @@ class SettingController extends GetxController { userLogin.value = user.get(UserBoxKey.userLogin) ?? false; feedBackEnable.value = setting.get(SettingBoxKey.feedBackEnable, defaultValue: false); + picQuality.value = + setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); + themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode, + defaultValue: ThemeType.system.code)]; } loginOut() async { @@ -25,6 +34,8 @@ class SettingController extends GetxController { await Get.find().resetUserInfo(); userLogin.value = user.get(UserBoxKey.userLogin) ?? false; userInfoCache.put('userInfoCache', null); + HomeController homeCtr = Get.find(); + homeCtr.updateLoginStatus(false); } // 开启关闭震动反馈 diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart new file mode 100644 index 00000000..d8fc7413 --- /dev/null +++ b/lib/pages/setting/play_setting.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/models/video/play/quality.dart'; +import 'package:pilipala/utils/storage.dart'; + +import 'widgets/switch_item.dart'; + +class PlaySetting extends StatefulWidget { + const PlaySetting({super.key}); + + @override + State createState() => _PlaySettingState(); +} + +class _PlaySettingState extends State { + Box setting = GStrorage.setting; + late dynamic defaultVideoQa; + late dynamic defaultAudioQa; + late dynamic defaultDecode; + + @override + void initState() { + super.initState(); + defaultVideoQa = setting.get(SettingBoxKey.defaultVideoQa, + defaultValue: VideoQuality.values.last.code); + defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa, + defaultValue: AudioQuality.values.last.code); + defaultDecode = setting.get(SettingBoxKey.defaultDecode, + defaultValue: VideoDecodeFormats.values.last.code); + } + + @override + Widget build(BuildContext context) { + TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!; + TextStyle subTitleStyle = Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline); + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + title: Text( + '播放设置', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: ListView( + children: [ + const SetSwitchItem( + title: '自动播放', + subTitle: '进入详情页自动播放', + setKey: SettingBoxKey.autoPlayEnable, + defaultVal: true, + ), + const SetSwitchItem( + title: '开启硬解', + subTitle: '以较低功耗播放视频', + setKey: SettingBoxKey.enableHA, + defaultVal: true, + ), + ListTile( + dense: false, + title: Text('默认画质', style: titleStyle), + subtitle: Text( + '当前画质' + VideoQualityCode.fromCode(defaultVideoQa)!.description!, + style: subTitleStyle, + ), + trailing: PopupMenuButton( + initialValue: defaultVideoQa, + icon: const Icon(Icons.arrow_forward_rounded, size: 22), + onSelected: (item) { + defaultVideoQa = item; + setting.put(SettingBoxKey.defaultVideoQa, item); + setState(() {}); + }, + itemBuilder: (BuildContext context) => [ + for (var i in VideoQuality.values.reversed) ...[ + PopupMenuItem( + value: i.code, + child: Text(i.description), + ), + ] + ], + ), + ), + ListTile( + dense: false, + title: Text('默认音质', style: titleStyle), + subtitle: Text( + '当前音质' + AudioQualityCode.fromCode(defaultAudioQa)!.description!, + style: subTitleStyle, + ), + trailing: PopupMenuButton( + initialValue: defaultAudioQa, + icon: const Icon(Icons.arrow_forward_rounded, size: 22), + onSelected: (item) { + defaultAudioQa = item; + setting.put(SettingBoxKey.defaultAudioQa, item); + setState(() {}); + }, + itemBuilder: (BuildContext context) => [ + for (var i in AudioQuality.values.reversed) ...[ + PopupMenuItem( + value: i.code, + child: Text(i.description), + ), + ] + ], + ), + ), + ListTile( + dense: false, + title: Text('默认解码格式', style: titleStyle), + subtitle: Text( + '当前解码格式' + + VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!, + style: subTitleStyle, + ), + trailing: PopupMenuButton( + initialValue: defaultDecode, + icon: const Icon(Icons.arrow_forward_rounded, size: 22), + onSelected: (item) { + defaultDecode = item; + setting.put(SettingBoxKey.defaultDecode, item); + setState(() {}); + }, + itemBuilder: (BuildContext context) => [ + for (var i in VideoDecodeFormats.values) ...[ + PopupMenuItem( + value: i.code, + child: Text(i.description), + ), + ] + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart new file mode 100644 index 00000000..397edbbf --- /dev/null +++ b/lib/pages/setting/style_setting.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/models/common/theme_type.dart'; +import 'package:pilipala/utils/storage.dart'; + +import 'controller.dart'; + +class StyleSetting extends StatefulWidget { + const StyleSetting({super.key}); + + @override + State createState() => _StyleSettingState(); +} + +class _StyleSettingState extends State { + final SettingController settingController = Get.put(SettingController()); + Box setting = GStrorage.setting; + late int picQuality; + late ThemeType _tempThemeValue; + + @override + void initState() { + super.initState(); + picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); + _tempThemeValue = settingController.themeType.value; + } + + @override + Widget build(BuildContext context) { + TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!; + TextStyle subTitleStyle = Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline); + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + title: Text( + '外观设置', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: ListView( + children: [ + Obx( + () => ListTile( + enableFeedback: true, + onTap: () => settingController.onOpenFeedBack(), + title: const Text('震动反馈'), + subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle), + trailing: Transform.scale( + scale: 0.8, + child: Switch( + thumbIcon: MaterialStateProperty.resolveWith( + (Set states) { + if (states.isNotEmpty && + states.first == MaterialState.selected) { + return const Icon(Icons.done); + } + return null; // All other states will use the default thumbIcon. + }), + value: settingController.feedBackEnable.value, + onChanged: (value) => settingController.onOpenFeedBack()), + ), + ), + ), + ListTile( + dense: false, + onTap: () { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, StateSetter setState) { + final SettingController settingController = + Get.put(SettingController()); + return AlertDialog( + title: const Text('图片质量'), + contentPadding: const EdgeInsets.only( + top: 20, left: 8, right: 8, bottom: 8), + content: SizedBox( + height: 40, + child: Slider( + value: picQuality.toDouble(), + min: 10, + max: 100, + divisions: 9, + label: '$picQuality%', + onChanged: (double val) { + picQuality = val.toInt(); + setState(() {}); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('取消', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline))), + TextButton( + onPressed: () { + setting.put( + SettingBoxKey.defaultPicQa, picQuality); + Get.back(); + settingController.picQuality.value = picQuality; + }, + child: const Text('确定'), + ) + ], + ); + }, + ); + }, + ); + }, + title: Text('图片质量', style: titleStyle), + subtitle: Text('选择合适的图片清晰度,上限100%', style: subTitleStyle), + trailing: Obx( + () => Text( + '${settingController.picQuality.value}%', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ), + ListTile( + dense: false, + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('主题模式'), + contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + content: StatefulBuilder( + builder: (context, StateSetter setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i in ThemeType.values) ...[ + RadioListTile( + value: i, + title: Text(i.description, style: titleStyle), + groupValue: _tempThemeValue, + onChanged: (ThemeType? value) { + setState(() { + _tempThemeValue = i; + }); + }, + ), + ] + ], + ); + }), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + )), + TextButton( + onPressed: () { + settingController.themeType.value = _tempThemeValue; + setting.put( + SettingBoxKey.themeMode, _tempThemeValue.code); + Get.forceAppUpdate(); + Get.back(); + }, + child: const Text('确定')) + ], + ); + }, + ); + }, + title: Text('主题模式', style: titleStyle), + subtitle: Obx(() => Text( + '当前模式:${settingController.themeType.value.description}', + style: subTitleStyle)), + trailing: const Icon(Icons.arrow_right_alt_outlined), + ), + ], + ), + ); + } +} diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index 155eec2a..752952d0 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -14,32 +14,30 @@ class SettingPage extends StatelessWidget { final SettingController settingController = Get.put(SettingController()); return Scaffold( appBar: AppBar( - title: const Text('设置'), + centerTitle: false, + titleSpacing: 0, + title: Text( + '设置', + style: Theme.of(context).textTheme.titleMedium, + ), ), body: Column( children: [ - Obx( - () => ListTile( - enableFeedback: true, - onTap: () => settingController.onOpenFeedBack(), - title: const Text('震动反馈'), - subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle), - trailing: Transform.scale( - scale: 0.8, - child: Switch( - thumbIcon: MaterialStateProperty.resolveWith( - (Set states) { - if (states.isNotEmpty && - states.first == MaterialState.selected) { - return const Icon(Icons.done); - } - return null; // All other states will use the default thumbIcon. - }), - value: settingController.feedBackEnable.value, - onChanged: (value) => settingController.onOpenFeedBack()), - ), - ), + ListTile( + onTap: () => Get.toNamed('/playSetting'), + dense: false, + title: const Text('播放设置'), ), + ListTile( + onTap: () => Get.toNamed('/styleSetting'), + dense: false, + title: const Text('外观设置'), + ), + // ListTile( + // onTap: () {}, + // dense: false, + // title: const Text('其他设置'), + // ), Obx( () => Visibility( visible: settingController.userLogin.value, diff --git a/lib/pages/setting/widgets/select_item.dart b/lib/pages/setting/widgets/select_item.dart new file mode 100644 index 00000000..297a5ecc --- /dev/null +++ b/lib/pages/setting/widgets/select_item.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/models/video/play/quality.dart'; +import 'package:pilipala/utils/storage.dart'; + +class SetSelectItem extends StatefulWidget { + final String? title; + final String? subTitle; + final String? setKey; + const SetSelectItem({ + this.title, + this.subTitle, + this.setKey, + Key? key, + }) : super(key: key); + + @override + State createState() => _SetSelectItemState(); +} + +class _SetSelectItemState extends State { + Box Setting = GStrorage.setting; + late var currentVal; + late int currentIndex; + late List menus; + late List popMenuItems; + + @override + void initState() { + super.initState(); + late String defaultVal; + switch (widget.setKey) { + case 'defaultVideoQa': + defaultVal = VideoQuality.values.last.description; + List list = menus = VideoQuality.values.reversed.toList(); + currentVal = Setting.get(widget.setKey, defaultValue: defaultVal); + currentIndex = + list.firstWhere((i) => i.description == currentVal).index; + + popMenuItems = [ + for (var i in list) ...[ + PopupMenuItem( + value: i.code, + child: Text(i.description), + ) + ] + ]; + + break; + case 'defaultAudioQa': + defaultVal = AudioQuality.values.last.description; + List list = menus = AudioQuality.values.reversed.toList(); + currentVal = Setting.get(widget.setKey, defaultValue: defaultVal); + currentIndex = + list.firstWhere((i) => i.description == currentVal).index; + + popMenuItems = [ + for (var i in list) ...[ + PopupMenuItem( + value: i.index, + child: Text(i.description), + ), + ] + ]; + break; + case 'defaultDecode': + defaultVal = VideoDecodeFormats.values[0].description; + currentVal = Setting.get(widget.setKey, defaultValue: defaultVal); + List list = menus = VideoDecodeFormats.values; + + currentIndex = + list.firstWhere((i) => i.description == currentVal).index; + + popMenuItems = [ + for (var i in list) ...[ + PopupMenuItem( + value: i.index, + child: Text(i.description), + ), + ] + ]; + break; + case 'defaultVideoSpeed': + defaultVal = '1.0'; + currentVal = Setting.get(widget.setKey, defaultValue: defaultVal); + + break; + } + } + + @override + Widget build(BuildContext context) { + TextStyle subTitleStyle = Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline); + return ListTile( + onTap: () {}, + dense: false, + title: Text(widget.title!), + subtitle: Text( + '当前${widget.title!} $currentVal', + style: subTitleStyle, + ), + trailing: PopupMenuButton( + initialValue: currentIndex, + icon: const Icon( + Icons.arrow_forward_rounded, + size: 22, + ), + onSelected: (item) { + currentVal = menus.firstWhere((e) => e.code == item).first; + setState(() {}); + }, + itemBuilder: (BuildContext context) => + [...popMenuItems], + ), + ); + } +} diff --git a/lib/pages/setting/widgets/switch_item.dart b/lib/pages/setting/widgets/switch_item.dart new file mode 100644 index 00000000..6ae642d3 --- /dev/null +++ b/lib/pages/setting/widgets/switch_item.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/utils/storage.dart'; + +class SetSwitchItem extends StatefulWidget { + final String? title; + final String? subTitle; + final String? setKey; + final bool? defaultVal; + + const SetSwitchItem({ + this.title, + this.subTitle, + this.setKey, + this.defaultVal, + Key? key, + }) : super(key: key); + + @override + State createState() => _SetSwitchItemState(); +} + +class _SetSwitchItemState extends State { + // ignore: non_constant_identifier_names + Box Setting = GStrorage.setting; + late bool val; + + @override + void initState() { + super.initState(); + val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false); + } + + @override + Widget build(BuildContext context) { + TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!; + TextStyle subTitleStyle = Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline); + return ListTile( + enableFeedback: true, + onTap: () { + Setting.put(widget.setKey, !val); + }, + title: Text(widget.title!, style: titleStyle), + subtitle: widget.subTitle != null + ? Text(widget.subTitle!, style: subTitleStyle) + : null, + trailing: Transform.scale( + scale: 0.8, + child: Switch( + thumbIcon: MaterialStateProperty.resolveWith( + (Set states) { + if (states.isNotEmpty && states.first == MaterialState.selected) { + return const Icon(Icons.done); + } + return null; // All other states will use the default thumbIcon. + }), + value: val, + onChanged: (value) { + val = value; + Setting.put(widget.setKey, value); + setState(() {}); + }), + ), + ); + } +} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index dc24df06..ebc5dd45 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -13,36 +13,47 @@ import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/utils/storage.dart'; +import 'package:pilipala/utils/utils.dart'; class VideoDetailController extends GetxController with GetSingleTickerProviderStateMixin { - int tabInitialIndex = 0; - TabController? tabCtr; - // tabs - RxList tabs = ['简介', '评论'].obs; - - // 视频aid + /// 路由传参 String bvid = Get.parameters['bvid']!; int cid = int.parse(Get.parameters['cid']!); - // 视频类型 默认投稿视频 - SearchType videoType = SearchType.video; - - late PlayUrlModel data; - // 当前画质 - late VideoQuality currentVideoQa; - // 当前音质 - late AudioQuality currentAudioQa; - - // 是否预渲染 骨架屏 - bool preRender = false; - - // 视频详情 上个页面传入 + String heroTag = Get.arguments['heroTag']; + // 视频详情 Map videoItem = {}; + // 视频类型 默认投稿视频 + SearchType videoType = Get.arguments['videoType'] ?? SearchType.video; + /// tabs相关配置 + int tabInitialIndex = 0; + late TabController tabCtr; + RxList tabs = ['简介', '评论'].obs; + + // 请求返回的视频信息 + late PlayUrlModel data; // 请求状态 RxBool isLoading = false.obs; - String heroTag = ''; + /// 播放器配置 画质 音质 解码格式 + late VideoQuality currentVideoQa; + late AudioQuality currentAudioQa; + late VideoDecodeFormats currentDecodeFormats; + // PlPlayerController plPlayerController = PlPlayerController(); + // 是否开始自动播放 存在多p的情况下,第二p需要为true + RxBool autoPlay = true.obs; + // 视频资源是否有效 + RxBool isEffective = true.obs; + // 封面图的展示 + RxBool isShowCover = true.obs; + // 硬解 + RxBool enableHA = true.obs; + + /// 本地存储 + Box user = GStrorage.user; + Box localCache = GStrorage.localCache; + Box setting = GStrorage.setting; int oid = 0; // 评论id 请求楼中楼评论使用 @@ -52,15 +63,7 @@ class VideoDetailController extends GetxController final scaffoldKey = GlobalKey(); Timer? timer; RxString bgCover = ''.obs; - Box user = GStrorage.user; - Box localCache = GStrorage.localCache; PlPlayerController plPlayerController = PlPlayerController.getInstance(); - // 是否开始自动播放 存在多p的情况下,第二p需要为true - RxBool autoPlay = true.obs; - // 视频资源是否有效 - RxBool isEffective = true.obs; - // 封面图的展示 - RxBool isShowCover = true.obs; late VideoItem firstVideo; late String videoUrl; @@ -70,24 +73,23 @@ class VideoDetailController extends GetxController @override void onInit() { super.onInit(); - if (Get.arguments.isNotEmpty) { - if (Get.arguments.containsKey('videoItem')) { - preRender = true; - var args = Get.arguments['videoItem']; + Map argMap = Get.arguments; + var keys = argMap.keys.toList(); + if (keys.isNotEmpty) { + if (keys.contains('videoItem')) { + var args = argMap['videoItem']; if (args.pic != null && args.pic != '') { videoItem['pic'] = args.pic; - bgCover.value = args.pic; } } - if (Get.arguments.containsKey('pic')) { - videoItem['pic'] = Get.arguments['pic']; - bgCover.value = Get.arguments['pic']; + if (keys.contains('pic')) { + videoItem['pic'] = argMap['pic']; } - heroTag = Get.arguments['heroTag']; - videoType = Get.arguments['videoType'] ?? SearchType.video; } tabCtr = TabController(length: 2, vsync: this); - // queryVideoUrl(); + autoPlay.value = + setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true); + enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true); } showReplyReplyPanel() { @@ -120,8 +122,15 @@ class VideoDetailController extends GetxController /// 暂不匹配解码规则 /// 根据currentVideoQa 重新设置videoUrl - firstVideo = - data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code); + // firstVideo = + // data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code); + // videoUrl = firstVideo.baseUrl!; + + /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl + List videoList = + data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList(); + firstVideo = videoList + .firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code)); videoUrl = firstVideo.baseUrl!; /// 根据currentAudioQa 重新设置audioUrl @@ -133,6 +142,7 @@ class VideoDetailController extends GetxController } Future playerInit({video, audio, seekToTime, duration}) async { + print('data.timeLength:${data.timeLength}'); await plPlayerController.setDataSource( DataSource( videoSource: video ?? videoUrl, @@ -145,7 +155,7 @@ class VideoDetailController extends GetxController }, ), // 硬解 - enableHA: true, + enableHA: enableHA.value, autoplay: autoPlay.value, seekTo: seekToTime ?? defaultST, duration: duration ?? Duration(milliseconds: data.timeLength ?? 0), @@ -170,14 +180,73 @@ class VideoDetailController extends GetxController data = result['data']; /// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量 - firstVideo = data.dash!.video!.first; - videoUrl = firstVideo.baseUrl!; - // - currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!; + // firstVideo = data.dash!.video!.first; + // videoUrl = firstVideo.baseUrl!; + // // + // currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!; + + // /// 优先顺序 设置中指定质量 -> 当前可选的最高质量 + // AudioItem firstAudio = + // data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem(); + // audioUrl = firstAudio.baseUrl ?? ''; + + List allVideosList = data.dash!.video!; + + try { + // 当前可播放的最高质量视频 + int currentHighVideoQa = allVideosList.first.quality!.code; + // + int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa, + defaultValue: currentHighVideoQa); + int resVideoQa = currentHighVideoQa; + if (cacheVideoQa <= currentHighVideoQa) { + List numbers = data.acceptQuality! + .where((e) => e <= currentHighVideoQa) + .toList(); + resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers); + } + currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!; + + /// 取出符合当前画质的videoList + List videosList = + allVideosList.where((e) => e.quality!.code == resVideoQa).toList(); + + /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 + List supportFormats = data.supportFormats!; + // 根据画质选编码格式 + List supportDecodeFormats = + supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!; + + try { + currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get( + SettingBoxKey.defaultDecode, + defaultValue: supportDecodeFormats.first))!; + } catch (_) {} + + /// 取出符合当前解码格式的videoItem + firstVideo = videosList + .firstWhere((e) => e.codecs!.startsWith(currentDecodeFormats.code)); + videoUrl = firstVideo.baseUrl!; + } catch (_) {} /// 优先顺序 设置中指定质量 -> 当前可选的最高质量 - AudioItem firstAudio = - data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem(); + late AudioItem firstAudio; + List audiosList = data.dash!.audio!; + try { + if (audiosList.isNotEmpty) { + firstAudio = audiosList.first; + int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa, + defaultValue: firstAudio.id); + // 选择最接近的那个音轨 + firstAudio = audiosList.firstWhere( + (e) => e.id == resultAudioQa, + orElse: () => AudioItem(), + ); + } else { + firstAudio = AudioItem(); + } + } catch (_) {} + audioUrl = firstAudio.baseUrl ?? ''; // if (firstAudio.id != null) { @@ -185,6 +254,13 @@ class VideoDetailController extends GetxController } defaultST = Duration(milliseconds: data.lastPlayTime!); await playerInit(); + + // await playerInit( + // firstVideo, + // audioUrl, + // defaultST: Duration(milliseconds: data.lastPlayTime!), + // duration: data.timeLength ?? 0, + // ); } else { SmartDialog.showToast(result['msg'].toString()); } diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 9a4039c7..55b63445 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -51,6 +51,8 @@ class VideoIntroController extends GetxController { RxMap followStatus = {}.obs; int _tempThemeValue = -1; + RxInt lastPlayCid = 0.obs; + @override void onInit() { super.onInit(); @@ -76,6 +78,7 @@ class VideoIntroController extends GetxController { } } userLogin = user.get(UserBoxKey.userLogin) != null; + lastPlayCid.value = int.parse(Get.parameters['cid']!); } // 获取视频简介&分p @@ -83,6 +86,9 @@ class VideoIntroController extends GetxController { var result = await VideoHttp.videoIntro(bvid: bvid); if (result['status']) { videoDetail.value = result['data']!; + if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) { + lastPlayCid.value = videoDetail.value.pages!.first.cid!; + } Get.find(tag: Get.arguments['heroTag']) .tabs .value = ['简介', '评论 ${result['data']!.stat!.reply}']; diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index b34d1109..04f8cbc0 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -20,6 +19,7 @@ import 'widgets/action_item.dart'; import 'widgets/action_row_item.dart'; import 'widgets/fav_panel.dart'; import 'widgets/intro_detail.dart'; +import 'widgets/page.dart'; import 'widgets/season.dart'; class VideoIntroPanel extends StatefulWidget { @@ -62,7 +62,6 @@ class _VideoIntroPanelState extends State if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data['status']) { // 请求成功 - // return _buildView(context, false, videoDetail); return Obx( () => VideoInfo( loadingStatus: false, @@ -95,22 +94,35 @@ class VideoInfo extends StatefulWidget { } class _VideoInfoState extends State with TickerProviderStateMixin { - Map videoItem = Get.put(VideoIntroController()).videoItem!; - final VideoIntroController videoIntroController = - Get.put(VideoIntroController(), tag: Get.arguments['heroTag']); - bool isExpand = false; + final String heroTag = Get.arguments['heroTag']; + late final VideoIntroController videoIntroController; + late final VideoDetailController videoDetailCtr; + late final Map videoItem; - late VideoDetailController? videoDetailCtr; Box localCache = GStrorage.localCache; late double sheetHeight; + late final bool loadingStatus; // 加载状态 + + late final dynamic owner; + late final dynamic follower; + late final dynamic followStatus; + @override void initState() { super.initState(); - videoDetailCtr = - Get.find(tag: Get.arguments['heroTag']); + videoIntroController = Get.put(VideoIntroController(), tag: heroTag); + videoDetailCtr = Get.find(tag: heroTag); + videoItem = videoIntroController.videoItem!; sheetHeight = localCache.get('sheetHeight'); + + loadingStatus = widget.loadingStatus; + owner = loadingStatus ? videoItem['owner'] : widget.videoDetail!.owner; + follower = loadingStatus + ? '-' + : Utils.numFormat(videoIntroController.userStat['follower']); + followStatus = videoIntroController.followStatus; } // 收藏 @@ -141,24 +153,39 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ); } + // 用户主页 + onPushMember() { + feedBack(); + int mid = !loadingStatus + ? widget.videoDetail!.owner!.mid + : videoItem['owner'].mid; + String face = !loadingStatus + ? widget.videoDetail!.owner!.face + : videoItem['owner'].face; + Get.toNamed('/member?mid=$mid', + arguments: {'face': face, 'heroTag': (mid + 99).toString()}); + } + @override Widget build(BuildContext context) { ThemeData t = Theme.of(context); + Color outline = t.colorScheme.outline; return SliverPadding( padding: const EdgeInsets.only( left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10), sliver: SliverToBoxAdapter( - child: !widget.loadingStatus || videoItem.isNotEmpty + child: !loadingStatus || videoItem.isNotEmpty ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - InkWell( + GestureDetector( + behavior: HitTestBehavior.translucent, onTap: () => showIntroDetail(), child: Row( children: [ Expanded( child: Text( - !widget.loadingStatus + !loadingStatus ? widget.videoDetail!.title : videoItem['title'], style: const TextStyle( @@ -182,14 +209,18 @@ class _VideoInfoState extends State with TickerProviderStateMixin { return t.highlightColor.withOpacity(0.2); }), ), - onPressed: () => showIntroDetail(), - icon: const Icon(Icons.more_horiz), + onPressed: showIntroDetail, + icon: Icon( + Icons.more_horiz, + color: Theme.of(context).colorScheme.primary, + ), ), ), ], ), ), - InkWell( + GestureDetector( + behavior: HitTestBehavior.translucent, onTap: () => showIntroDetail(), child: Row( children: [ @@ -237,7 +268,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 点赞收藏转发 布局样式2 // actionGrid(context, videoIntroController), // 合集 - if (!widget.loadingStatus && + if (!loadingStatus && widget.videoDetail!.ugcSeason != null) ...[ SeasonPanel( ugcSeason: widget.videoDetail!.ugcSeason!, @@ -247,97 +278,86 @@ class _VideoInfoState extends State with TickerProviderStateMixin { .changeSeasonOrbangu(bvid, cid, aid), ) ], + if (!loadingStatus && + widget.videoDetail!.pages != null && + widget.videoDetail!.pages!.length > 1) ...[ + Obx(() => PagesPanel( + pages: widget.videoDetail!.pages!, + cid: videoIntroController.lastPlayCid.value, + sheetHeight: sheetHeight, + changeFuc: (cid) => + videoIntroController.changeSeasonOrbangu( + videoIntroController.bvid, cid, null), + )) + ], GestureDetector( - onTap: () { - feedBack(); - int mid = !widget.loadingStatus - ? widget.videoDetail!.owner!.mid - : videoItem['owner'].mid; - String face = !widget.loadingStatus - ? widget.videoDetail!.owner!.face - : videoItem['owner'].face; - Get.toNamed('/member?mid=$mid', arguments: { - 'face': face, - 'heroTag': (mid + 99).toString() - }); - }, - child: Padding( - padding: const EdgeInsets.only( - top: 12, bottom: 12, left: 4, right: 4), + onTap: onPushMember, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 4), child: Row( children: [ NetworkImgLayer( type: 'avatar', - src: !widget.loadingStatus - ? widget.videoDetail!.owner!.face - : videoItem['owner'].face, + src: loadingStatus + ? owner.face + : widget.videoDetail!.owner!.face, width: 34, height: 34, fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, ), const SizedBox(width: 10), - Text( - !widget.loadingStatus - ? widget.videoDetail!.owner!.name - : videoItem['owner'].name, - style: const TextStyle(fontSize: 13), - ), + Text(owner.name, + style: const TextStyle(fontSize: 13)), const SizedBox(width: 6), Text( - widget.loadingStatus - ? '-' - : Utils.numFormat( - videoIntroController.userStat['follower']), + follower, style: TextStyle( - fontSize: t.textTheme.labelSmall!.fontSize, - color: t.colorScheme.outline), + fontSize: t.textTheme.labelSmall!.fontSize, + color: outline, + ), ), const Spacer(), AnimatedOpacity( - opacity: widget.loadingStatus ? 0 : 1, + opacity: loadingStatus ? 0 : 1, duration: const Duration(milliseconds: 150), child: SizedBox( height: 32, child: Obx( - () => videoIntroController - .followStatus.isNotEmpty - ? TextButton( - onPressed: () => videoIntroController - .actionRelationMod(), - style: TextButton.styleFrom( - padding: const EdgeInsets.only( - left: 8, right: 8), - foregroundColor: - videoIntroController.followStatus[ - 'attribute'] != - 0 - ? t.colorScheme.outline - : t.colorScheme.onPrimary, - backgroundColor: videoIntroController - .followStatus[ - 'attribute'] != - 0 - ? t.colorScheme.onInverseSurface - : t.colorScheme - .primary, // 设置按钮背景色 - ), - child: Text( - videoIntroController.followStatus[ - 'attribute'] != - 0 - ? '已关注' - : '关注', - style: TextStyle( - fontSize: t.textTheme.labelMedium! - .fontSize), - ), - ) - : ElevatedButton( - onPressed: () => videoIntroController - .actionRelationMod(), - child: const Text('关注'), - ), + () => + videoIntroController.followStatus.isNotEmpty + ? TextButton( + onPressed: videoIntroController + .actionRelationMod, + style: TextButton.styleFrom( + padding: const EdgeInsets.only( + left: 8, right: 8), + foregroundColor: + followStatus['attribute'] != 0 + ? outline + : t.colorScheme.onPrimary, + backgroundColor: + followStatus['attribute'] != 0 + ? t.colorScheme + .onInverseSurface + : t.colorScheme + .primary, // 设置按钮背景色 + ), + child: Text( + followStatus['attribute'] != 0 + ? '已关注' + : '关注', + style: TextStyle( + fontSize: t.textTheme + .labelMedium!.fontSize), + ), + ) + : ElevatedButton( + onPressed: videoIntroController + .actionRelationMod, + child: const Text('关注'), + ), ), ), ), @@ -359,66 +379,64 @@ class _VideoInfoState extends State with TickerProviderStateMixin { Widget actionGrid(BuildContext context, videoIntroController) { return LayoutBuilder(builder: (context, constraints) { - return Padding( + return Container( padding: const EdgeInsets.only(top: 6, bottom: 10), - child: SizedBox( - height: constraints.maxWidth / 5 * 0.8, - child: GridView.count( - primary: false, - padding: const EdgeInsets.all(0), - crossAxisCount: 5, - childAspectRatio: 1.25, - children: [ - Obx( - () => ActionItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), - onTap: () => videoIntroController.actionLikeVideo(), - selectStatus: videoIntroController.hasLike.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.like!.toString() - : '-'), - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.clock), - onTap: () => videoIntroController.actionShareVideo(), - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: '稍后再看'), - Obx( - () => ActionItem( - icon: const Icon(FontAwesomeIcons.b), - selectIcon: const Icon(FontAwesomeIcons.b), - onTap: () => videoIntroController.actionCoinVideo(), - selectStatus: videoIntroController.hasCoin.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.coin!.toString() - : '-'), - ), - Obx( - () => ActionItem( - icon: const Icon(FontAwesomeIcons.star), - selectIcon: const Icon(FontAwesomeIcons.solidStar), - // onTap: () => videoIntroController.actionFavVideo(), - onTap: () => showFavBottomSheet(), - selectStatus: videoIntroController.hasFav.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.favorite!.toString() - : '-'), - ), - ActionItem( - icon: const Icon(FontAwesomeIcons.shareFromSquare), - onTap: () => videoIntroController.actionShareVideo(), - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.share!.toString() + height: constraints.maxWidth / 5 * 0.8, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 5, + childAspectRatio: 1.25, + children: [ + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.thumbsUp), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), + onTap: () => videoIntroController.actionLikeVideo(), + selectStatus: videoIntroController.hasLike.value, + loadingStatus: loadingStatus, + text: !loadingStatus + ? widget.videoDetail!.stat!.like!.toString() : '-'), - ], - ), + ), + ActionItem( + icon: const Icon(FontAwesomeIcons.clock), + onTap: () => videoIntroController.actionShareVideo(), + selectStatus: false, + loadingStatus: loadingStatus, + text: '稍后再看'), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), + onTap: () => videoIntroController.actionCoinVideo(), + selectStatus: videoIntroController.hasCoin.value, + loadingStatus: loadingStatus, + text: !loadingStatus + ? widget.videoDetail!.stat!.coin!.toString() + : '-'), + ), + Obx( + () => ActionItem( + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.solidStar), + // onTap: () => videoIntroController.actionFavVideo(), + onTap: () => showFavBottomSheet(), + selectStatus: videoIntroController.hasFav.value, + loadingStatus: loadingStatus, + text: !loadingStatus + ? widget.videoDetail!.stat!.favorite!.toString() + : '-'), + ), + ActionItem( + icon: const Icon(FontAwesomeIcons.shareFromSquare), + onTap: () => videoIntroController.actionShareVideo(), + selectStatus: false, + loadingStatus: loadingStatus, + text: !loadingStatus + ? widget.videoDetail!.stat!.share!.toString() + : '-'), + ], ), ); }); @@ -431,10 +449,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin { icon: const Icon(FontAwesomeIcons.thumbsUp), onTap: () => videoIntroController.actionLikeVideo(), selectStatus: videoIntroController.hasLike.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.like!.toString() - : '-', + loadingStatus: loadingStatus, + text: + !loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-', ), ), const SizedBox(width: 8), @@ -443,10 +460,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin { icon: const Icon(FontAwesomeIcons.b), onTap: () => videoIntroController.actionCoinVideo(), selectStatus: videoIntroController.hasCoin.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.coin!.toString() - : '-', + loadingStatus: loadingStatus, + text: + !loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-', ), ), const SizedBox(width: 8), @@ -455,8 +471,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { icon: const Icon(FontAwesomeIcons.heart), onTap: () => showFavBottomSheet(), selectStatus: videoIntroController.hasFav.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus + loadingStatus: loadingStatus, + text: !loadingStatus ? widget.videoDetail!.stat!.favorite!.toString() : '-', ), @@ -468,57 +484,20 @@ class _VideoInfoState extends State with TickerProviderStateMixin { videoDetailCtr.tabCtr.animateTo(1); }, selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.videoDetail!.stat!.reply!.toString() - : '-', + loadingStatus: loadingStatus, + text: + !loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-', ), const SizedBox(width: 8), ActionRowItem( icon: const Icon(FontAwesomeIcons.share), onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, - loadingStatus: widget.loadingStatus, - // text: !widget.loadingStatus + loadingStatus: loadingStatus, + // text: !loadingStatus // ? widget.videoDetail!.stat!.share!.toString() // : '-', text: '转发'), ]); } - - InlineSpan buildContent(BuildContext context, content) { - String desc = content.desc; - List descV2 = content.descV2; - // type - // 1 普通文本 - // 2 @用户 - List spanChilds = []; - if (descV2.isNotEmpty) { - for (var i = 0; i < descV2.length; i++) { - if (descV2[i].type == 1) { - spanChilds.add(TextSpan(text: descV2[i].rawText)); - } else if (descV2[i].type == 2) { - spanChilds.add( - TextSpan( - text: '@${descV2[i].rawText}', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - String heroTag = Utils.makeHeroTag(descV2[i].bizId); - Get.toNamed( - '/member?mid=${descV2[i].bizId}', - arguments: {'face': '', 'heroTag': heroTag}, - ); - }, - ), - ); - } - } - } else { - spanChilds.add(TextSpan(text: desc)); - } - return TextSpan(children: spanChilds); - } } diff --git a/lib/pages/video/detail/introduction/widgets/fav_panel.dart b/lib/pages/video/detail/introduction/widgets/fav_panel.dart index 44502b0e..6a52e683 100644 --- a/lib/pages/video/detail/introduction/widgets/fav_panel.dart +++ b/lib/pages/video/detail/introduction/widgets/fav_panel.dart @@ -33,24 +33,13 @@ class _FavPanelState extends State { child: Column( children: [ AppBar( - toolbarHeight: 50, - automaticallyImplyLeading: false, centerTitle: false, - elevation: 1, - title: Text( - '选择文件夹', - style: Theme.of(context).textTheme.titleMedium, - ), - actions: [ - TextButton( - onPressed: () async { - feedBack(); - await widget.ctr!.actionFavVideo(); - }, - child: const Text('完成'), - ), - const SizedBox(width: 6), - ], + elevation: 0, + leading: IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close_outlined)), + title: + Text('添加到收藏夹', style: Theme.of(context).textTheme.titleMedium), ), Expanded( child: Material( @@ -63,45 +52,33 @@ class _FavPanelState extends State { return Obx( () => ListView.builder( itemCount: - widget.ctr!.favFolderData.value.list!.length + 1, + widget.ctr!.favFolderData.value.list!.length, itemBuilder: (context, index) { - if (index == 0) { - return const SizedBox(height: 10); - } else { - return ListTile( - onTap: () => widget.ctr!.onChoose( - widget.ctr!.favFolderData.value - .list![index - 1].favState != - 1, - index - 1), - dense: true, - leading: - const Icon(Icons.folder_special_outlined), - minLeadingWidth: 0, - title: Text(widget.ctr!.favFolderData.value - .list![index - 1].title!), - subtitle: Text( - '${widget.ctr!.favFolderData.value.list![index - 1].mediaCount}个内容', - style: TextStyle( - color: - Theme.of(context).colorScheme.outline, - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize), + return ListTile( + onTap: () => widget.ctr!.onChoose( + widget.ctr!.favFolderData.value.list![index] + .favState != + 1, + index), + dense: true, + leading: const Icon(Icons.folder_outlined), + minLeadingWidth: 0, + title: Text(widget.ctr!.favFolderData.value + .list![index].title!), + subtitle: Text( + '${widget.ctr!.favFolderData.value.list![index].mediaCount}个内容', + ), + trailing: Transform.scale( + scale: 0.9, + child: Checkbox( + value: widget.ctr!.favFolderData.value + .list![index].favState == + 1, + onChanged: (bool? checkValue) => + widget.ctr!.onChoose(checkValue!, index), ), - trailing: Transform.scale( - scale: 0.9, - child: Checkbox( - value: widget.ctr!.favFolderData.value - .list![index - 1].favState == - 1, - onChanged: (bool? checkValue) => widget.ctr! - .onChoose(checkValue!, index - 1), - ), - ), - ); - } + ), + ); }, ), ); @@ -119,6 +96,46 @@ class _FavPanelState extends State { ), ), ), + Divider( + height: 1, + color: Theme.of(context).disabledColor.withOpacity(0.08), + ), + Padding( + padding: EdgeInsets.only( + left: 20, + right: 20, + top: 12, + bottom: MediaQuery.of(context).padding.bottom), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 30, right: 30), + backgroundColor: Theme.of(context) + .colorScheme + .onInverseSurface, // 设置按钮背景色 + ), + child: const Text('取消'), + ), + const SizedBox(width: 10), + TextButton( + onPressed: () async { + feedBack(); + await widget.ctr!.actionFavVideo(); + }, + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 30, right: 30), + foregroundColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: + Theme.of(context).colorScheme.primary, // 设置按钮背景色 + ), + child: const Text('完成'), + ), + ], + ), + ), ], ), ); diff --git a/lib/pages/video/detail/introduction/widgets/intro_detail.dart b/lib/pages/video/detail/introduction/widgets/intro_detail.dart index ab07e456..99a40793 100644 --- a/lib/pages/video/detail/introduction/widgets/intro_detail.dart +++ b/lib/pages/video/detail/introduction/widgets/intro_detail.dart @@ -27,19 +27,20 @@ class IntroDetail extends StatelessWidget { height: sheetHeight, child: Column( children: [ - Container( - height: 35, - padding: const EdgeInsets.only(bottom: 2), - child: Center( - child: Container( - width: 32, - height: 3, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer - .withOpacity(0.5), - borderRadius: const BorderRadius.all(Radius.circular(3))), + InkWell( + onTap: () => Get.back(), + child: Container( + height: 35, + padding: const EdgeInsets.only(bottom: 2), + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: + const BorderRadius.all(Radius.circular(3))), + ), ), ), ), @@ -125,33 +126,29 @@ class IntroDetail extends StatelessWidget { // type // 1 普通文本 // 2 @用户 - List spanChilds = []; - if (descV2.isNotEmpty) { - for (var i = 0; i < descV2.length; i++) { - if (descV2[i].type == 1) { - spanChilds.add(TextSpan(text: descV2[i].rawText)); - } else if (descV2[i].type == 2) { - spanChilds.add( - TextSpan( - text: '@${descV2[i].rawText}', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - String heroTag = Utils.makeHeroTag(descV2[i].bizId); - Get.toNamed( - '/member?mid=${descV2[i].bizId}', - arguments: {'face': '', 'heroTag': heroTag}, - ); - }, - ), + List spanChilds = List.generate(descV2.length, (index) { + final currentDesc = descV2[index]; + switch (currentDesc.type) { + case 1: + return TextSpan(text: currentDesc.rawText); + case 2: + final colorSchemePrimary = Theme.of(context).colorScheme.primary; + final heroTag = Utils.makeHeroTag(currentDesc.bizId); + return TextSpan( + text: '@${currentDesc.rawText}', + style: TextStyle(color: colorSchemePrimary), + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.toNamed( + '/member?mid=${currentDesc.bizId}', + arguments: {'face': '', 'heroTag': heroTag}, + ); + }, ); - } + default: + return TextSpan(); } - } else { - spanChilds.add(TextSpan(text: desc)); - } + }); return TextSpan(children: spanChilds); } } diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart new file mode 100644 index 00000000..261b6227 --- /dev/null +++ b/lib/pages/video/detail/introduction/widgets/page.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/models/video_detail_res.dart'; + +class PagesPanel extends StatefulWidget { + final List pages; + final int? cid; + final double? sheetHeight; + final Function? changeFuc; + + const PagesPanel({ + super.key, + required this.pages, + this.cid, + this.sheetHeight, + this.changeFuc, + }); + + @override + State createState() => _PagesPanelState(); +} + +class _PagesPanelState extends State { + late List episodes; + late int currentIndex; + + @override + void initState() { + super.initState(); + episodes = widget.pages; + currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); + } + + void changeFucCall(item, i) async { + await widget.changeFuc!( + item.cid, + ); + currentIndex = i; + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('视频选集 '), + Expanded( + child: Text( + ' 正在播放:${widget.pages[currentIndex].pagePart}', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + const SizedBox(width: 10), + SizedBox( + height: 34, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + showBottomSheet( + context: context, + builder: (_) => Container( + height: widget.sheetHeight, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + Container( + height: 45, + padding: + const EdgeInsets.only(left: 14, right: 14), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + '合集(${episodes.length})', + style: + Theme.of(context).textTheme.titleMedium, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + Divider( + height: 1, + color: Theme.of(context) + .dividerColor + .withOpacity(0.1), + ), + Expanded( + child: Material( + child: ListView.builder( + itemCount: episodes.length, + itemBuilder: (context, index) { + return InkWell( + onTap: () { + changeFucCall(episodes[index], index); + Get.back(); + }, + child: Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 15, + right: 15), + child: Text( + episodes[index].pagePart!, + style: TextStyle( + color: index == currentIndex + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .onSurface), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ); + }, + child: Text( + '共${widget.pages.length}集', + style: const TextStyle(fontSize: 13), + ), + ), + ), + ], + ), + ), + Container( + height: 35, + margin: const EdgeInsets.only(bottom: 8), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: widget.pages.length, + itemExtent: 150, + itemBuilder: ((context, i) { + return Container( + width: 150, + margin: const EdgeInsets.only(right: 10), + child: Material( + color: Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(6), + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () => changeFucCall(widget.pages[i], i), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 8), + child: Row( + children: [ + if (i == currentIndex) ...[ + Image.asset( + 'assets/images/live.gif', + color: Theme.of(context).colorScheme.primary, + height: 12, + ), + const SizedBox(width: 6) + ], + Expanded( + child: Text( + widget.pages[i].pagePart!, + maxLines: 1, + style: TextStyle( + fontSize: 13, + color: i == currentIndex + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface), + overflow: TextOverflow.ellipsis, + )) + ], + ), + ), + ), + ), + ); + }), + ), + ) + ], + ); + } +} diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 0f2e35b7..ba71b9da 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -391,6 +391,7 @@ class ReplyItemRow extends StatelessWidget { ), if (replies![i].isUp) const WidgetSpan( + alignment: PlaceholderAlignment.top, child: UpTag(), ), buildContent( diff --git a/lib/pages/video/detail/replyNew/view.dart b/lib/pages/video/detail/replyNew/view.dart index f86683a0..6f538e4e 100644 --- a/lib/pages/video/detail/replyNew/view.dart +++ b/lib/pages/video/detail/replyNew/view.dart @@ -118,7 +118,7 @@ class _VideoReplyNewDialogState extends State @override Widget build(BuildContext context) { return Container( - height: 400, + height: 500, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: const BorderRadius.only( diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 26c5de99..c4d06c21 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/sliver_header.dart'; +import 'package:pilipala/http/user.dart'; import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/pages/bangumi/introduction/index.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; @@ -163,8 +165,11 @@ class _VideoDetailPageState extends State scrolledUnderElevation: 0, forceElevated: innerBoxIsScrolled, expandedHeight: videoHeight, - // backgroundColor: Colors.transparent, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: + MediaQuery.of(Get.context!).platformBrightness == + Brightness.dark + ? Colors.black + : Theme.of(context).colorScheme.background, flexibleSpace: FlexibleSpaceBar( background: Padding( padding: EdgeInsets.only(top: statusBarHeight), @@ -233,10 +238,17 @@ class _VideoDetailPageState extends State backgroundColor: Colors.transparent, actions: [ - /// TODO IconButton( tooltip: '稍后再看', - onPressed: () {}, + onPressed: () async { + var res = await UserHttp + .toViewLater( + bvid: + videoDetailController + .bvid); + SmartDialog.showToast( + res['msg']); + }, icon: const Icon(Icons .history_outlined)) ], @@ -291,39 +303,20 @@ class _VideoDetailPageState extends State children: [ Opacity( opacity: 0, - child: Container( + child: SizedBox( width: double.infinity, height: 0, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context) - .dividerColor - .withOpacity(0.1), - ), + child: Obx( + () => TabBar( + controller: videoDetailController.tabCtr, + dividerColor: Colors.transparent, + indicatorColor: + Theme.of(context).colorScheme.background, + tabs: videoDetailController.tabs + .map((String name) => Tab(text: name)) + .toList(), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - width: 280, - margin: const EdgeInsets.only(left: 20), - child: Obx( - () => TabBar( - controller: videoDetailController.tabCtr, - dividerColor: Colors.transparent, - indicatorColor: - Theme.of(context).colorScheme.background, - tabs: videoDetailController.tabs - .map((String name) => Tab(text: name)) - .toList(), - ), - ), - ), - ], - ), ), ), Expanded( diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index bedcd008..9a429d11 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -28,6 +28,7 @@ class _HeaderControlState extends State { late PlayUrlModel videoInfo; List playSpeed = PlaySpeed.values; TextStyle subTitleStyle = const TextStyle(fontSize: 12); + TextStyle titleStyle = const TextStyle(fontSize: 14); Size get preferredSize => const Size(double.infinity, kToolbarHeight); @override @@ -81,7 +82,7 @@ class _HeaderControlState extends State { enabled: false, leading: const Icon(Icons.network_cell_outlined, size: 20), - title: const Text('省流模式'), + title: Text('省流模式', style: titleStyle), subtitle: Text('低画质 | 减少视频缓存', style: subTitleStyle), trailing: Transform.scale( scale: 0.75, @@ -99,22 +100,22 @@ class _HeaderControlState extends State { ), ), ), - Obx( - () => ListTile( - onTap: () => {Get.back(), showSetSpeedSheet()}, - dense: true, - leading: const Icon(Icons.speed_outlined, size: 20), - title: const Text('播放速度'), - subtitle: Text( - '当前倍速 x${widget.controller!.playbackSpeed}', - style: subTitleStyle), - ), - ), + // Obx( + // () => ListTile( + // onTap: () => {Get.back(), showSetSpeedSheet()}, + // dense: true, + // leading: const Icon(Icons.speed_outlined, size: 20), + // title: Text('播放速度', style: titleStyle), + // subtitle: Text( + // '当前倍速 x${widget.controller!.playbackSpeed}', + // style: subTitleStyle), + // ), + // ), ListTile( onTap: () => {Get.back(), showSetVideoQa()}, dense: true, leading: const Icon(Icons.play_circle_outline, size: 20), - title: const Text('选择画质'), + title: Text('选择画质', style: titleStyle), subtitle: Text( '当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}', style: subTitleStyle), @@ -123,24 +124,33 @@ class _HeaderControlState extends State { onTap: () => {Get.back(), showSetAudioQa()}, dense: true, leading: const Icon(Icons.album_outlined, size: 20), - title: const Text('选择音质'), + title: Text('选择音质', style: titleStyle), subtitle: Text( '当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}', style: subTitleStyle), ), ListTile( - onTap: () {}, + onTap: () => {Get.back(), showSetDecodeFormats()}, dense: true, - enabled: false, - leading: const Icon(Icons.play_circle_outline, size: 20), - title: const Text('播放设置'), + leading: const Icon(Icons.av_timer_outlined, size: 20), + title: Text('解码格式', style: titleStyle), + subtitle: Text( + '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', + style: subTitleStyle), ), + // ListTile( + // onTap: () {}, + // dense: true, + // enabled: false, + // leading: const Icon(Icons.play_circle_outline, size: 20), + // title: Text('播放设置', style: titleStyle), + // ), ListTile( onTap: () {}, dense: true, enabled: false, leading: const Icon(Icons.subtitles_outlined, size: 20), - title: const Text('弹幕设置'), + title: Text('弹幕设置', style: titleStyle), ), ], ), @@ -250,7 +260,7 @@ class _HeaderControlState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('选择画质'), + Text('选择画质', style: titleStyle), const SizedBox(width: 4), Icon( Icons.info_outline, @@ -329,7 +339,9 @@ class _HeaderControlState extends State { margin: const EdgeInsets.all(12), child: Column( children: [ - const SizedBox(height: 45, child: Center(child: Text('选择音质'))), + SizedBox( + height: 45, + child: Center(child: Text('选择音质', style: titleStyle))), Expanded( child: Material( child: ListView( @@ -370,6 +382,74 @@ class _HeaderControlState extends State { ); } + // 选择解码格式 + void showSetDecodeFormats() { + // 当前选中的解码格式 + VideoDecodeFormats currentDecodeFormats = + widget.videoDetailCtr!.currentDecodeFormats; + // 当前视频可用的解码格式 + List videoFormat = videoInfo.supportFormats!; + List list = videoFormat.first.codecs!; + + showModalBottomSheet( + context: context, + elevation: 0, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Container( + width: double.infinity, + height: 250, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(12), + child: Column( + children: [ + SizedBox( + height: 45, + child: Center(child: Text('选择解码格式', style: titleStyle))), + Expanded( + child: Material( + child: ListView( + children: [ + for (var i in list) ...[ + ListTile( + onTap: () { + widget.videoDetailCtr!.currentDecodeFormats = + VideoDecodeFormatsCode.fromString(i)!; + widget.videoDetailCtr!.updatePlayer(); + Get.back(); + }, + dense: true, + contentPadding: + const EdgeInsets.only(left: 20, right: 20), + title: Text(VideoDecodeFormatsCode.fromString(i)! + .description!), + subtitle: Text( + i!, + style: subTitleStyle, + ), + trailing: i.startsWith(currentDecodeFormats.code) + ? Icon( + Icons.done, + color: Theme.of(context).colorScheme.primary, + ) + : const SizedBox(), + ), + ] + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final _ = widget.controller!; diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index 6411ce55..a0f6a44a 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -7,6 +7,7 @@ import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/init.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/mine/index.dart'; import 'package:pilipala/pages/rcmd/controller.dart'; import 'package:pilipala/utils/cookie.dart'; @@ -71,17 +72,24 @@ class WebviewController extends GetxController { print('网页登录: $result'); if (result['status'] && result['data'].isLogin) { SmartDialog.showToast('登录成功'); - Box user = GStrorage.user; - user.put(UserBoxKey.userLogin, true); - user.put(UserBoxKey.userName, result['data'].uname); - user.put(UserBoxKey.userFace, result['data'].face); - user.put(UserBoxKey.userMid, result['data'].mid); - Box userInfoCache = GStrorage.userInfo; - userInfoCache.put('userInfoCache', result['data']); - Get.find().userInfo.value = result['data']; - Get.find().onInit(); - Get.find().queryRcmdFeed('onRefresh'); - Get.find().queryFollowDynamic(); + try { + Box user = GStrorage.user; + user.put(UserBoxKey.userLogin, true); + user.put(UserBoxKey.userName, result['data'].uname); + user.put(UserBoxKey.userFace, result['data'].face); + user.put(UserBoxKey.userMid, result['data'].mid); + + Box userInfoCache = GStrorage.userInfo; + userInfoCache.put('userInfoCache', result['data']); + + Get.find().userInfo.value = result['data']; + Get.find().onInit(); + Get.find().queryRcmdFeed('onRefresh'); + Get.find().queryFollowDynamic(); + + HomeController homeCtr = Get.find(); + homeCtr.updateLoginStatus(true); + } catch (_) {} Get.back(); } } catch (e) { diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index d6de8ae4..14be2aae 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -14,6 +14,8 @@ import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/searchResult/index.dart'; +import 'package:pilipala/pages/setting/play_setting.dart'; +import 'package:pilipala/pages/setting/style_setting.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/webview/index.dart'; @@ -68,5 +70,10 @@ class Routes { GetPage(name: '/member', page: () => const MemberPage()), // 二级回复 GetPage(name: '/replyReply', page: () => const VideoReplyReplyPanel()), + + // 播放设置 + GetPage(name: '/playSetting', page: () => const PlaySetting()), + + GetPage(name: '/styleSetting', page: () => const StyleSetting()), ]; } diff --git a/lib/utils/cookie.dart b/lib/utils/cookie.dart index 8d5c891b..09fb1bcf 100644 --- a/lib/utils/cookie.dart +++ b/lib/utils/cookie.dart @@ -21,6 +21,8 @@ class SetCookie { await cookieJar.saveFromResponse(Uri.parse(url), jarCookies); // 重新设置 cookie Request.setCookie(); + Request.cookieManager.cookieJar + .saveFromResponse(Uri.parse(url), jarCookies); return true; } } diff --git a/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index 227e58a7..8e2e6d70 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -46,4 +46,28 @@ class IdUtils { } return (r - ADD) ^ XOR; } + + // 匹配 + static Map matchAvorBv({String? input}) { + Map result = {}; + if (input == null || input == '') { + return result; + } + RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false); + RegExp avRegex = RegExp(r'AV\d+', caseSensitive: false); + + Iterable bvMatches = bvRegex.allMatches(input); + Iterable avMatches = avRegex.allMatches(input); + + List bvs = bvMatches.map((match) => match.group(0)!).toList(); + List avs = avMatches.map((match) => match.group(0)!).toList(); + + if (bvs.isNotEmpty) { + result['BV'] = bvs[0].substring(0, 2).toUpperCase() + bvs[0].substring(2); + } + if (avs.isNotEmpty) { + result['AV'] = avs[0].substring(2); + } + return result; + } } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index b39dcbfc..f1a25f49 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -68,6 +68,17 @@ class UserBoxKey { class SettingBoxKey { static const String themeMode = 'themeMode'; static const String feedBackEnable = 'feedBackEnable'; + static const String defaultFontSize = 'fontSize'; + static const String defaultVideoQa = 'defaultVideoQa'; + static const String defaultAudioQa = 'defaultAudioQa'; + static const String defaultDecode = 'defaultDecode'; + static const String defaultVideoSpeed = 'defaultVideoSpeed'; + static const String autoUpgradeEnable = 'autoUpgradeEnable'; + static const String autoPlayEnable = 'autoPlayEnable'; + static const String enableHA = 'enableHA'; + static const String defaultPicQa = 'defaultPicQa'; + + static const String danmakuEnable = 'danmakuEnable'; } class LocalCacheKey { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index fdf5009e..7dfe972e 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -162,4 +162,20 @@ class Utils { } return 0; } + + static int findClosestNumber(int target, List numbers) { + int minDiff = 127; + late int closestNumber; + try { + for (int number in numbers) { + int diff = (number - target).abs(); + + if (diff < minDiff) { + minDiff = diff; + closestNumber = number; + } + } + } catch (_) {} + return closestNumber; + } } diff --git a/pubspec.lock b/pubspec.lock index e60d3c50..1b1a5f1c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -414,6 +422,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0046e890..a739df24 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -119,6 +119,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 + flutter_launcher_icons: ^0.13.1 # flutter_launcher_icons: # git: # url: https://github.com/nvi9/flutter_launcher_icons.git @@ -130,12 +131,12 @@ flutter_icons: android: true ios: true remove_alpha_ios: false - image_path: assets/images/logo/logo_android.png - image_path_android: assets/images/logo/logo_android.png + image_path: assets/images/logo/logo_android_2.png + image_path_android: assets/images/logo/logo_android_2.png image_path_ios: assets/images/logo/logo_ios.png adaptive_icon_background: "#ffffff" - adaptive_icon_foreground: assets/images/logo/logo_android.png - adaptive_icon_monochrome: assets/images/logo/logo_android.png + adaptive_icon_foreground: assets/images/logo/logo_android_2.png + adaptive_icon_monochrome: assets/images/logo/logo_android_2.png # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -170,9 +171,9 @@ flutter: - family: Jura-Bold fonts: - asset: assets/fonts/Jura-Bold.ttf - - family: HarmonyOS - fonts: - - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf + # - family: HarmonyOS + # fonts: + # - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf # For details regarding fonts from package dependencies,