diff --git a/android/app/build.gradle b/android/app/build.gradle index 4c258354..5b25c505 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,10 +47,11 @@ android { applicationId "com.example.pilipala" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + // minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + minSdkVersion 19 } buildTypes { diff --git a/android/app/src/main/res/drawable-hdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-hdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 00000000..df13b128 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi-v26/ic_launcher_monochrome.png differ 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 new file mode 100644 index 00000000..df13b128 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-mdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 00000000..8cdf4851 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi-v26/ic_launcher_monochrome.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 new file mode 100644 index 00000000..8cdf4851 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xhdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 00000000..1bdb8bb3 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi-v26/ic_launcher_monochrome.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 new file mode 100644 index 00000000..1bdb8bb3 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xxhdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 00000000..140baca6 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi-v26/ic_launcher_monochrome.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 new file mode 100644 index 00000000..140baca6 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi-v26/ic_launcher_monochrome.png b/android/app/src/main/res/drawable-xxxhdpi-v26/ic_launcher_monochrome.png new file mode 100644 index 00000000..b1b58395 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi-v26/ic_launcher_monochrome.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 new file mode 100644 index 00000000..b1b58395 Binary files /dev/null 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 new file mode 100644 index 00000000..f606c4d8 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + 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 db77bb4b..1cb4b0c4 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 17987b79..9eaf9392 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 09d43914..6bf71b8f 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 d5f1c8d3..9c4d6c7a 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 4d6372ee..59f64433 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/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..ab983282 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #ffffff + \ No newline at end of file diff --git a/assets/fonts/ArchivoNarrow-BoldItalic.ttf b/assets/fonts/ArchivoNarrow-BoldItalic.ttf new file mode 100644 index 00000000..e69fb6a5 Binary files /dev/null and b/assets/fonts/ArchivoNarrow-BoldItalic.ttf differ diff --git a/assets/fonts/fansCard.ttf b/assets/fonts/fansCard.ttf new file mode 100644 index 00000000..09ade3a0 Binary files /dev/null and b/assets/fonts/fansCard.ttf differ diff --git a/assets/images/logo/logo_android.png b/assets/images/logo/logo_android.png new file mode 100644 index 00000000..5e220fb4 Binary files /dev/null and b/assets/images/logo/logo_android.png differ diff --git a/assets/images/logo/logo_big.png b/assets/images/logo/logo_big.png new file mode 100644 index 00000000..62370832 Binary files /dev/null and b/assets/images/logo/logo_big.png differ diff --git a/assets/images/logo/logo_ios.png b/assets/images/logo/logo_ios.png new file mode 100644 index 00000000..a9992b4a Binary files /dev/null and b/assets/images/logo/logo_ios.png differ diff --git a/assets/images/lv/lv0.png b/assets/images/lv/lv0.png new file mode 100644 index 00000000..3b9999cf Binary files /dev/null and b/assets/images/lv/lv0.png differ diff --git a/assets/images/lv/lv1.png b/assets/images/lv/lv1.png new file mode 100644 index 00000000..9973e4e7 Binary files /dev/null and b/assets/images/lv/lv1.png differ diff --git a/assets/images/lv/lv2.png b/assets/images/lv/lv2.png new file mode 100644 index 00000000..895653ec Binary files /dev/null and b/assets/images/lv/lv2.png differ diff --git a/assets/images/lv/lv3.png b/assets/images/lv/lv3.png new file mode 100644 index 00000000..54e08d2d Binary files /dev/null and b/assets/images/lv/lv3.png differ diff --git a/assets/images/lv/lv4.png b/assets/images/lv/lv4.png new file mode 100644 index 00000000..bdb5fd41 Binary files /dev/null and b/assets/images/lv/lv4.png differ diff --git a/assets/images/lv/lv5.png b/assets/images/lv/lv5.png new file mode 100644 index 00000000..6973c0a7 Binary files /dev/null and b/assets/images/lv/lv5.png differ diff --git a/assets/images/lv/lv6.png b/assets/images/lv/lv6.png new file mode 100644 index 00000000..14ba6181 Binary files /dev/null and b/assets/images/lv/lv6.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 14e569a5..c7f1a66e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,23 +2,44 @@ PODS: - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift + - device_info_plus (0.0.1): + - Flutter - Flutter (1.0.0) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) + - image_gallery_saver (1.5.0): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.0.4): + - Flutter - ReachabilitySwift (5.0.0) + - share_plus (0.0.1): + - Flutter - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) + - url_launcher_ios (0.0.1): + - Flutter + - webview_cookie_manager (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: @@ -28,21 +49,42 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_cookie_manager: + :path: ".symlinks/plugins/webview_cookie_manager/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 + webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 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 dc9ada47..7f19eb8f 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 7353c41e..4b8fadf7 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 797d452e..bf7de64d 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 6ed2d933..59305c75 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 4cd7b009..b22a8706 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 fe730945..4eb2bd8d 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 321773cd..8cfd292f 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 797d452e..bf7de64d 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 502f463a..fd0c7eab 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 0ec30343..c2d7c252 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 new file mode 100644 index 00000000..e1f6fde2 Binary files /dev/null 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 new file mode 100644 index 00000000..48ce80ad Binary files /dev/null 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 new file mode 100644 index 00000000..0b3f7b66 Binary files /dev/null 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 new file mode 100644 index 00000000..ebc40996 Binary files /dev/null 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 0ec30343..c2d7c252 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 e9f5fea2..41f9638c 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 new file mode 100644 index 00000000..5ba23fa6 Binary files /dev/null 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 new file mode 100644 index 00000000..ccfaddf5 Binary files /dev/null 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 84ac32ae..59436734 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 8953cba0..371d4763 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 0467bf12..04d36300 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/lib/common/skeleton/video_card_h.dart b/lib/common/skeleton/video_card_h.dart new file mode 100644 index 00000000..46b858d3 --- /dev/null +++ b/lib/common/skeleton/video_card_h.dart @@ -0,0 +1,119 @@ +import 'package:pilipala/common/constants.dart'; +import 'package:flutter/material.dart'; +import 'skeleton.dart'; + +class VideoCardHSkeleton extends StatefulWidget { + const VideoCardHSkeleton({super.key}); + + @override + State createState() => _VideoCardHSkeletonState(); +} + +class _VideoCardHSkeletonState extends State { + @override + Widget build(BuildContext context) { + return Skeleton( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.cardSpace, 7, StyleString.cardSpace, 7), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + borderRadius: BorderRadius.circular( + StyleString.imgRadius.x), + ), + ); + }, + ), + ), + // VideoContent(videoItem: videoItem) + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 4, 6, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 200, + height: 11, + margin: const EdgeInsets.only(bottom: 5), + ), + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 150, + height: 13, + ), + const Spacer(), + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 100, + height: 13, + margin: const EdgeInsets.only(bottom: 5), + ), + Row( + children: [ + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 40, + height: 13, + margin: const EdgeInsets.only(right: 8), + ), + Container( + color: Theme.of(context) + .colorScheme + .onInverseSurface, + width: 40, + height: 13, + ), + ], + ) + ], + ), + )), + ], + ), + ); + }, + ), + ), + Divider( + height: 1, + indent: 8, + endIndent: 12, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), + ); + } +} diff --git a/lib/common/skeleton/video_card_v.dart b/lib/common/skeleton/video_card_v.dart index 1c9ef23d..859f95aa 100644 --- a/lib/common/skeleton/video_card_v.dart +++ b/lib/common/skeleton/video_card_v.dart @@ -23,14 +23,7 @@ class VideoCardVSkeleton extends StatelessWidget { return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(6), - border: Border.all( - color: Theme.of(context) - .colorScheme - .outline - .withOpacity(0.1), - ), ), ); }, @@ -49,15 +42,15 @@ class VideoCardVSkeleton extends StatelessWidget { Container( width: 200, height: 13, + margin: const EdgeInsets.only(bottom: 5), color: Theme.of(context).colorScheme.background, ), - const SizedBox(height: 5), Container( width: 150, height: 13, + margin: const EdgeInsets.only(bottom: 12), color: Theme.of(context).colorScheme.background, ), - const SizedBox(height: 12), Container( width: 80, height: 13, diff --git a/lib/common/skeleton/video_reply.dart b/lib/common/skeleton/video_reply.dart new file mode 100644 index 00000000..adfba431 --- /dev/null +++ b/lib/common/skeleton/video_reply.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'skeleton.dart'; + +class VideoReplySkeleton extends StatelessWidget { + const VideoReplySkeleton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Color bgColor = Theme.of(context).colorScheme.onInverseSurface; + return Skeleton( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 2), + child: Row( + children: [ + ClipOval( + child: Container( + width: 34, + height: 34, + color: bgColor, + ), + ), + const SizedBox(width: 12), + Container( + width: 80, + height: 13, + color: bgColor, + ) + ], + ), + ), + Container( + width: double.infinity, + margin: + const EdgeInsets.only(top: 4, left: 57, right: 6, bottom: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 300, + height: 14, + margin: const EdgeInsets.only(bottom: 4), + color: bgColor, + ), + Container( + width: 180, + height: 14, + margin: const EdgeInsets.only(bottom: 10), + color: bgColor, + ), + Row( + children: [ + Container( + width: 40, + height: 14, + margin: const EdgeInsets.only(bottom: 4), + color: bgColor, + ), + const Spacer(), + Container( + width: 40, + height: 14, + margin: const EdgeInsets.only(bottom: 4), + color: bgColor, + ), + const SizedBox(width: 8) + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/common/widgets/appbar.dart b/lib/common/widgets/appbar.dart new file mode 100644 index 00000000..ad2b0e3b --- /dev/null +++ b/lib/common/widgets/appbar.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { + const AppBarWidget({ + required this.child, + required this.controller, + required this.visible, + Key? key, + }) : super(key: key); + + final PreferredSizeWidget child; + final AnimationController controller; + final bool visible; + + @override + // TODO: implement preferredSize + Size get preferredSize => child.preferredSize; + + @override + Widget build(BuildContext context) { + visible ? controller.reverse() : controller.forward(); + return SlideTransition( + position: Tween( + begin: Offset.zero, + end: const Offset(0, -1), + ).animate(CurvedAnimation( + parent: controller, + curve: Curves.easeInOutBack, + )), + child: child, + ); + } +} diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart new file mode 100644 index 00000000..607fffe0 --- /dev/null +++ b/lib/common/widgets/http_error.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class HttpError extends StatelessWidget { + HttpError({required this.errMsg, required this.fn, super.key}); + + String errMsg = ''; + final Function()? fn; + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: SizedBox( + height: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errMsg, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + fn!(); + }, + child: const Text('点击重试')) + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index abaa369d..a09ec850 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:pilipala/common/constants.dart'; class NetworkImgLayer extends StatelessWidget { final String? src; @@ -29,11 +30,16 @@ class NetworkImgLayer extends StatelessWidget { // double pr = 2; return src != '' ? ClipRRect( - borderRadius: BorderRadius.circular(type == 'avatar' ? 50 : 4), + borderRadius: BorderRadius.circular(type == 'avatar' + ? 50 + : type == 'emote' + ? 0 + : StyleString.imgRadius.x), child: CachedNetworkImage( imageUrl: src!, width: width ?? double.infinity, height: height ?? double.infinity, + alignment: Alignment.center, maxWidthDiskCache: ((cacheW ?? width!) * pr).toInt(), // maxHeightDiskCache: (cacheH ?? height!).toInt(), memCacheWidth: ((cacheW ?? width!) * pr).toInt(), diff --git a/lib/common/widgets/stat/danmu.dart b/lib/common/widgets/stat/danmu.dart index 44f63b21..d8deb3e2 100644 --- a/lib/common/widgets/stat/danmu.dart +++ b/lib/common/widgets/stat/danmu.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/utils/utils.dart'; @@ -11,21 +12,21 @@ class StatDanMu extends StatelessWidget { @override Widget build(BuildContext context) { + Color color = + theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline; return Row( children: [ - Image.asset( - 'assets/images/dm_$theme.png', - width: size == 'medium' ? 16 : 14, - height: size == 'medium' ? 16 : 14, + Icon( + CupertinoIcons.ellipses_bubble, + size: 14, + color: color, ), - const SizedBox(width: 2), + const SizedBox(width: 3), Text( Utils.numFormat(danmu!), style: TextStyle( fontSize: size == 'medium' ? 12 : 11, - color: theme == 'white' - ? Colors.white - : Theme.of(context).colorScheme.outline, + color: color, ), ) ], diff --git a/lib/common/widgets/stat/up.dart b/lib/common/widgets/stat/up.dart new file mode 100644 index 00000000..d217e2c4 --- /dev/null +++ b/lib/common/widgets/stat/up.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class UpTag extends StatelessWidget { + const UpTag({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 14, + height: 10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + border: Border.all(color: Theme.of(context).colorScheme.outline)), + margin: const EdgeInsets.only(right: 4), + child: Center( + child: Text( + 'UP', + style: TextStyle( + fontSize: 6, color: Theme.of(context).colorScheme.outline), + ), + ), + ); + } +} diff --git a/lib/common/widgets/stat/view.dart b/lib/common/widgets/stat/view.dart index 6f6d1960..302ceee6 100644 --- a/lib/common/widgets/stat/view.dart +++ b/lib/common/widgets/stat/view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/utils/utils.dart'; @@ -6,26 +7,26 @@ class StatView extends StatelessWidget { final int? view; final String? size; - const StatView({Key? key, this.theme, this.view, this.size}) : super(key: key); + const StatView({Key? key, this.theme, this.view, this.size}) + : super(key: key); @override Widget build(BuildContext context) { + Color color = + theme == 'white' ? Colors.white : Theme.of(context).colorScheme.outline; return Row( children: [ - Image.asset( - 'assets/images/view_$theme.png', - width: size == 'medium' ? 16 : 14, - height: size == 'medium' ? 16 : 14, + Icon( + CupertinoIcons.play_rectangle, + size: 13, + color: color, ), - const SizedBox(width: 2), + const SizedBox(width: 3), Text( Utils.numFormat(view!), - // videoItem['stat']['view'].toString(), style: TextStyle( fontSize: size == 'medium' ? 12 : 11, - color: theme == 'white' - ? Colors.white - : Theme.of(context).colorScheme.outline, + color: color, ), ), ], diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index e06dd150..81935b81 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; // 视频卡片 - 水平布局 class VideoCardH extends StatelessWidget { + // ignore: prefer_typing_uninitialized_variables var videoItem; Function()? longPress; Function()? longPressEnd; @@ -20,84 +21,94 @@ class VideoCardH extends StatelessWidget { @override Widget build(BuildContext context) { - return Material( - child: Ink( - child: GestureDetector( - onLongPress: () { - longPress!(); - }, - onLongPressEnd: (details) { - longPressEnd!(); - }, - child: InkWell( - onTap: () async { - await Future.delayed(const Duration(milliseconds: 200)); - int aid = videoItem.aid ?? videoItem.id; - Get.toNamed('/video?aid=$aid', - arguments: {'videoItem': videoItem}); - }, - child: Container( + int aid = videoItem.aid; + String heroTag = Utils.makeHeroTag(aid); + return GestureDetector( + onLongPress: () { + longPress!(); + }, + onLongPressEnd: (details) { + longPressEnd!(); + }, + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=$aid', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + Padding( padding: const EdgeInsets.fromLTRB( - StyleString.cardSpace, 5, StyleString.cardSpace, 5), - child: LayoutBuilder(builder: (context, boxConstraints) { - double width = - (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; - return SizedBox( - height: width / StyleString.aspectRatio, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - // child: ClipRRect( - // borderRadius: StyleString.mdRadius, - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - double PR = MediaQuery.of(context).devicePixelRatio; - return Stack( - children: [ - NetworkImgLayer( - // src: videoItem['pic'] + - // '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, - ), - // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), - Positioned( - right: 4, - bottom: 4, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 1, horizontal: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.black54.withOpacity(0.4)), - child: Text( - Utils.timeFormat(videoItem.duration!), - style: const TextStyle( - fontSize: 11, color: Colors.white), + StyleString.cardSpace, 7, StyleString.cardSpace, 7), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = + MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, ), ), // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), - ) - ], - ); - }, + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + color: + Colors.black54.withOpacity(0.4)), + child: Text( + Utils.timeFormat(videoItem.duration!), + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) + ], + ); + }, + ), ), - // ), - ), - VideoContent(videoItem: videoItem) - ], - ), - ); - }), - // height: 124, + VideoContent(videoItem: videoItem) + ], + ), + ); + }, + ), ), - ), + Divider( + height: 1, + indent: 8, + endIndent: 12, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], ), ), ); @@ -126,7 +137,7 @@ class VideoContent extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const Spacer(), - if (videoItem.rcmdReason != '' && + if (videoItem.rcmdReason != null && videoItem.rcmdReason.content != '') Container( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5), @@ -145,12 +156,6 @@ class VideoContent extends StatelessWidget { const SizedBox(height: 4), Row( children: [ - Image.asset( - 'assets/images/up_gray.png', - width: 14, - height: 12, - ), - const SizedBox(width: 2), Text( videoItem.owner.name, style: TextStyle( diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 3165fbec..12d5fe99 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -22,6 +22,7 @@ class VideoCardV extends StatelessWidget { @override Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(videoItem.id); return Card( elevation: 0.8, clipBehavior: Clip.hardEdge, @@ -40,7 +41,7 @@ class VideoCardV extends StatelessWidget { onTap: () async { await Future.delayed(const Duration(milliseconds: 200)); Get.toNamed('/video?aid=${videoItem.id}', - arguments: {'videoItem': videoItem}); + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); }, child: Column( children: [ @@ -57,12 +58,15 @@ class VideoCardV extends StatelessWidget { double PR = MediaQuery.of(context).devicePixelRatio; return Stack( children: [ - NetworkImgLayer( - // 指定图片尺寸 - // src: videoItem.pic + '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, + Hero( + tag: heroTag, + child: NetworkImgLayer( + // 指定图片尺寸 + // src: videoItem.pic + '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, + ), ), Positioned( left: 0, @@ -77,7 +81,7 @@ class VideoCardV extends StatelessWidget { duration: videoItem.duration, ), ), - ) + ), ], ); }), @@ -141,6 +145,25 @@ class VideoContent extends StatelessWidget { ), ), const SizedBox(width: 4) + ] else if (videoItem.isFollowed == 1) ...[ + Container( + padding: const EdgeInsets.fromLTRB(3, 1, 3, 1), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primaryContainer + .withOpacity(0.6), + borderRadius: BorderRadius.circular(3)), + child: Text( + '已关注', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + const SizedBox(width: 4) ], Expanded( child: LayoutBuilder(builder: diff --git a/lib/http/api.dart b/lib/http/api.dart index ccea17f7..b8c5c47c 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -1,9 +1,124 @@ class Api { // 推荐视频 - static const String recommendList = '/x/web-interface/index/top/feed/rcmd'; + static const String recommendList = '/x/web-interface/index/top/rcmd'; + // 热门视频 static const String hotList = '/x/web-interface/popular'; + // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 - static const String videoDetail = '/x/web-interface/view'; + // https://api.bilibili.com/x/web-interface/view/detail 获取视频超详细信息(web端) + static const String videoIntro = '/x/web-interface/view'; + // 视频详情 超详细 + // https://api.bilibili.com/x/web-interface/view/detail?aid=527403921 + + /// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/action.md + // 点赞 Post + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// like num 操作方式 必要 1:点赞 2:取消赞 + // csrf str CSRF Token(位于cookie) 必要 + // https://api.bilibili.com/x/web-interface/archive/like + static const String likeVideo = '/x/web-interface/archive/like'; + + //判断视频是否被点赞(双端)Get + // access_key str APP登录Token APP方式必要 + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // https://api.bilibili.com/x/web-interface/archive/has/like + static const String hasLikeVideo = '/x/web-interface/archive/has/like'; + + // 视频点踩 web端不支持 + + // 投币视频(web端)POST + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// multiply num 投币数量 必要 上限为2 + /// select_like num 是否附加点赞 非必要 0:不点赞 1:同时点赞 默认为0 + // csrf str CSRF Token(位于cookie) 必要 + // https://api.bilibili.com/x/web-interface/coin/add + static const String coinVideo = '/x/web-interface/coin/add'; + + // 判断视频是否被投币(双端)GET + // access_key str APP登录Token APP方式必要 + /// aid num 稿件avid 必要(可选) avid与bvid任选一个 + /// bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + /// https://api.bilibili.com/x/web-interface/archive/coins + static const String hasCoinVideo = '/x/web-interface/archive/coins'; + + // 收藏视频(双端)POST + // access_key str APP登录Token APP方式必要 + /// rid num 稿件avid 必要 + /// type num 必须为2 必要 + /// add_media_ids nums 需要加入的收藏夹mlid 非必要 同时添加多个,用,(%2C)分隔 + /// del_media_ids nums 需要取消的收藏夹mlid 非必要 同时取消多个,用,(%2C)分隔 + // csrf str CSRF Token(位于cookie) Cookie方式必要 + // https://api.bilibili.com/medialist/gateway/coll/resource/deal + // https://api.bilibili.com/x/v3/fav/resource/deal + static const String favVideo = '/x/v3/fav/resource/deal'; + + // 判断视频是否被收藏(双端)GET + /// aid + // https://api.bilibili.com/x/v2/fav/video/favoured + static const String hasFavVideo = '/x/v2/fav/video/favoured'; + + // 分享视频 (Web端) POST + // https://api.bilibili.com/x/web-interface/share/add + // aid num 稿件avid 必要(可选) avid与bvid任选一个 + // bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // csrf str CSRF Token(位于cookie) 必要 + + // 一键三连 + // https://api.bilibili.com/x/web-interface/archive/like/triple + // aid num 稿件avid 必要(可选) avid与bvid任选一个 + // bvid str 稿件bvid 必要(可选) avid与bvid任选一个 + // csrf str CSRF Token(位于cookie) 必要 + static const String oneThree = '/x/web-interface/archive/like/triple'; + + // 获取指定用户创建的所有收藏夹信息 + // 该接口也能查询目标内容id存在于那些收藏夹中 + // up_mid num 目标用户mid 必要 + // type num 目标内容属性 非必要 默认为全部 0:全部 2:视频稿件 + // rid num 目标 视频稿件avid + static const String videoInFolder = '/x/v3/fav/folder/created/list-all'; + + // 视频详情页 相关视频 + static const String relatedList = '/x/web-interface/archive/related'; + + // 评论列表 + static const String replyList = '/x/v2/reply'; + + // 楼中楼 + static const String replyReplyList = '/x/v2/reply/reply'; + + // 发表评论 + // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/comment/action.md + static const String replyAdd = '/x/v2/reply/add'; + + // 用户(被)关注数、投稿数 + // https://api.bilibili.com/x/relation/stat?vmid=697166795 + static const String userStat = '/x/relation/stat'; + + // 获取用户信息 + static const String userInfo = '/x/web-interface/nav'; + + // 获取当前用户状态 + static const String userStatOwner = '/x/web-interface/nav/stat'; + + // 收藏夹 + // https://api.bilibili.com/x/v3/fav/folder/created/list?pn=1&ps=10&up_mid=17340771 + static const String userFavFolder = '/x/v3/fav/folder/created/list'; + + /// 收藏夹 详情 + /// media_id int 收藏夹id + /// pn int 当前页 + /// ps int pageSize + /// keyword String 搜索词 + /// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿 + /// tid int 分区id + // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0 + static const String userFavFolderDetail = '/x/v3/fav/resource/list'; + + // 正在直播的up & 关注的up + // https://api.bilibili.com/x/polymer/web-dynamic/v1/portal } diff --git a/lib/http/init.dart b/lib/http/init.dart index 0ac94f85..240251dd 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -12,6 +12,7 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart'; class Request { static final Request _instance = Request._internal(); + static late CookieManager cookieManager; factory Request() => _instance; @@ -31,11 +32,9 @@ class Request { ignoreExpires: true, storage: FileStorage(cookiePath), ); - - dio.interceptors.add(CookieManager(cookieJar)); - - var cookie = await CookieManager(cookieJar) - .cookieJar + cookieManager = CookieManager(cookieJar); + dio.interceptors.add(cookieManager); + var cookie = await cookieManager.cookieJar .loadForRequest(Uri.parse(HttpString.baseUrl)); if (cookie.isEmpty) { try { @@ -46,6 +45,27 @@ class Request { } } + // 移除cookie + static removeCookie() async { + await cookieManager.cookieJar + .saveFromResponse(Uri.parse(HttpString.baseUrl), []); + await cookieManager.cookieJar + .saveFromResponse(Uri.parse(HttpString.baseApiUrl), []); + cookieManager.cookieJar.deleteAll(); + dio.interceptors.add(cookieManager); + } + + // 从cookie中获取 csrf token + static Future getCsrf() async { + var cookies = await cookieManager.cookieJar + .loadForRequest(Uri.parse(HttpString.baseApiUrl)); + // for (var i in cookies) { + // print(i); + // } + var token = cookies.firstWhere((e) => e.name == 'bili_jct').value; + return token; + } + /* * config it and create */ @@ -111,20 +131,21 @@ class Request { return response; } on DioError catch (e) { print('get error: $e'); - return Future.error(ApiInterceptor.dioError(e)); + return Future.error(await ApiInterceptor.dioError(e)); } } /* * post请求 */ - post(url, {data, options, cancelToken, extra}) async { + post(url, {data, queryParameters, options, cancelToken, extra}) async { print('post-data: $data'); Response response; try { response = await dio.post( url, data: data, + queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); @@ -132,7 +153,7 @@ class Request { return response; } on DioError catch (e) { print('post error: $e'); - return Future.error(ApiInterceptor.dioError(e)); + return Future.error(await ApiInterceptor.dioError(e)); } } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index d7140b00..8ee8c8a0 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -1,10 +1,12 @@ import 'package:dio/dio.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart' hide Response; class ApiInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - // print("请求之前"); + print("请求之前"); // 在请求之前添加头部或认证信息 // options.headers['Authorization'] = 'Bearer token'; // options.headers['Content-Type'] = 'application/json'; @@ -13,15 +15,14 @@ class ApiInterceptor extends Interceptor { @override void onResponse(Response response, ResponseInterceptorHandler handler) { - // print("响应之前"); handler.next(response); } @override - void onError(DioError err, ErrorInterceptorHandler handler) { + void onError(DioError err, ErrorInterceptorHandler handler) async { // 处理网络请求错误 - - handler.next(err); + // handler.next(err); + SmartDialog.showToast(await dioError(err)); super.onError(err, handler); } @@ -43,7 +44,7 @@ class ApiInterceptor extends Interceptor { return "发送请求超时,请检查网络设置"; case DioErrorType.unknown: var res = await checkConect(); - return "$res 网络异常,请稍后重试!"; + return res + " \n 网络异常,请稍后重试!"; default: return "Dio异常"; } diff --git a/lib/http/reply.dart b/lib/http/reply.dart new file mode 100644 index 00000000..e9a609d2 --- /dev/null +++ b/lib/http/reply.dart @@ -0,0 +1,70 @@ +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; + +class ReplyHttp { + static Future replyList({ + required String oid, + required int pageNum, + required int type, + int sort = 1, + }) async { + var res = await Request().get(Api.replyList, data: { + 'oid': oid, + 'pn': pageNum, + 'type': type, + 'sort': 1, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + Map errMap = { + -400: '请求错误', + -404: '无此项', + 12002: '评论区已关闭', + 12009: '评论主体的type不合法', + }; + return { + 'status': false, + 'date': [], + 'msg': errMap[res.data['code']] ?? '请求异常', + }; + } + } + + static Future replyReplyList({ + required String oid, + required String root, + required int pageNum, + required int type, + int sort = 1, + }) async { + var res = await Request().get(Api.replyReplyList, data: { + 'oid': oid, + 'root': root, + 'pn': pageNum, + 'type': type, + 'sort': 1, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + Map errMap = { + -400: '请求错误', + -404: '无此项', + 12002: '评论区已关闭', + 12009: '评论主体的type不合法', + }; + return { + 'status': false, + 'date': [], + 'msg': errMap[res.data['code']] ?? '请求异常', + }; + } + } +} diff --git a/lib/http/user.dart b/lib/http/user.dart new file mode 100644 index 00000000..6d4be974 --- /dev/null +++ b/lib/http/user.dart @@ -0,0 +1,79 @@ +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/user/fav_detail.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/models/user/info.dart'; +import 'package:pilipala/models/user/stat.dart'; + +class UserHttp { + static Future userStat({required int mid}) async { + var res = await Request().get(Api.userStat, data: {'vmid': mid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false}; + } + } + + static Future userInfo() async { + var res = await Request().get(Api.userInfo); + if (res.data['code'] == 0) { + UserInfoData data = UserInfoData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future userStatOwner() async { + var res = await Request().get(Api.userStatOwner); + if (res.data['code'] == 0) { + UserStat data = UserStat.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // 收藏夹 + static Future userfavFolder({ + required int pn, + required int ps, + required int mid, + }) async { + var res = await Request().get(Api.userFavFolder, data: { + 'pn': pn, + 'ps': ps, + 'up_mid': mid, + }); + if (res.data['code'] == 0) { + FavFolderData data = FavFolderData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + static Future userFavFolderDetail( + {required int mediaId, + required int pn, + required int ps, + String keyword = '', + String order = 'mtime'}) async { + var res = await Request().get(Api.userFavFolderDetail, data: { + 'media_id': mediaId, + 'pn': pn, + 'ps': ps, + 'keyword': keyword, + 'order': order, + 'type': 0, + 'tid': 0 + }); + if (res.data['code'] == 0) { + FavDetailData data = FavDetailData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } +} diff --git a/lib/http/video.dart b/lib/http/video.dart new file mode 100644 index 00000000..1e6bbec9 --- /dev/null +++ b/lib/http/video.dart @@ -0,0 +1,227 @@ +import 'dart:developer'; + +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/models/common/reply_type.dart'; +import 'package:pilipala/models/model_hot_video_item.dart'; +import 'package:pilipala/models/model_rec_video_item.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/models/video_detail_res.dart'; + +/// res.data['code'] == 0 请求正常返回结果 +/// res.data['data'] 为结果 +/// 返回{'status': bool, 'data': List} +/// view层根据 status 判断渲染逻辑 +class VideoHttp { + // 首页推荐视频 + static Future rcmdVideoList({required int ps, required int freshIdx}) async { + try { + var res = await Request().get( + Api.recommendList, + data: { + 'feed_version': 'V4', + 'ps': ps, + 'fresh_idx': freshIdx, + }, + ); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']['item']) { + list.add(RecVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': [], 'msg': ''}; + } + } catch (err) { + return {'status': false, 'data': [], 'msg': err.toString()}; + } + } + + // 最热视频 + static Future hotVideoList({required int pn, required int ps}) async { + try { + var res = await Request().get( + Api.hotList, + data: {'pn': pn, 'ps': ps}, + ); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']['list']) { + list.add(HotVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; + } + } catch (err) { + return {'status': false, 'data': [], 'msg': err}; + } + } + + // 视频信息 标题、简介 + static Future videoIntro({required String aid}) async { + var res = await Request().get(Api.videoIntro, data: {'aid': aid}); + VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); + if (result.code == 0) { + return {'status': true, 'data': result.data!}; + } else { + Map errMap = { + -400: '请求错误', + -403: '权限不足', + -404: '无视频', + 62002: '稿件不可见', + 62004: '稿件审核中', + }; + return { + 'status': false, + 'data': null, + 'msg': errMap[result.code] ?? '请求异常', + }; + } + } + + // 相关视频 + static Future relatedVideoList({required String aid}) async { + var res = await Request().get(Api.relatedList, data: {'aid': aid}); + if (res.data['code'] == 0) { + List list = []; + for (var i in res.data['data']) { + list.add(HotVideoItemModel.fromJson(i)); + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': []}; + } + } + + // 获取点赞状态 + static Future hasLikeVideo({required String aid}) async { + var res = await Request().get(Api.hasLikeVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } + + // 获取投币状态 + static Future hasCoinVideo({required String aid}) async { + var res = await Request().get(Api.hasCoinVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': true, 'data': []}; + } + } + + // 获取收藏状态 + static Future hasFavVideo({required String aid}) async { + var res = await Request().get(Api.hasFavVideo, data: {'aid': aid}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } + + // 一键三连 + static Future oneThree({required String aid}) async { + var res = await Request().post( + Api.oneThree, + queryParameters: { + 'aid': aid, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // (取消)点赞 + static Future likeVideo({required String aid, required bool type}) async { + var res = await Request().post( + Api.likeVideo, + queryParameters: { + 'aid': aid, + 'like': type ? 1 : 2, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // (取消)收藏 + static Future favVideo( + {required String aid, String? addIds, String? delIds}) async { + var res = await Request().post(Api.favVideo, queryParameters: { + 'rid': aid, + 'type': 2, + 'add_media_ids': addIds ?? '', + 'del_media_ids': delIds ?? '', + 'csrf': await Request.getCsrf(), + }); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } + + // 查看视频被收藏在哪个文件夹 + static Future videoInFolder({required int mid, required String rid}) async { + var res = await Request() + .get(Api.videoInFolder, data: {'up_mid': mid, 'rid': rid}); + if (res.data['code'] == 0) { + FavFolderData data = FavFolderData.fromJson(res.data['data']); + return {'status': true, 'data': data}; + } else { + return {'status': false, 'data': []}; + } + } + + // 发表评论 replyAdd + + // type num 评论区类型代码 必要 类型代码见表 + // oid num 目标评论区id 必要 + // root num 根评论rpid 非必要 二级评论以上使用 + // parent num 父评论rpid 非必要 二级评论同根评论id 大于二级评论为要回复的评论id + // message str 发送评论内容 必要 最大1000字符 + // plat num 发送平台标识 非必要 1:web端 2:安卓客户端 3:ios客户端 4:wp客户端 + static Future replyAdd({ + required ReplyType type, + required int oid, + required String message, + int? root, + int? parent, + }) async { + if(message == ''){ + return {'status': false, 'data': [], 'msg': '请输入评论内容'}; + } + print('root:$root'); + print('parent: $parent'); + + var res = await Request() + .post(Api.replyAdd, queryParameters: { + 'type': type.index, + 'oid': oid, + 'root': root ?? '', + 'parent': parent == null || parent == 0 ? '' : parent, + 'message': message, + 'csrf': await Request.getCsrf(), + }); + log(res.toString()); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': []}; + } + } +} diff --git a/lib/main.dart b/lib/main.dart index b3eb8796..f853f4d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,15 @@ +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:pilipala/http/init.dart'; import 'package:pilipala/router/app_pages.dart'; import 'package:pilipala/pages/main/view.dart'; +import 'package:pilipala/utils/storage.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await GStrorage.init(); await Request.setCookie(); runApp(const MyApp()); } @@ -23,14 +26,24 @@ class MyApp extends StatelessWidget { return GetMaterialApp( title: 'PiLiPaLa', theme: ThemeData( - colorScheme: lightDynamic ?? - ColorScheme.fromSeed( - seedColor: Colors.green, brightness: Brightness.light), - useMaterial3: true), - darkTheme: ThemeData(colorScheme: darkDynamic, useMaterial3: true), + colorScheme: lightDynamic ?? + ColorScheme.fromSeed( + seedColor: Colors.green, + brightness: Brightness.light, + ), + useMaterial3: true, + ), + darkTheme: ThemeData( + colorScheme: darkDynamic ?? + ColorScheme.fromSeed( + seedColor: Colors.green, + brightness: Brightness.dark, + ), + useMaterial3: true, + ), getPages: Routes.getPages, home: const MainApp(), - // home: const Scaffold(), + builder: FlutterSmartDialog.init(), ); }), ); diff --git a/lib/models/common/reply_type.dart b/lib/models/common/reply_type.dart new file mode 100644 index 00000000..a6e8bdb1 --- /dev/null +++ b/lib/models/common/reply_type.dart @@ -0,0 +1,46 @@ +enum ReplyType { + unset, + // 视频 + video, + // 话题 + topic, + // 活动 + activity, + // 小视频 + videoS, + // 小黑屋封禁信息 + blockMsg, + // 公告信息 + publicMsg, + // 直播活动 + liveActivity, + // 活动稿件 + activityFile, + // 直播公告 + livePublic, + // 相簿 + album, + // 专栏 + column, + // 票务 + ticket, + // 音频 + audio, + + // 点评 + comment, + // 动态 + dynamics, + // 播单 + playList, + // 音乐播单 + musicPlayList, + // 漫画 + comics1, + // 漫画 + comics2, + // 漫画 + comics3, + // 课程 + course, +} diff --git a/lib/models/model_hot_video_item.dart b/lib/models/model_hot_video_item.dart index 7c30dcd4..1a9f582e 100644 --- a/lib/models/model_hot_video_item.dart +++ b/lib/models/model_hot_video_item.dart @@ -80,7 +80,9 @@ class HotVideoItemModel { pubLocation = json["pub_location"]; seasontype = json["seasontype"]; isOgv = json["isOgv"]; - rcmdReason = RcmdReason.fromJson(json['rcmd_reason']); + rcmdReason = json['rcmd_reason'] != '' + ? RcmdReason.fromJson(json['rcmd_reason']) + : null; } } diff --git a/lib/models/model_rec_video_item.dart b/lib/models/model_rec_video_item.dart index 849def9f..b651aa00 100644 --- a/lib/models/model_rec_video_item.dart +++ b/lib/models/model_rec_video_item.dart @@ -13,6 +13,7 @@ class RecVideoItemModel { this.pubdate, this.owner, this.stat, + this.isFollowed, this.rcmdReason, }); @@ -27,6 +28,7 @@ class RecVideoItemModel { int? pubdate = -1; Owner? owner; Stat? stat; + int? isFollowed; RcmdReason? rcmdReason; RecVideoItemModel.fromJson(Map json) { @@ -41,6 +43,7 @@ class RecVideoItemModel { pubdate = json["pubdate"]; owner = Owner.fromJson(json["owner"]); stat = Stat.fromJson(json["stat"]); + isFollowed = json["is_followed"] ?? 0; rcmdReason = json["rcmd_reason"] != null ? RcmdReason.fromJson(json["rcmd_reason"]) : RcmdReason(content: ''); diff --git a/lib/models/user/fav_detail.dart b/lib/models/user/fav_detail.dart new file mode 100644 index 00000000..3085dc9d --- /dev/null +++ b/lib/models/user/fav_detail.dart @@ -0,0 +1,100 @@ +import 'package:pilipala/models/model_owner.dart'; + +class FavDetailData { + FavDetailData({ + this.info, + this.medias, + this.hasMore, + }); + + Map? info; + List? medias; + bool? hasMore; + + FavDetailData.fromJson(Map json) { + info = json['info']; + medias = json['medias'] != null + ? json['medias'] + .map((e) => FavDetailItemData.fromJson(e)) + .toList() + : [FavDetailItemData()]; + hasMore = json['has_more']; + } +} + +class FavDetailItemData { + FavDetailItemData({ + this.id, + this.type, + this.title, + this.pic, + this.intro, + this.page, + this.duration, + this.owner, + this.attr, + this.cntInfo, + this.link, + this.ctime, + this.pubdate, + this.favTime, + this.bvId, + this.bvid, + // this.season, + // this.ogv, + this.stat, + }); + + int? id; + int? type; + String? title; + String? pic; + String? intro; + int? page; + int? duration; + Owner? owner; + int? attr; + Map? cntInfo; + String? link; + int? ctime; + int? pubdate; + int? favTime; + String? bvId; + String? bvid; + Stat? stat; + + FavDetailItemData.fromJson(Map json) { + id = json['id']; + type = json['type']; + title = json['title']; + pic = json['cover']; + intro = json['intro']; + page = json['page']; + duration = json['duration']; + owner = Owner.fromJson(json['upper']); + attr = json['attr']; + cntInfo = json['cnt_info']; + link = json['link']; + ctime = json['ctime']; + pubdate = json['pubtime']; + favTime = json['fav_time']; + bvId = json['bv_id']; + bvid = json['bvid']; + stat = Stat.fromJson(json['cnt_info']); + } +} + +class Stat { + Stat({ + this.view, + this.danmaku, + }); + + int? view; + int? danmaku; + + Stat.fromJson(Map json) { + view = json['play']; + danmaku = json['danmaku']; + } +} diff --git a/lib/models/user/fav_folder.dart b/lib/models/user/fav_folder.dart new file mode 100644 index 00000000..6d3f9975 --- /dev/null +++ b/lib/models/user/fav_folder.dart @@ -0,0 +1,108 @@ +class FavFolderData { + FavFolderData({ + this.count, + this.list, + this.hasMore, + }); + + int? count; + List? list; + bool? hasMore; + + FavFolderData.fromJson(Map json) { + count = json['count']; + list = json['list'] != null + ? json['list'] + .map((e) => FavFolderItemData.fromJson(e)) + .toList() + : [FavFolderItemData()]; + hasMore = json['has_more']; + } +} + +class FavFolderItemData { + FavFolderItemData({ + this.id, + this.fid, + this.mid, + this.attr, + this.title, + this.cover, + this.upper, + this.coverType, + this.intro, + this.ctime, + this.mtime, + this.state, + this.favState, + this.mediaCount, + this.viewCount, + this.vt, + this.playSwitch, + this.type, + this.link, + this.bvid, + }); + + int? id; + int? fid; + int? mid; + int? attr; + String? title; + String? cover; + Upper? upper; + int? coverType; + String? intro; + int? ctime; + int? mtime; + int? state; + int? favState; + int? mediaCount; + int? viewCount; + int? vt; + int? playSwitch; + int? type; + String? link; + String? bvid; + + FavFolderItemData.fromJson(Map json) { + id = json['id']; + fid = json['fid']; + mid = json['mid']; + attr = json['attr']; + title = json['title']; + cover = json['cover']; + upper = json['upper'] != null ? Upper.fromJson(json['upper']) : Upper(); + coverType = json['cover_type']; + intro = json['intro']; + ctime = json['ctime']; + mtime = json['mtime']; + state = json['state']; + favState = json['fav_state']; + mediaCount = json['media_count']; + viewCount = json['view_count']; + vt = json['vt']; + playSwitch = json['play_switch']; + type = json['type']; + link = json['link']; + bvid = json['bvid']; + } +} + +class Upper { + Upper({ + this.mid, + this.name, + this.face, + }); + + int? mid; + String? name; + String? face; + + Upper.fromJson(Map json) { + mid = json['mid']; + name = json['name']; + face = json['face']; + } +} diff --git a/lib/models/user/info.dart b/lib/models/user/info.dart new file mode 100644 index 00000000..b51c68f0 --- /dev/null +++ b/lib/models/user/info.dart @@ -0,0 +1,103 @@ +class UserInfoData { + UserInfoData({ + this.isLogin, + this.emailVerified, + this.face, + this.levelInfo, + this.mid, + this.mobileVerified, + this.money, + this.moral, + this.official, + this.officialVerify, + this.pendant, + this.scores, + this.uname, + this.vipDueDate, + this.vipStatus, + this.vipType, + this.vipPayType, + this.vipThemeType, + this.vipLabel, + this.vipAvatarSub, + this.vipNicknameColor, + this.wallet, + this.hasShop, + this.shopUrl, + }); + + bool? isLogin; + int? emailVerified; + String? face; + LevelInfo? levelInfo; + int? mid; + int? mobileVerified; + int? money; + int? moral; + Map? official; + Map? officialVerify; + Map? pendant; + int? scores; + String? uname; + int? vipDueDate; + int? vipStatus; + int? vipType; + int? vipPayType; + int? vipThemeType; + Map? vipLabel; + int? vipAvatarSub; + String? vipNicknameColor; + Map? wallet; + bool? hasShop; + String? shopUrl; + + UserInfoData.fromJson(Map json) { + isLogin = json['isLogin'] ?? false; + emailVerified = json['email_verified']; + face = json['face']; + levelInfo = json['level_info'] != null + ? LevelInfo.fromJson(json['level_info']) + : LevelInfo(); + mid = json['mid']; + mobileVerified = json['mobile_verified']; + money = json['money']; + moral = json['moral']; + official = json['official']; + officialVerify = json['officialVerify']; + pendant = json['pendant']; + scores = json['scores']; + uname = json['uname']; + vipDueDate = json['vipDueDate']; + vipStatus = json['vipStatus']; + vipType = json['vipType']; + vipPayType = json['vip_pay_type']; + vipThemeType = json['vip_theme_type']; + vipLabel = json['vip_label']; + vipAvatarSub = json['vip_avatar_subscript']; + vipNicknameColor = json['vip_nickname_color']; + wallet = json['wallet']; + hasShop = json['has_shop']; + shopUrl = json['shop_url']; + } +} + +class LevelInfo { + LevelInfo({ + this.currentLevel, + this.currentMin, + this.currentExp, + this.nextExp, + }); + + int? currentLevel; + int? currentMin; + int? currentExp; + int? nextExp; + + LevelInfo.fromJson(Map json) { + currentLevel = json['current_level']; + currentMin = json['current_min']; + currentExp = json['current_exp']; + nextExp = json['next_exp']; + } +} diff --git a/lib/models/user/stat.dart b/lib/models/user/stat.dart new file mode 100644 index 00000000..0b56a499 --- /dev/null +++ b/lib/models/user/stat.dart @@ -0,0 +1,17 @@ +class UserStat { + UserStat({ + this.following, + this.follower, + this.dynamicCount, + }); + + int? following; + int? follower; + int? dynamicCount; + + UserStat.fromJson(Map json) { + following = json['following']; + follower = json['follower']; + dynamicCount = json['dynamic_count']; + } +} diff --git a/lib/models/video/reply/config.dart b/lib/models/video/reply/config.dart new file mode 100644 index 00000000..90574f7c --- /dev/null +++ b/lib/models/video/reply/config.dart @@ -0,0 +1,17 @@ +class ReplyConfig { + ReplyConfig({ + this.showtopic, + this.showUpFlag, + this.readOnly, + }); + + int? showtopic; + bool? showUpFlag; + bool? readOnly; + + ReplyConfig.fromJson(Map json) { + showtopic = json['showtopic']; + showUpFlag = json['show_up_flag']; + readOnly = json['read_only']; + } +} diff --git a/lib/models/video/reply/content.dart b/lib/models/video/reply/content.dart new file mode 100644 index 00000000..97897f75 --- /dev/null +++ b/lib/models/video/reply/content.dart @@ -0,0 +1,29 @@ +class ReplyContent { + ReplyContent({ + this.message, + this.atNameToMid, // @的用户的mid null + this.memebers, // 被@的用户List 如果有的话 [] + this.emote, // 表情包 如果有的话 null + this.jumpUrl, // {} + this.pictures, // {} + this.vote, + }); + + String? message; + Map? atNameToMid; + List? memebers; + Map? emote; + Map? jumpUrl; + List? pictures; + Map? vote; + + ReplyContent.fromJson(Map json) { + message = json['message']; + atNameToMid = json['at_name_to_mid'] ?? {}; + memebers = json['memebers'] ?? []; + emote = json['emote'] ?? {}; + jumpUrl = json['jump_url'] ?? {}; + pictures = json['pictures'] ?? []; + vote = json['vote'] ?? {}; + } +} diff --git a/lib/models/video/reply/data.dart b/lib/models/video/reply/data.dart new file mode 100644 index 00000000..cc419777 --- /dev/null +++ b/lib/models/video/reply/data.dart @@ -0,0 +1,40 @@ +import 'package:pilipala/models/video/reply/item.dart'; + +import 'config.dart'; +import 'page.dart'; +import 'upper.dart'; + +class ReplyData { + ReplyData({ + this.page, + this.config, + this.replies, + this.topReplies, + this.upper, + }); + + ReplyPage? page; + ReplyConfig? config; + late List? replies; + late List? topReplies; + ReplyUpper? upper; + + ReplyData.fromJson(Map json) { + page = ReplyPage.fromJson(json['page']); + config = ReplyConfig.fromJson(json['config']); + replies = json['replies'] != null + ? json['replies'] + .map( + (item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) + .toList() + : []; + topReplies = json['top_replies'] != null + ? json['top_replies'] + .map((item) => ReplyItemModel.fromJson( + item, json['upper']['mid'], + isTopStatus: true)) + .toList() + : []; + upper = ReplyUpper.fromJson(json['upper']); + } +} diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart new file mode 100644 index 00000000..3ae811ec --- /dev/null +++ b/lib/models/video/reply/item.dart @@ -0,0 +1,159 @@ +import 'content.dart'; +import 'member.dart'; + +class ReplyItemModel { + ReplyItemModel({ + this.rpid, + this.oid, + this.type, + this.mid, + this.root, + this.parent, + this.dialog, + this.count, + this.floor, + this.state, + this.fansgrade, + this.attr, + this.ctime, + this.rpidStr, + this.rootStr, + this.parentStr, + this.like, + this.action, + this.member, + this.content, + this.replies, + this.assist, + this.upAction, + this.invisible, + this.replyControl, + this.isUp, + this.isTop, + this.cardLabel, + }); + + int? rpid; + int? oid; + int? type; + int? mid; + int? root; + int? parent; + int? dialog; + int? count; + int? floor; + int? state; + int? fansgrade; + int? attr; + int? ctime; + String? rpidStr; + String? rootStr; + String? parentStr; + int? like; + int? action; + ReplyMember? member; + ReplyContent? content; + List? replies; + int? assist; + UpAction? upAction; + bool? invisible; + ReplyControl? replyControl; + bool? isUp; + bool? isTop = false; + List? cardLabel; + + ReplyItemModel.fromJson(Map json, upperMid, + {isTopStatus = false}) { + rpid = json['rpid']; + oid = json['oid']; + type = json['type']; + mid = json['mid']; + root = json['root']; + parent = json['parent']; + dialog = json['dialog']; + count = json['count']; + floor = json['floor']; + state = json['state']; + fansgrade = json['fansgrade']; + attr = json['attr']; + ctime = json['ctime']; + rpidStr = json['rpid_str']; + rootStr = json['root_str']; + parentStr = json['parent_str']; + like = json['like']; + action = json['action']; + member = ReplyMember.fromJson(json['member']); + content = ReplyContent.fromJson(json['content']); + replies = json['replies'] != null + ? json['replies'] + .map((item) => ReplyItemModel.fromJson(item, upperMid)) + .toList() + : []; + assist = json['assist']; + upAction = UpAction.fromJson(json['up_action']); + invisible = json['invisible']; + replyControl = json['reply_control'] == null + ? null + : ReplyControl.fromJson(json['reply_control']); + isUp = upperMid.toString() == json['member']['mid']; + isTop = isTopStatus; + cardLabel = json['card_label'] != null + ? json['card_label'].map((e) => e['text_content']).toList() + : []; + } +} + +class UpAction { + UpAction({this.like, this.reply}); + + bool? like; + bool? reply; + + UpAction.fromJson(Map json) { + like = json['like']; + reply = json['reply']; + } +} + +class ReplyControl { + ReplyControl({ + this.upReply, + this.isUpTop, + this.upLike, + this.isShow, + this.entryText, + this.titleText, + this.time, + this.location, + }); + + bool? upReply; + bool? isUpTop; + bool? upLike; + bool? isShow; + String? entryText; + String? titleText; + String? time; + String? location; + + ReplyControl.fromJson(Map json) { + upReply = json['up_reply'] ?? false; + isUpTop = json['is_up_top'] ?? false; + upLike = json['up_like'] ?? false; + if (json['sub_reply_entry_text'] != null) { + final RegExp regex = RegExp(r"\d+"); + final RegExpMatch match = regex.firstMatch( + json['sub_reply_entry_text'] == null + ? '' + : json['sub_reply_entry_text']!)!; + isShow = int.parse(match.group(0)!) >= 3; + } else { + isShow = false; + } + + entryText = json['sub_reply_entry_text']; + titleText = json['sub_reply_title_text']; + time = json['time_desc']; + location = json['location'] != null ? json['location'].split(':')[1] : ''; + } +} diff --git a/lib/models/video/reply/member.dart b/lib/models/video/reply/member.dart new file mode 100644 index 00000000..5576cbd1 --- /dev/null +++ b/lib/models/video/reply/member.dart @@ -0,0 +1,71 @@ +import 'package:get/get.dart'; + +class ReplyMember { + ReplyMember({ + this.mid, + this.uname, + this.sign, + this.avatar, + this.level, + this.pendant, + this.officialVerify, + this.vip, + this.fansDetail, + }); + + String? mid; + String? uname; + String? sign; + String? avatar; + int? level; + Pendant? pendant; + Map? officialVerify; + Map? vip; + Map? fansDetail; + UserSailing? userSailing; + + ReplyMember.fromJson(Map json) { + mid = json['mid']; + uname = json['uname']; + sign = json['sign']; + avatar = json['avatar']; + level = json['level_info']['current_level']; + pendant = Pendant.fromJson(json['pendant']); + officialVerify = json['officia_verify']; + vip = json['vip']; + fansDetail = json['fans_detail']; + userSailing = json['user_sailing'] != null + ? UserSailing.fromJson(json['user_sailing']) + : UserSailing(); + } +} + +class Pendant { + Pendant({ + this.pid, + this.name, + this.image, + }); + + int? pid; + String? name; + String? image; + + Pendant.fromJson(Map json) { + pid = json['pid']; + name = json['name']; + image = json['image']; + } +} + +class UserSailing { + UserSailing({this.pendant, this.cardbg}); + + Map? pendant; + Map? cardbg; + + UserSailing.fromJson(Map json) { + pendant = json['pendant']; + cardbg = json['cardbg']; + } +} diff --git a/lib/models/video/reply/page.dart b/lib/models/video/reply/page.dart new file mode 100644 index 00000000..771b0515 --- /dev/null +++ b/lib/models/video/reply/page.dart @@ -0,0 +1,20 @@ +class ReplyPage { + ReplyPage({ + this.num, + this.size, + this.count, + this.acount, + }); + + int? num; + int? size; + int? count; + int? acount; + + ReplyPage.fromJson(Map json) { + num = json['num']; + size = json['size']; + count = json['count']; + acount = json['acount']; + } +} diff --git a/lib/models/video/reply/top_replies.dart b/lib/models/video/reply/top_replies.dart new file mode 100644 index 00000000..f769a834 --- /dev/null +++ b/lib/models/video/reply/top_replies.dart @@ -0,0 +1 @@ +class ReplyTop {} diff --git a/lib/models/video/reply/upper.dart b/lib/models/video/reply/upper.dart new file mode 100644 index 00000000..1d1f6071 --- /dev/null +++ b/lib/models/video/reply/upper.dart @@ -0,0 +1,18 @@ +import 'item.dart'; + +class ReplyUpper { + ReplyUpper({ + this.mid, + this.top, + }); + + int? mid; + ReplyItemModel? top; + + ReplyUpper.fromJson(Map json) { + mid = json['mid']; + top = json['top'] != null + ? ReplyItemModel.fromJson(json['top'], json['mid']) + : null; + } +} diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart new file mode 100644 index 00000000..8b5180f3 --- /dev/null +++ b/lib/models/video_detail_res.dart @@ -0,0 +1,524 @@ +import 'dart:convert'; + +class VideoDetailResponse { + int? code; + String? message; + int? ttl; + VideoDetailData? data; + + VideoDetailResponse({ + this.code, + this.message, + this.ttl, + this.data, + }); + + VideoDetailResponse.fromJson(Map json) { + code = json["code"]; + message = json["message"]; + ttl = json["ttl"]; + data = json["data"] == null ? null : VideoDetailData.fromJson(json["data"]); + } + + Map toJson() { + final Map data = {}; + data["code"] = code; + data["message"] = message; + data["ttl"] = ttl; + data["data"] = data; + + return data; + } +} + +class VideoDetailData { + String? bvid; + int? aid; + int? videos; + int? tid; + String? tname; + int? copyright; + String? pic; + String? title; + int? pubdate; + int? ctime; + String? desc; + List? descV2; + int? state; + int? duration; + Map? rights; + Owner? owner; + Stat? stat; + String? videoDynamic; + int? cid; + Dimension? dimension; + dynamic premiere; + int? teenageMode; + bool? isChargeableSeason; + bool? isStory; + bool? noCache; + List? pages; + Subtitle? subtitle; + // Label? label; + bool? isSeasonDisplay; + UserGarb? userGarb; + HonorReply? honorReply; + String? likeIcon; + bool? needJumpBv; + + VideoDetailData({ + this.bvid, + this.aid, + this.videos, + this.tid, + this.tname, + this.copyright, + this.pic, + this.title, + this.pubdate, + this.ctime, + this.desc, + this.descV2, + this.state, + this.duration, + this.rights, + this.owner, + this.stat, + this.videoDynamic, + this.cid, + this.dimension, + this.premiere, + this.teenageMode, + this.isChargeableSeason, + this.isStory, + this.noCache, + this.pages, + this.subtitle, + this.isSeasonDisplay, + this.userGarb, + this.honorReply, + this.likeIcon, + this.needJumpBv, + }); + + VideoDetailData.fromJson(Map json) { + bvid = json["bvid"]; + aid = json["aid"]; + videos = json["videos"]; + tid = json["tid"]; + tname = json["tname"]; + copyright = json["copyright"]; + pic = json["pic"]; + title = json["title"]; + pubdate = json["pubdate"]; + ctime = json["ctime"]; + desc = json["desc"]; + descV2 = json["desc_v2"] == null + ? [] + : List.from(json["desc_v2"]!.map((e) => DescV2.fromJson(e))); + state = json["state"]; + duration = json["duration"]; + rights = + Map.from(json["rights"]!).map((k, v) => MapEntry(k, v)); + owner = json["owner"] == null ? null : Owner.fromJson(json["owner"]); + stat = json["stat"] == null ? null : Stat.fromJson(json["stat"]); + videoDynamic = json["dynamic"]; + cid = json["cid"]; + dimension = json["dimension"] == null + ? null + : Dimension.fromJson(json["dimension"]); + premiere = json["premiere"]; + teenageMode = json["teenage_mode"]; + isChargeableSeason = json["is_chargeable_season"]; + isStory = json["is_story"]; + noCache = json["no_cache"]; + pages = json["pages"] == null + ? [] + : List.from(json["pages"]!.map((e) => Page.fromJson(e))); + subtitle = + json["subtitle"] == null ? null : Subtitle.fromJson(json["subtitle"]); + isSeasonDisplay = json["is_season_display"]; + userGarb = + json["user_garb"] == null ? null : UserGarb.fromJson(json["user_garb"]); + honorReply = json["honor_reply"] == null + ? null + : HonorReply.fromJson(json["honor_reply"]); + likeIcon = json["like_icon"]; + needJumpBv = json["need_jump_bv"]; + } + + Map toJson() => { + "bvid": bvid, + "aid": aid, + "videos": videos, + "tid": tid, + "tname": tname, + "copyright": copyright, + "pic": pic, + "title": title, + "pubdate": pubdate, + "ctime": ctime, + "desc": desc, + "desc_v2": descV2 == null + ? [] + : List.from(descV2!.map((e) => e.toJson())), + "state": state, + "duration": duration, + "rights": + Map.from(rights!).map((k, v) => MapEntry(k, v)), + "owner": owner?.toJson(), + "stat": stat?.toJson(), + "dynamic": videoDynamic, + "cid": cid, + "dimension": dimension?.toJson(), + "premiere": premiere, + "teenage_mode": teenageMode, + "is_chargeable_season": isChargeableSeason, + "is_story": isStory, + "no_cache": noCache, + "pages": pages == null + ? [] + : List.from(pages!.map((e) => e.toJson())), + "subtitle": subtitle?.toJson(), + "is_season_display": isSeasonDisplay, + "user_garb": userGarb?.toJson(), + "honor_reply": honorReply?.toJson(), + "like_icon": likeIcon, + "need_jump_bv": needJumpBv, + }; +} + +class DescV2 { + String? rawText; + int? type; + int? bizId; + + DescV2({ + this.rawText, + this.type, + this.bizId, + }); + + fromRawJson(String str) { + return DescV2.fromJson(json.decode(str)); + } + + String toRawJson() => json.encode(toJson()); + + DescV2.fromJson(Map json) { + rawText = json["raw_text"]; + type = json["type"]; + bizId = json["biz_id"]; + } + + Map toJson() { + final Map data = {}; + + data["raw_text"] = rawText; + data["type"] = type; + data["biz_id"] = bizId; + + return data; + } +} + +class Dimension { + int? width; + int? height; + int? rotate; + + Dimension({ + this.width, + this.height, + this.rotate, + }); + + fromRawJson(String str) => Dimension.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Dimension.fromJson(Map json) { + width = json["width"]; + height = json["height"]; + rotate = json["rotate"]; + } + + Map toJson() { + final Map data = {}; + + data["width"] = width; + data["height"] = height; + data["rotate"] = rotate; + data["data"] = data; + + return data; + } +} + +class HonorReply { + List? honor; + + HonorReply({ + this.honor, + }); + + fromRawJson(String str) => HonorReply.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + HonorReply.fromJson(Map json) { + honor = json["honor"] == null + ? [] + : List.from(json["honor"]!.map((x) => Honor.fromJson(x))); + } + + Map toJson() { + final Map data = {}; + + data["honor"] = + honor == null ? [] : List.from(honor!.map((x) => x.toJson())); + return data; + } +} + +class Honor { + int? aid; + int? type; + String? desc; + int? weeklyRecommendNum; + + Honor({ + this.aid, + this.type, + this.desc, + this.weeklyRecommendNum, + }); + + fromRawJson(String str) => Honor.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Honor.fromJson(Map json) { + aid = json["aid"]; + type = json["type"]; + desc = json["desc"]; + weeklyRecommendNum = json["weekly_recommend_num"]; + } + + Map toJson() { + final Map data = {}; + + data["aid"] = aid; + data["type"] = type; + data["desc"] = desc; + data["weekly_recommend_num"] = weeklyRecommendNum; + + return data; + } +} + +class Owner { + int? mid; + String? name; + String? face; + + Owner({ + this.mid, + this.name, + this.face, + }); + + fromRawJson(String str) => Owner.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Owner.fromJson(Map json) { + mid = json["mid"]; + name = json["name"]; + face = json["face"]; + } + + Map toJson() { + final Map data = {}; + data["mid"] = mid; + data["name"] = name; + data["face"] = face; + return data; + } +} + +class Page { + int? cid; + int? page; + String? from; + String? pagePart; + int? duration; + String? vid; + String? weblink; + Dimension? dimension; + String? firstFrame; + + Page({ + this.cid, + this.page, + this.from, + this.pagePart, + this.duration, + this.vid, + this.weblink, + this.dimension, + this.firstFrame, + }); + + fromRawJson(String str) => Page.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Page.fromJson(Map json) { + cid = json["cid"]; + page = json["page"]; + from = json["from"]; + pagePart = json["part"]; + duration = json["duration"]; + vid = json["vid"]; + weblink = json["weblink"]; + dimension = json["dimension"] == null + ? null + : Dimension.fromJson(json["dimension"]); + firstFrame = json["first_frame"]; + } + + Map toJson() { + final Map data = {}; + data["cid"] = cid; + data["page"] = page; + data["from"] = from; + data["part"] = pagePart; + data["duration"] = duration; + data["vid"] = vid; + data["weblink"] = weblink; + data["dimension"] = dimension?.toJson(); + data["first_frame"] = firstFrame; + return data; + } +} + +class Stat { + int? aid; + int? view; + int? danmaku; + int? reply; + int? favorite; + int? coin; + int? share; + int? nowRank; + int? hisRank; + int? like; + int? dislike; + String? evaluation; + String? argueMsg; + + Stat({ + this.aid, + this.view, + this.danmaku, + this.reply, + this.favorite, + this.coin, + this.share, + this.nowRank, + this.hisRank, + this.like, + this.dislike, + this.evaluation, + this.argueMsg, + }); + + fromRawJson(String str) => Stat.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Stat.fromJson(Map json) { + aid = json["aid"]; + view = json["view"]; + danmaku = json["danmaku"]; + reply = json["reply"]; + favorite = json["favorite"]; + coin = json["coin"]; + share = json["share"]; + nowRank = json["now_rank"]; + hisRank = json["his_rank"]; + like = json["like"]; + dislike = json["dislike"]; + evaluation = json["evaluation"]; + argueMsg = json["argue_msg"]; + } + + Map toJson() { + final Map data = {}; + + data["aid"] = aid; + data["view"] = view; + data["danmaku"] = danmaku; + data["reply"] = reply; + data["favorite"] = favorite; + data["coin"] = coin; + data["share"] = share; + data["now_rank"] = nowRank; + data["his_rank"] = hisRank; + data["like"] = like; + data["dislike"] = dislike; + data["evaluation"] = evaluation; + data["argue_msg"] = argueMsg; + return data; + } +} + +class Subtitle { + bool? allowSubmit; + List? list; + + Subtitle({ + this.allowSubmit, + this.list, + }); + + fromRawJson(String str) => Subtitle.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + Subtitle.fromJson(Map json) { + allowSubmit = json["allow_submit"]; + list = json["list"] == null + ? [] + : List.from(json["list"]!.map((x) => x)); + } + + Map toJson() { + final Map data = {}; + + data["allow_submit"] = allowSubmit; + data["list"] = list == null ? [] : List.from(list!.map((x) => x)); + return data; + } +} + +class UserGarb { + String? urlImageAniCut; + + UserGarb({ + this.urlImageAniCut, + }); + + fromRawJson(String str) => UserGarb.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + UserGarb.fromJson(Map json) { + urlImageAniCut = json["url_image_ani_cut"]; + } + + Map toJson() => {"url_image_ani_cut": urlImageAniCut}; +} + +class Label {} diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart new file mode 100644 index 00000000..0321ea39 --- /dev/null +++ b/lib/pages/fav/controller.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/utils/storage.dart'; + +class FavController extends GetxController { + Rx favFolderData = FavFolderData().obs; + + Future queryFavFolder() async { + var res = await await UserHttp.userfavFolder( + pn: 1, + ps: 10, + mid: GStrorage.user.get(UserBoxKey.userMid), + ); + favFolderData.value = res['data']; + return res; + } +} diff --git a/lib/pages/fav/index.dart b/lib/pages/fav/index.dart new file mode 100644 index 00000000..84d36325 --- /dev/null +++ b/lib/pages/fav/index.dart @@ -0,0 +1,4 @@ +library fav; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart new file mode 100644 index 00000000..3f23f451 --- /dev/null +++ b/lib/pages/fav/view.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/fav/index.dart'; + +class FavPage extends StatefulWidget { + const FavPage({super.key}); + + @override + State createState() => _FavPageState(); +} + +class _FavPageState extends State { + final FavController _favController = Get.put(FavController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + title: const Text('我的收藏'), + ), + body: FutureBuilder( + future: _favController.queryFavFolder(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => ListView.builder( + itemCount: _favController.favFolderData.value.list!.length, + itemBuilder: (context, index) { + return ListTile( + onTap: () => Get.toNamed( + '/favDetail', + arguments: + _favController.favFolderData.value.list![index], + parameters: { + 'mediaId': _favController + .favFolderData.value.list![index].id + .toString(), + }, + ), + leading: const Icon(Icons.folder_special_outlined), + minLeadingWidth: 0, + title: Text(_favController + .favFolderData.value.list![index].title!), + subtitle: Text( + '${_favController.favFolderData.value.list![index].mediaCount}个内容', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ), + ); + }, + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return Text('请求中'); + } + }, + ), + ); + } +} diff --git a/lib/pages/favDetail/controller.dart b/lib/pages/favDetail/controller.dart new file mode 100644 index 00000000..cc59676a --- /dev/null +++ b/lib/pages/favDetail/controller.dart @@ -0,0 +1,50 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/user/fav_detail.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; + +class FavDetailController extends GetxController { + FavFolderItemData? item; + Rx favDetailData = FavDetailData().obs; + int? mediaId; + + @override + void onInit() { + item = Get.arguments; + if (Get.parameters.keys.isNotEmpty) { + mediaId = int.parse(Get.parameters['mediaId']!); + } + super.onInit(); + } + + Future queryUserFavFolderDetail() async { + var res = await await UserHttp.userFavFolderDetail( + pn: 1, + ps: 15, + mediaId: mediaId!, + ); + favDetailData.value = res['data']; + return res; + } + + onCancelFav(int id) async { + var result = await VideoHttp.favVideo( + aid: id.toString(), addIds: '', delIds: mediaId.toString()); + if (result['status']) { + if (result['data']['prompt']) { + List dataList = favDetailData.value.medias!; + for (var i in dataList) { + if (i.id == id) { + dataList.remove(i); + break; + } + } + favDetailData.value.medias = dataList; + favDetailData.refresh(); + SmartDialog.showToast('取消收藏'); + } + } + } +} diff --git a/lib/pages/favDetail/index.dart b/lib/pages/favDetail/index.dart new file mode 100644 index 00000000..dfeafac8 --- /dev/null +++ b/lib/pages/favDetail/index.dart @@ -0,0 +1,4 @@ +library favdetail; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/favDetail/view.dart b/lib/pages/favDetail/view.dart new file mode 100644 index 00000000..7ee7d429 --- /dev/null +++ b/lib/pages/favDetail/view.dart @@ -0,0 +1,219 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/pages/favDetail/index.dart'; + +import 'widget/fav_video_card.dart'; + +class FavDetailPage extends StatefulWidget { + const FavDetailPage({super.key}); + + @override + State createState() => _FavDetailPageState(); +} + +class _FavDetailPageState extends State { + late final ScrollController _controller = ScrollController(); + final FavDetailController _favDetailController = + Get.put(FavDetailController()); + late StreamController titleStreamC; // a + + @override + void initState() { + super.initState(); + titleStreamC = StreamController(); + _controller.addListener( + () { + if (_controller.offset > 160) { + titleStreamC.add(true); + } else if (_controller.offset <= 160) { + titleStreamC.add(false); + } + }, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + controller: _controller, + slivers: [ + SliverAppBar( + expandedHeight: 260 - MediaQuery.of(context).padding.top, + pinned: true, + title: StreamBuilder( + stream: titleStreamC.stream, + initialData: false, + builder: (context, AsyncSnapshot snapshot) { + return AnimatedOpacity( + opacity: snapshot.data ? 1 : 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 500), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _favDetailController.item!.title!, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + '共${_favDetailController.item!.mediaCount!}条视频', + style: Theme.of(context).textTheme.labelMedium, + ) + ], + ) + ], + ), + ); + }, + ), + // actions: [ + // IconButton( + // onPressed: () {}, + // icon: const Icon(Icons.more_vert), + // ), + // const SizedBox(width: 4) + // ], + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.2), + ), + ), + ), + padding: EdgeInsets.only( + top: kTextTabBarHeight + + MediaQuery.of(context).padding.top + + 30, + left: 20, + right: 20), + child: SizedBox( + height: 200, + child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 180, + height: 110, + child: NetworkImgLayer( + width: 180, + height: 110, + src: _favDetailController.item!.cover, + ), + ), + const SizedBox(width: 14), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + _favDetailController.item!.title!, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + _favDetailController.item!.upper!.name!, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ) + ], + ) + ], + ), + ), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), + child: Obx( + () => Text( + '共${_favDetailController.favDetailData.value.medias != null ? _favDetailController.favDetailData.value.medias!.length : '-'}条视频', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + letterSpacing: 1), + ), + ), + ), + ), + FutureBuilder( + future: _favDetailController.queryUserFavFolderDetail(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + if (_favDetailController.item!.mediaCount == 0) { + return const SliverToBoxAdapter( + child: SizedBox( + height: 300, + child: Center(child: Text('没有内容')), + ), + ); + } else { + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return FavVideoCardH( + videoItem: _favDetailController + .favDetailData.value.medias![index], + ); + }, + childCount: _favDetailController + .favDetailData.value.medias!.length), + ), + ); + } + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + return const SliverToBoxAdapter( + child: SizedBox( + height: 300, + child: Center(child: Text('加载中')), + ), + ); + } + }, + ), + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.of(context).padding.bottom + 20, + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/favDetail/widget/fav_video_card.dart b/lib/pages/favDetail/widget/fav_video_card.dart new file mode 100644 index 00000000..d4e9f6da --- /dev/null +++ b/lib/pages/favDetail/widget/fav_video_card.dart @@ -0,0 +1,165 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/stat/danmu.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; + +import '../controller.dart'; + +// 收藏视频卡片 - 水平布局 +class FavVideoCardH extends StatelessWidget { + var videoItem; + final FavDetailController _favDetailController = + Get.put(FavDetailController()); + + FavVideoCardH({Key? key, required this.videoItem}) : super(key: key); + + @override + Widget build(BuildContext context) { + int id = videoItem.id; + String heroTag = Utils.makeHeroTag(id); + return Dismissible( + movementDuration: const Duration(milliseconds: 300), + background: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.clear_all_rounded), + SizedBox(width: 6), + Text('取消收藏') + ], + )), + direction: DismissDirection.endToStart, + key: ValueKey(videoItem.id), + onDismissed: (DismissDirection direction) { + _favDetailController.onCancelFav(videoItem.id); + // widget.onDeleteNotice(); + }, + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=$id', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 5, 12, 5), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = + MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, + ), + ), + // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + color: + Colors.black54.withOpacity(0.4)), + child: Text( + Utils.timeFormat(videoItem.duration!), + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) + ], + ); + }, + ), + ), + VideoContent(videoItem: videoItem) + ], + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} + +class VideoContent extends StatelessWidget { + final videoItem; + const VideoContent({super.key, required this.videoItem}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + videoItem.title, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, + fontWeight: FontWeight.w500), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Text( + videoItem.owner.name, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + Row( + children: [ + StatView( + theme: 'gray', + view: videoItem.cntInfo['play'], + ), + const SizedBox(width: 8), + StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']) + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index b81c4de7..7d05e312 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; -import 'package:pilipala/http/api.dart'; -import 'package:pilipala/http/init.dart'; +import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/model_rec_video_item.dart'; class HomeController extends GetxController { @@ -17,28 +16,27 @@ class HomeController extends GetxController { @override void onInit() { super.onInit(); - queryRcmdFeed('init'); + // queryRcmdFeed('init'); } // 获取推荐 Future queryRcmdFeed(type) async { - var res = await Request().get( - Api.recommendList, - data: {'feed_version': "V3", 'ps': count, 'fresh_idx': _currentPage}, + var res = await VideoHttp.rcmdVideoList( + ps: count, + freshIdx: _currentPage, ); - List list = []; - for (var i in res.data['data']['item']) { - list.add(RecVideoItemModel.fromJson(i)); + if (res['status']) { + if (type == 'init') { + videoList.value = res['data']; + } else if (type == 'onRefresh') { + videoList.insertAll(0, res['data']); + } else if (type == 'onLoad') { + videoList.addAll(res['data']); + } + _currentPage += 1; } - if (type == 'init') { - videoList.value = list; - } else if (type == 'onRefresh') { - videoList.insertAll(0, list); - } else if (type == 'onLoad') { - videoList.addAll(list); - } - _currentPage += 1; isLoadingMore = false; + return res; } // 下拉刷新 @@ -48,7 +46,6 @@ class HomeController extends GetxController { // 上拉加载 Future onLoad() async { - await Future.delayed(const Duration(milliseconds: 500)); queryRcmdFeed('onLoad'); } diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index be20a34c..37bd73d2 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_v.dart'; import './controller.dart'; import 'package:pilipala/common/constants.dart'; @@ -18,6 +19,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with AutomaticKeepAliveClientMixin { final HomeController _homeController = Get.put(HomeController()); + Future? _futureBuilderFuture; List videoList = []; @override @@ -26,6 +28,7 @@ class _HomePageState extends State @override void initState() { super.initState(); + _futureBuilderFuture = _homeController.queryRcmdFeed('init'); _homeController.videoList.listen((value) { videoList = value; setState(() {}); @@ -71,37 +74,25 @@ class _HomePageState extends State ? EdgeInsets.zero : const EdgeInsets.fromLTRB( StyleString.cardSpace, 0, StyleString.cardSpace, 8), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - // 行间距 - mainAxisSpacing: StyleString.cardSpace, - // 列间距 - crossAxisSpacing: StyleString.cardSpace, - // 列数 - crossAxisCount: _homeController.crossAxisCount, - mainAxisExtent: MediaQuery.of(context).size.width / - _homeController.crossAxisCount / - StyleString.aspectRatio + - 72), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return videoList.isNotEmpty - ? VideoCardV( - videoItem: videoList[index], - longPress: () { - _homeController.popupDialog = - _createPopupDialog(videoList[index]); - Overlay.of(context) - .insert(_homeController.popupDialog!); - }, - longPressEnd: () { - _homeController.popupDialog?.remove(); - }, - ) - : const VideoCardVSkeleton(); - }, - childCount: videoList.isNotEmpty ? videoList.length : 10, - ), + sliver: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx(() => contentGrid( + _homeController, _homeController.videoList)); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return contentGrid(_homeController, []); + } + }, ), ), const LoadingMore() @@ -114,8 +105,44 @@ class _HomePageState extends State OverlayEntry _createPopupDialog(videoItem) { return OverlayEntry( - builder: (context) => AnimatedDialog( - child: OverlayPop(videoItem: videoItem), + builder: (context) => AnimatedDialog( + child: OverlayPop(videoItem: videoItem), + )); + } + + Widget contentGrid(ctr, videoList) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + // 行间距 + mainAxisSpacing: StyleString.cardSpace, + // 列间距 + crossAxisSpacing: StyleString.cardSpace, + // 列数 + crossAxisCount: ctr.crossAxisCount, + mainAxisExtent: MediaQuery.of(context).size.width / + ctr.crossAxisCount / + StyleString.aspectRatio + + 70, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return videoList!.isNotEmpty + ? + // VideoCardV(videoItem: videoList![index]) + VideoCardV( + videoItem: videoList[index], + longPress: () { + _homeController.popupDialog = + _createPopupDialog(videoList[index]); + Overlay.of(context).insert(_homeController.popupDialog!); + }, + longPressEnd: () { + _homeController.popupDialog?.remove(); + }, + ) + : const VideoCardVSkeleton(); + }, + childCount: videoList!.isNotEmpty ? videoList!.length : 10, ), ); } diff --git a/lib/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index 56a53861..872d7eef 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -1,5 +1,9 @@ import 'dart:io'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/mine/view.dart'; class HomeAppBar extends StatelessWidget { const HomeAppBar({super.key}); @@ -9,11 +13,7 @@ class HomeAppBar extends StatelessWidget { return SliverAppBar( // forceElevated: true, scrolledUnderElevation: 0, - toolbarHeight: Platform.isAndroid - ? (MediaQuery.of(context).padding.top + 6) - : Platform.isIOS - ? MediaQuery.of(context).padding.top - 2 - : kToolbarHeight, + toolbarHeight: MediaQuery.of(context).padding.top, expandedHeight: kToolbarHeight + MediaQuery.of(context).padding.top, automaticallyImplyLeading: false, pinned: true, @@ -29,19 +29,26 @@ class HomeAppBar extends StatelessWidget { title: const Text( 'PiLiPaLa', style: TextStyle( - fontSize: 18, + fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 1, + fontFamily: 'ArchivoNarrow', ), ), actions: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.notifications_none_rounded), + icon: const Icon(CupertinoIcons.search, size: 22), ), + // IconButton( + // onPressed: () {}, + // icon: const Icon(CupertinoIcons.bell, size: 22), + // ), IconButton( - onPressed: () {}, - icon: const Icon(Icons.search_rounded), + onPressed: () { + Get.bottomSheet(const MinePage()); + }, + icon: const Icon(CupertinoIcons.person, size: 22), ), const SizedBox(width: 10) ], diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index 3f8a2bb0..65706c32 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -1,8 +1,6 @@ -import 'package:flutter/animation.dart'; -import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; -import 'package:pilipala/http/api.dart'; -import 'package:pilipala/http/init.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; class HotController extends GetxController { @@ -14,31 +12,24 @@ class HotController extends GetxController { bool flag = false; OverlayEntry? popupDialog; - @override - void onInit() { - super.onInit(); - queryHotFeed('init'); - } - // 获取推荐 Future queryHotFeed(type) async { - var res = await Request().get( - Api.hotList, - data: {'pn': _currentPage, 'ps': _count}, + var res = await VideoHttp.hotVideoList( + pn: _currentPage, + ps: _count, ); - List list = []; - for (var i in res.data['data']['list']) { - list.add(HotVideoItemModel.fromJson(i)); + if (res['status']) { + if (type == 'init') { + videoList.value = res['data']; + } else if (type == 'onRefresh') { + videoList.insertAll(0, res['data']); + } else if (type == 'onLoad') { + videoList.addAll(res['data']); + } + _currentPage += 1; } - if (type == 'init') { - videoList.value = list; - } else if (type == 'onRefresh') { - videoList.insertAll(0, list); - } else if (type == 'onLoad') { - videoList.addAll(list); - } - _currentPage += 1; isLoadingMore = false; + return res; } // 下拉刷新 diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index f0a5ab0b..3a8c51af 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/pages/hot/controller.dart'; import 'package:pilipala/pages/home/widgets/app_bar.dart'; @@ -16,6 +18,7 @@ class HotPage extends StatefulWidget { class _HotPageState extends State with AutomaticKeepAliveClientMixin { final HotController _hotController = Get.put(HotController()); List videoList = []; + Future? _futureBuilderFuture; @override bool get wantKeepAlive => true; @@ -23,11 +26,7 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { @override void initState() { super.initState(); - _hotController.videoList.listen((value) { - videoList = value; - setState(() {}); - }); - + _futureBuilderFuture = _hotController.queryHotFeed('init'); _hotController.scrollController.addListener( () { if (_hotController.scrollController.position.pixels >= @@ -54,20 +53,46 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { controller: _hotController.scrollController, slivers: [ const HomeAppBar(), - SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return VideoCardH( - videoItem: videoList[index], - longPress: () { - _hotController.popupDialog = - _createPopupDialog(videoList[index]); - Overlay.of(context).insert(_hotController.popupDialog!); - }, - longPressEnd: () { - _hotController.popupDialog?.remove(); - }, - ); - }, childCount: videoList.length)), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return VideoCardH( + videoItem: _hotController.videoList[index], + longPress: () { + _hotController.popupDialog = _createPopupDialog( + _hotController.videoList[index]); + Overlay.of(context) + .insert(_hotController.popupDialog!); + }, + longPressEnd: () { + _hotController.popupDialog?.remove(); + }, + ); + }, childCount: _hotController.videoList.length), + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), + ); + } + }, + ), SliverToBoxAdapter( child: SizedBox( height: MediaQuery.of(context).padding.bottom + 10, diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index f7ef9e49..7066cd9f 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -1,30 +1,97 @@ +import 'package:flutter/cupertino.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/pages/home/view.dart'; import 'package:pilipala/pages/hot/view.dart'; -import 'package:pilipala/pages/mine/view.dart'; +import 'package:pilipala/pages/media/index.dart'; +import 'package:pilipala/utils/storage.dart'; class MainController extends GetxController { List pages = [ const HomePage(), const HotPage(), - const MinePage(), + const MediaPage(), ]; - List navigationBars = [ + RxList navigationBars = [ { - 'icon': const Icon(Icons.home_outlined), - 'selectedIcon': const Icon(Icons.home), + // 'icon': const Icon(Icons.home_outlined), + // 'selectedIcon': const Icon(Icons.home), + 'icon': const Icon( + CupertinoIcons.square_favorites_alt, + size: 21, + ), + 'selectedIcon': const Icon( + CupertinoIcons.square_favorites_alt_fill, + size: 21, + ), 'label': "推荐", }, { - 'icon': const Icon(Icons.whatshot_outlined), - 'selectedIcon': const Icon(Icons.whatshot_rounded), + // 'icon': const Icon(Icons.whatshot_outlined), + // 'selectedIcon': const Icon(Icons.whatshot_rounded), + 'icon': const Icon( + CupertinoIcons.flame, + size: 20, + ), + 'selectedIcon': const Icon( + CupertinoIcons.flame_fill, + size: 20, + ), 'label': "热门", }, + // { + // 'icon': const Icon( + // CupertinoIcons.person, + // size: 21, + // ), + // 'selectedIcon': const Icon( + // CupertinoIcons.person_fill, + // size: 21, + // ), + // 'label': "我的", + // }, { - 'icon': const Icon(Icons.person_outline), - 'selectedIcon': const Icon(Icons.person), - 'label': "我的", + // 'icon': const Icon(Icons.person_outline), + // 'selectedIcon': const Icon(Icons.person), + 'icon': const Icon( + CupertinoIcons.folder, + size: 20, + ), + 'selectedIcon': const Icon( + CupertinoIcons.folder_fill, + size: 20, + ), + 'label': "媒体库", } - ]; + ].obs; + + @override + void onInit() { + super.onInit(); + // readuUserFace(); + } + + // 设置头像 + // readuUserFace() async { + // Box user = GStrorage.user; + // if (user.get(UserBoxKey.userFace) != null) { + // navigationBars.last['icon'] = + // navigationBars.last['selectedIcon'] = NetworkImgLayer( + // width: 25, + // height: 25, + // type: 'avatar', + // src: user.get(UserBoxKey.userFace), + // ); + // navigationBars.last['label'] = '我'; + // } + // } + + // 重置 + // resetLast() { + // navigationBars.last['icon'] = const Icon(Icons.person_outline); + // navigationBars.last['selectedIcon'] = const Icon(Icons.person); + // navigationBars.last['label'] = '我的'; + // } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 0b7556f4..ded53199 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -15,6 +15,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { final MainController _mainController = Get.put(MainController()); final HomeController _homeController = Get.put(HomeController()); final HotController _hotController = Get.put(HotController()); + PageController? _pageController; late AnimationController? _animationController; late Animation? _fadeAnimation; @@ -36,6 +37,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { _slideAnimation = Tween(begin: 0.8, end: 1.0).animate(_animationController!); _lastSelectTime = DateTime.now().millisecondsSinceEpoch; + _pageController = PageController(initialPage: selectedIndex); } void setIndex(int value) async { @@ -47,7 +49,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { }); setState(() {}); } - + _pageController!.jumpToPage(value); var currentPage = _mainController.pages[value]; if (currentPage is HomePage) { if (_homeController.flag) { @@ -98,23 +100,30 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { reverseCurve: Curves.linear, ), ), - child: IndexedStack( - index: selectedIndex, + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + onPageChanged: (index) { + selectedIndex = index; + setState(() {}); + }, children: _mainController.pages, ), ), ), - bottomNavigationBar: NavigationBar( - elevation: 1, - destinations: _mainController.navigationBars.map((e) { - return NavigationDestination( - icon: e['icon'], - selectedIcon: e['selectedIcon'], - label: e['label'], - ); - }).toList(), - selectedIndex: selectedIndex, - onDestinationSelected: (value) => setIndex(value), + bottomNavigationBar: Obx( + () => NavigationBar( + elevation: 1, + destinations: _mainController.navigationBars.map((e) { + return NavigationDestination( + icon: e['icon'], + selectedIcon: e['selectedIcon'], + label: e['label'], + ); + }).toList(), + selectedIndex: selectedIndex, + onDestinationSelected: (value) => setIndex(value), + ), ), ); } diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart new file mode 100644 index 00000000..f32ca4c3 --- /dev/null +++ b/lib/pages/media/controller.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/utils/storage.dart'; + +class MediaController extends GetxController { + Rx favFolderData = FavFolderData().obs; + List list = [ + { + 'icon': Icons.file_download_outlined, + 'title': '离线缓存', + 'onTap': () {}, + }, + { + 'icon': Icons.history, + 'title': '观看记录', + 'onTap': () {}, + }, + { + 'icon': Icons.star_border, + 'title': '我的收藏', + 'onTap': () => Get.toNamed('/fav'), + }, + { + 'icon': Icons.watch_later_outlined, + 'title': '稍候再看', + 'onTap': () => {}, + }, + ]; + + Future queryFavFolder() async { + var res = await await UserHttp.userfavFolder( + pn: 1, + ps: 5, + mid: GStrorage.user.get(UserBoxKey.userMid), + ); + favFolderData.value = res['data']; + return res; + } +} diff --git a/lib/pages/media/index.dart b/lib/pages/media/index.dart new file mode 100644 index 00000000..8fae4891 --- /dev/null +++ b/lib/pages/media/index.dart @@ -0,0 +1,4 @@ +library media; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart new file mode 100644 index 00000000..a18b1e3d --- /dev/null +++ b/lib/pages/media/view.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/pages/media/index.dart'; + +class MediaPage extends StatefulWidget { + const MediaPage({super.key}); + + @override + State createState() => _MediaPageState(); +} + +class _MediaPageState extends State + with AutomaticKeepAliveClientMixin { + final MediaController _mediaController = Get.put(MediaController()); + Future? _futureBuilderFuture; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = _mediaController.queryFavFolder(); + } + + @override + Widget build(BuildContext context) { + Color primary = Theme.of(context).colorScheme.primary; + return Scaffold( + appBar: AppBar(toolbarHeight: 30), + body: Column( + children: [ + ListTile( + leading: null, + title: Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + '媒体库', + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleLarge!.fontSize, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + for (var i in _mediaController.list) ...[ + ListTile( + onTap: () => i['onTap'](), + dense: true, + leading: Padding( + padding: const EdgeInsets.only(left: 15), + child: Icon( + i['icon'], + color: primary, + ), + ), + contentPadding: + const EdgeInsets.only(left: 15, top: 2, bottom: 2), + minLeadingWidth: 0, + title: Text( + i['title'], + style: const TextStyle(fontSize: 15), + ), + ), + ], + favFolder() + ], + ), + ); + } + + Widget favFolder() { + return Column( + children: [ + Divider( + height: 35, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ListTile( + onTap: () {}, + leading: null, + dense: true, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Obx( + () => Text.rich( + TextSpan( + children: [ + TextSpan( + text: '收藏夹 ', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleMedium!.fontSize, + fontWeight: FontWeight.bold), + ), + if (_mediaController.favFolderData.value.count != null) + TextSpan( + text: _mediaController.favFolderData.value.count + .toString(), + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + ), + trailing: IconButton( + onPressed: () => _mediaController.queryFavFolder(), + icon: const Icon( + Icons.refresh, + size: 20, + ), + ), + ), + // const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 170, + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + List favFolderList = + _mediaController.favFolderData.value.list!; + int favFolderCount = + _mediaController.favFolderData.value.count!; + bool flag = favFolderCount > favFolderList.length; + return Obx(() => ListView.builder( + itemCount: _mediaController + .favFolderData.value.list!.length + + (flag ? 1 : 0), + itemBuilder: (context, index) { + if (flag && index == favFolderList.length) { + return Padding( + padding: const EdgeInsets.only( + right: 14, bottom: 35), + child: Center( + child: IconButton( + onPressed: () => Get.toNamed('/fav'), + icon: Icon( + Icons.arrow_forward_ios, + size: 18, + color: Theme.of(context).primaryColor, + ), + ), + )); + } else { + return FavFolderItem( + item: _mediaController + .favFolderData.value.list![index], + index: index); + } + }, + scrollDirection: Axis.horizontal, + )); + } else { + return SizedBox( + height: 160, + child: Center(child: Text(data['msg'])), + ); + } + } else { + // 骨架屏 + return SizedBox(); + } + }), + ), + ], + ); + } +} + +class FavFolderItem extends StatelessWidget { + FavFolderItem({super.key, this.item, this.index}); + FavFolderItemData? item; + int? index; + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14), + child: GestureDetector( + onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: { + 'mediaId': item!.id.toString(), + }), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Container( + width: 180, + height: 110, + margin: const EdgeInsets.only(bottom: 8), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.onInverseSurface, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.onInverseSurface, + offset: const Offset(4, -12), // 阴影与容器的距离 + blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。 + spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。 + ), + ], + ), + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + return NetworkImgLayer( + src: item!.cover, + width: box.maxWidth, + height: box.maxHeight, + ); + }, + ), + ), + Text( + ' ${item!.title}', + overflow: TextOverflow.fade, + maxLines: 1, + ), + Text( + ' 共${item!.mediaCount}条视频', + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Theme.of(context).colorScheme.outline), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index e69de29b..db73fd6f 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -0,0 +1,72 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/info.dart'; +import 'package:pilipala/models/user/stat.dart'; +import 'package:pilipala/pages/main/controller.dart'; +import 'package:pilipala/utils/storage.dart'; + +class MineController extends GetxController { + // 用户信息 头像、昵称、lv + Rx userInfo = UserInfoData().obs; + // 用户状态 动态、关注、粉丝 + Rx userStat = UserStat().obs; + Box user = GStrorage.user; + RxBool userLogin = false.obs; + + onLogin() { + Get.toNamed( + '/webview', + parameters: { + 'url': 'https://passport.bilibili.com/h5-app/passport/login', + 'type': 'login', + 'pageTitle': '登录bilibili', + }, + ); + } + + Future queryUserInfo() async { + if (user.get(UserBoxKey.userLogin) == null) { + return {'status': false}; + } + var res = await UserHttp.userInfo(); + if (res['status']) { + if (res['data'].isLogin) { + userInfo.value = res['data']; + user.put(UserBoxKey.userName, res['data'].uname); + user.put(UserBoxKey.userFace, res['data'].face); + user.put(UserBoxKey.userMid, res['data'].mid); + user.put(UserBoxKey.userLogin, true); + userLogin.value = true; + // Get.find().readuUserFace(); + } else { + resetUserInfo(); + } + } else { + resetUserInfo(); + // SmartDialog.showToast(res['msg']); + } + await queryUserStatOwner(); + return res; + } + + Future queryUserStatOwner() async { + var res = await UserHttp.userStatOwner(); + if (res['status']) { + userStat.value = res['data']; + } + return res; + } + + Future resetUserInfo() async { + userInfo.value = UserInfoData(); + userStat.value = UserStat(); + await user.delete(UserBoxKey.userName); + await user.delete(UserBoxKey.userFace); + await user.delete(UserBoxKey.userMid); + await user.delete(UserBoxKey.userLogin); + userLogin.value = false; + // Get.find().resetLast(); + } +} diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index fd455db0..e0a72d96 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -1,4 +1,9 @@ +import 'package:flutter/cupertino.dart'; 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 'controller.dart'; class MinePage extends StatefulWidget { const MinePage({super.key}); @@ -8,11 +13,352 @@ class MinePage extends StatefulWidget { } class _MinePageState extends State { + final MineController _mineController = Get.put(MineController()); + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('我的'), + automaticallyImplyLeading: false, + scrolledUnderElevation: 0, + elevation: 0, + toolbarHeight: kTextTabBarHeight + 20, + backgroundColor: Colors.transparent, + title: null, + actions: [ + IconButton( + onPressed: () { + Get.changeThemeMode(ThemeMode.dark); + }, + icon: Icon( + Get.theme == ThemeData.light() + ? CupertinoIcons.moon + : CupertinoIcons.sun_max, + size: 22, + ), + ), + IconButton( + onPressed: () => Get.toNamed('/setting'), + icon: const Icon( + CupertinoIcons.slider_horizontal_3, + ), + ), + const SizedBox(width: 10), + ], + ), + body: RefreshIndicator( + onRefresh: () async { + await _mineController.queryUserInfo(); + await _mineController.queryUserStatOwner(); + }, + child: LayoutBuilder( + builder: (context, constraint) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraint.maxHeight, + child: Column( + children: [ + const SizedBox(height: 10), + FutureBuilder( + future: _mineController.queryUserInfo(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + print(snapshot.data); + if (snapshot.data['status']) { + return Obx(() => userInfoBuild()); + } else { + return userInfoBuild(); + } + } else { + return userInfoBuild(); + } + }, + ), + const SizedBox(height: 20), + ], + ), + ), + ); + }, + ), + ), + ); + } + + Widget userInfoBuild() { + return Column( + children: [ + const SizedBox(height: 5), + GestureDetector( + onTap: () => _mineController.onLogin(), + child: ClipOval( + child: Container( + width: 85, + height: 85, + color: Theme.of(context).colorScheme.onInverseSurface, + child: Center( + child: _mineController.userInfo.value.face != null + ? NetworkImgLayer( + src: _mineController.userInfo.value.face, + width: 85, + height: 85) + : Image.asset('assets/images/loading.png'), + ), + ), + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _mineController.userInfo.value.uname ?? '点击头像登录', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(width: 4), + Image.asset( + 'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png', + height: 10, + ), + ], + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich(TextSpan(children: [ + TextSpan( + text: '硬币: ', + style: + TextStyle(color: Theme.of(context).colorScheme.outline)), + TextSpan( + text: (_mineController.userInfo.value.money ?? 'pilipala') + .toString(), + style: + TextStyle(color: Theme.of(context).colorScheme.primary)), + ])) + ], + ), + const SizedBox(height: 5), + if (_mineController.userInfo.value.levelInfo != null) ...[ + LayoutBuilder( + builder: (context, BoxConstraints box) { + return SizedBox( + width: box.maxWidth, + height: 24, + child: Stack( + children: [ + Positioned( + top: 0, + right: 0, + child: SizedBox( + height: 22, + width: box.maxWidth * + (1 - + (_mineController + .userInfo.value.levelInfo!.currentExp! / + _mineController + .userInfo.value.levelInfo!.nextExp!)), + child: Center( + child: Text( + (_mineController + .userInfo.value.levelInfo!.nextExp! - + _mineController + .userInfo.value.levelInfo!.currentExp!) + .toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 12, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ), + LayoutBuilder( + builder: (context, BoxConstraints box) { + return Container( + width: box.maxWidth, + height: 1, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context).colorScheme.onInverseSurface, + ), + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + bottom: 0, + child: Container( + width: box.maxWidth * + (_mineController + .userInfo.value.levelInfo!.currentExp! / + _mineController + .userInfo.value.levelInfo!.nextExp!), + height: 1, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ); + }, + ), + ], + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: LayoutBuilder( + builder: (context, constraints) { + TextStyle style = TextStyle( + fontSize: Theme.of(context).textTheme.titleMedium!.fontSize, + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold); + return SizedBox( + height: constraints.maxWidth / 3 * 0.6, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 3, + childAspectRatio: 1.67, + children: [ + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + (_mineController.userStat.value.dynamicCount ?? + '-') + .toString(), + key: ValueKey(_mineController + .userStat.value.dynamicCount + .toString()), + style: style), + ), + const SizedBox(height: 8), + Text( + '动态', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + (_mineController.userStat.value.following ?? + '-') + .toString(), + key: ValueKey(_mineController + .userStat.value.following + .toString()), + style: style), + ), + const SizedBox(height: 8), + Text( + '关注', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + (_mineController.userStat.value.follower ?? '-') + .toString(), + key: ValueKey(_mineController + .userStat.value.follower + .toString()), + style: style), + ), + const SizedBox(height: 8), + Text( + '粉丝', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } +} + +class ActionItem extends StatelessWidget { + Icon? icon; + Function? onTap; + String? text; + + ActionItem({ + Key? key, + this.icon, + this.onTap, + this.text, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon!.icon!), + const SizedBox(height: 8), + Text( + text!, + style: Theme.of(context).textTheme.labelMedium, + ), + ], ), ); } diff --git a/lib/pages/preview/controller.dart b/lib/pages/preview/controller.dart new file mode 100644 index 00000000..fd0d4472 --- /dev/null +++ b/lib/pages/preview/controller.dart @@ -0,0 +1,72 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:get/get.dart'; +import 'dart:typed_data'; +import 'package:dio/dio.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:share_plus/share_plus.dart'; + +class PreviewController extends GetxController { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + RxInt initialPage = 0.obs; + RxInt currentPage = 1.obs; + RxList imgList = [].obs; + bool storage = true; + bool videos = true; + bool photos = true; + bool visiable = true; + + @override + void onInit() { + super.onInit(); + if (Get.arguments != null) { + initialPage.value = Get.arguments['initialPage']!; + currentPage.value = Get.arguments['initialPage']! + 1; + imgList.value = Get.arguments['imgList']; + } + } + + requestPermission() async { + Map statuses = await [ + Permission.storage, + // Permission.photos + ].request(); + + final info = statuses[Permission.storage].toString(); + // final photosInfo = statuses[Permission.photos].toString(); + + print('授权状态:$info'); + } + + // 图片保存 + void onSaveImg() async { + var response = await Dio().get(imgList[initialPage.value], + options: Options(responseType: ResponseType.bytes)); + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 100, + name: "pic_vvex${DateTime.now().toString().split('-').join()}"); + if (result != null) { + if (result['isSuccess']) { + print('已保存到相册'); + } + } + } + + // 图片分享 + void onShareImg() async { + requestPermission(); + var response = await Dio().get(imgList[initialPage.value], + options: Options(responseType: ResponseType.bytes)); + final temp = await getTemporaryDirectory(); + String imgName = + "pic_vvex${DateTime.now().toString().split('-').join()}.jpg"; + var path = '${temp.path}/$imgName'; + File(path).writeAsBytesSync(response.data); + Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]); + } +} diff --git a/lib/pages/preview/index.dart b/lib/pages/preview/index.dart new file mode 100644 index 00000000..9fb82e8d --- /dev/null +++ b/lib/pages/preview/index.dart @@ -0,0 +1,4 @@ +library preview; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart new file mode 100644 index 00000000..2ff9bd73 --- /dev/null +++ b/lib/pages/preview/view.dart @@ -0,0 +1,183 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:pilipala/common/widgets/appbar.dart'; +import 'controller.dart'; + +typedef DoubleClickAnimationListener = void Function(); + +class ImagePreview extends StatefulWidget { + const ImagePreview({Key? key}) : super(key: key); + + @override + _ImagePreviewState createState() => _ImagePreviewState(); +} + +class _ImagePreviewState extends State + with TickerProviderStateMixin { + final PreviewController _previewController = Get.put(PreviewController()); + late AnimationController animationController; + late AnimationController _doubleClickAnimationController; + Animation? _doubleClickAnimation; + late DoubleClickAnimationListener _doubleClickAnimationListener; + List doubleTapScales = [1.0, 2.0]; + + @override + void initState() { + super.initState(); + animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 400)); + _doubleClickAnimationController = AnimationController( + duration: const Duration(milliseconds: 250), vsync: this); + } + + @override + void dispose() { + animationController.dispose(); + _doubleClickAnimationController.dispose(); + clearGestureDetailsCache(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBarWidget( + controller: animationController, + visible: _previewController.visiable, + child: AppBar( + backgroundColor: Theme.of(context).colorScheme.background, + elevation: 0, + centerTitle: false, + title: Obx( + () => Text.rich( + TextSpan(children: [ + TextSpan(text: _previewController.currentPage.toString()), + const TextSpan(text: ' / '), + TextSpan(text: _previewController.imgList.length.toString()), + ]), + ), + ), + actions: [ + PopupMenuButton( + icon: const Icon(Icons.more_vert), + tooltip: 'action', + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: 'share', + onTap: _previewController.onShareImg, + child: const Text('分享'), + ), + PopupMenuItem( + value: 'save', + onTap: _previewController.onSaveImg, + child: const Text('保存'), + ), + ], + ), + ], + ), + ), + body: GestureDetector( + onTap: () { + _previewController.visiable = !_previewController.visiable; + setState(() {}); + }, + child: ExtendedImageGesturePageView.builder( + controller: ExtendedPageController( + initialPage: _previewController.initialPage.value, + pageSpacing: 0, + ), + onPageChanged: (int index) { + _previewController.initialPage.value = index; + _previewController.currentPage.value = index + 1; + }, + canScrollPage: (GestureDetails? gestureDetails) => + gestureDetails!.totalScale! <= 1.0, + preloadPagesCount: 2, + itemCount: _previewController.imgList.length, + itemBuilder: (BuildContext context, int index) { + return ExtendedImage.network( + _previewController.imgList[index], + fit: BoxFit.contain, + mode: ExtendedImageMode.gesture, + onDoubleTap: (ExtendedImageGestureState state) { + final Offset? pointerDownPosition = state.pointerDownPosition; + final double? begin = state.gestureDetails!.totalScale; + double end; + + //remove old + _doubleClickAnimation + ?.removeListener(_doubleClickAnimationListener); + + //stop pre + _doubleClickAnimationController.stop(); + + //reset to use + _doubleClickAnimationController.reset(); + + if (begin == doubleTapScales[0]) { + end = doubleTapScales[1]; + } else { + end = doubleTapScales[0]; + } + + _doubleClickAnimationListener = () { + state.handleDoubleTap( + scale: _doubleClickAnimation!.value, + doubleTapPosition: pointerDownPosition); + }; + _doubleClickAnimation = _doubleClickAnimationController + .drive(Tween(begin: begin, end: end)); + + _doubleClickAnimation! + .addListener(_doubleClickAnimationListener); + + _doubleClickAnimationController.forward(); + }, + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.loading) { + final ImageChunkEvent? loadingProgress = + state.loadingProgress; + final double? progress = + loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 150.0, + child: LinearProgressIndicator(value: progress), + ), + const SizedBox(height: 10.0), + Text('${((progress ?? 0.0) * 100).toInt()}%'), + ], + ), + ); + } + }, + initGestureConfigHandler: (ExtendedImageState state) { + return GestureConfig( + inPageView: true, + initialScale: 1.0, + maxScale: 5.0, + animationMaxScale: 6.0, + initialAlignment: InitialAlignment.center, + ); + }, + ); + }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _previewController.onSaveImg(), + child: const Icon(Icons.save_alt_rounded), + ), + ); + } +} diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart new file mode 100644 index 00000000..af6b6026 --- /dev/null +++ b/lib/pages/setting/controller.dart @@ -0,0 +1,23 @@ +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/pages/mine/controller.dart'; +import 'package:pilipala/utils/storage.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +class SettingController extends GetxController { + Box user = GStrorage.user; + RxBool userLogin = false.obs; + + @override + void onInit() { + super.onInit(); + userLogin.value = user.get(UserBoxKey.userLogin) ?? false; + } + + loginOut() async { + await Request.removeCookie(); + await Get.find().resetUserInfo(); + userLogin.value = user.get(UserBoxKey.userLogin) ?? false; + } +} diff --git a/lib/pages/setting/index.dart b/lib/pages/setting/index.dart new file mode 100644 index 00000000..30fa06b3 --- /dev/null +++ b/lib/pages/setting/index.dart @@ -0,0 +1,4 @@ +library setting; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart new file mode 100644 index 00000000..63321afa --- /dev/null +++ b/lib/pages/setting/view.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/setting/index.dart'; + +class SettingPage extends StatefulWidget { + const SettingPage({super.key}); + + @override + State createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + final SettingController _settingController = Get.put(SettingController()); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('设置'), + ), + body: Column( + children: [ + Obx( + () => Visibility( + visible: _settingController.userLogin.value, + child: ListTile( + onTap: () => _settingController.loginOut(), + dense: false, + title: const Text('退出登录'), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index e69de29b..744c6c74 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -0,0 +1,36 @@ +import 'package:get/get.dart'; + +class VideoDetailController extends GetxController { + int tabInitialIndex = 0; + // tabs + RxList tabs = ['简介', '评论'].obs; + + // 视频aid + String aid = Get.parameters['aid']!; + + // 是否预渲染 骨架屏 + bool preRender = false; + + // 视频详情 上个页面传入 + Map videoItem = {}; + + // 请求状态 + RxBool isLoading = false.obs; + + String heroTag = ''; + + @override + void onInit() { + super.onInit(); + if (Get.arguments.isNotEmpty) { + if (Get.arguments.containsKey('videoItem')) { + preRender = true; + var args = Get.arguments['videoItem']; + if (args.pic != null && args.pic != '') { + videoItem['pic'] = args.pic; + } + } + heroTag = Get.arguments['heroTag']; + } + } +} diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart new file mode 100644 index 00000000..faf7cfb6 --- /dev/null +++ b/lib/pages/video/detail/introduction/controller.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/controller.dart'; +import 'package:pilipala/utils/storage.dart'; + +class VideoIntroController extends GetxController { + // 视频aid + String aid = Get.parameters['aid']!; + + // 是否预渲染 骨架屏 + bool preRender = false; + + // 视频详情 上个页面传入 + Map? videoItem = {}; + + // 请求状态 + RxBool isLoading = false.obs; + + // 视频详情 请求返回 + Rx videoDetail = VideoDetailData().obs; + + // 请求返回的信息 + String responseMsg = '请求异常'; + + // up主粉丝数 + Map userStat = {'follower': '-'}; + + // 是否点赞 + RxBool hasLike = false.obs; + // 是否投币 + RxBool hasCoin = false.obs; + // 是否收藏 + RxBool hasFav = false.obs; + Box user = GStrorage.user; + bool userLogin = false; + Rx favFolderData = FavFolderData().obs; + List addMediaIdsNew = []; + List delMediaIdsNew = []; + + @override + void onInit() { + super.onInit(); + if (Get.arguments.isNotEmpty) { + if (Get.arguments.containsKey('videoItem')) { + preRender = true; + var args = Get.arguments['videoItem']; + videoItem!['pic'] = args.pic; + videoItem!['title'] = args.title; + if (args.stat != null) { + videoItem!['stat'] = args.stat; + } + videoItem!['pubdate'] = args.pubdate; + videoItem!['owner'] = args.owner; + } + } + userLogin = user.get(UserBoxKey.userLogin) != null; + } + + // 获取视频简介 + Future queryVideoIntro() async { + var result = await VideoHttp.videoIntro(aid: aid); + if (result['status']) { + videoDetail.value = result['data']!; + Get.find(tag: Get.arguments['heroTag']) + .tabs + .value = ['简介', '评论 ${result['data']!.stat!.reply}']; + } else { + responseMsg = result['msg']; + } + // 获取到粉丝数再返回 + await queryUserStat(); + if (userLogin) { + // 获取点赞状态 + queryHasLikeVideo(); + // 获取投币状态 + queryHasCoinVideo(); + // 获取收藏状态 + queryHasFavVideo(); + } + + return result; + } + + // 获取up主粉丝数 + Future queryUserStat() async { + var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!); + if (result['status']) { + userStat = result['data']; + } + } + + // 获取点赞状态 + Future queryHasLikeVideo() async { + var result = await VideoHttp.hasLikeVideo(aid: aid); + // data num 被点赞标志 0:未点赞 1:已点赞 + hasLike.value = result["data"] == 1 ? true : false; + } + + // 获取投币状态 + Future queryHasCoinVideo() async { + var result = await VideoHttp.hasCoinVideo(aid: aid); + hasCoin.value = result["data"]['multiply'] == 0 ? false : true; + } + + // 获取收藏状态 + Future queryHasFavVideo() async { + var result = await VideoHttp.hasFavVideo(aid: aid); + hasFav.value = result["data"]['favoured']; + } + + // 一键三连 + Future actionOneThree() async { + if (hasLike.value && hasCoin.value && hasFav.value) { + // 已点赞、投币、收藏 + SmartDialog.showToast('🙏 UP已经收到了~'); + return false; + } + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: const Text('一键三连 给UP送温暖'), + actions: [ + TextButton( + onPressed: () => SmartDialog.dismiss(), + child: const Text('点错了')), + TextButton( + onPressed: () async { + var result = await VideoHttp.oneThree(aid: aid); + if (result['status']) { + hasLike.value = result["data"]["like"]; + hasCoin.value = result["data"]["coin"]; + hasFav.value = result["data"]["fav"]; + SmartDialog.showToast('三连成功 🎉'); + } else { + SmartDialog.showToast(result['msg']); + } + SmartDialog.dismiss(); + }, + child: const Text('确认'), + ) + ], + ); + }, + ); + } + + // (取消)点赞 + Future actionLikeVideo() async { + var result = await VideoHttp.likeVideo(aid: aid, type: !hasLike.value); + if (result['status']) { + hasLike.value = result["data"] == 1 ? true : false; + if (hasLike.value) { + SmartDialog.showToast('已点赞 👍'); + } else { + SmartDialog.showToast('取消赞'); + } + } else { + SmartDialog.showToast(result['msg']); + } + } + + // 投币 + Future actionCoinVideo() async { + print('投币'); + } + + // (取消)收藏 + Future actionFavVideo() async { + try { + for (var i in favFolderData.value.list!.toList()) { + if (i.favState == 1) { + addMediaIdsNew.add(i.id); + } else { + delMediaIdsNew.add(i.id); + } + } + } catch (e) {} + var result = await VideoHttp.favVideo( + aid: aid, + addIds: addMediaIdsNew.join(','), + delIds: delMediaIdsNew.join(',')); + if (result['status']) { + if (result['data']['prompt']) { + addMediaIdsNew = []; + delMediaIdsNew = []; + Get.back(); + // 重新获取收藏状态 + queryHasFavVideo(); + SmartDialog.showToast('✅ 操作成功'); + } + } + } + + // 分享视频 + Future actionShareVideo() async { + print('分享视频'); + } + + Future queryVideoInFolder() async { + var result = await VideoHttp.videoInFolder( + mid: user.get(UserBoxKey.userMid), rid: aid); + if (result['status']) { + favFolderData.value = result['data']; + } + return result; + } + + // 选择文件夹 + onChoose(bool checkValue, int index) { + List datalist = favFolderData.value.list!; + for (var i = 0; i < datalist.length; i++) { + if (i == index) { + datalist[i].favState = checkValue == true ? 1 : 0; + datalist[i].mediaCount = checkValue == true + ? datalist[i].mediaCount! + 1 + : datalist[i].mediaCount! - 1; + } + } + favFolderData.value.list = datalist; + favFolderData.refresh(); + } +} diff --git a/lib/pages/video/detail/introduction/index.dart b/lib/pages/video/detail/introduction/index.dart new file mode 100644 index 00000000..5eaae572 --- /dev/null +++ b/lib/pages/video/detail/introduction/index.dart @@ -0,0 +1,4 @@ +library video_intro_panel; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart new file mode 100644 index 00000000..843126ed --- /dev/null +++ b/lib/pages/video/detail/introduction/view.dart @@ -0,0 +1,541 @@ +import 'package:flutter/cupertino.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/fav/index.dart'; +import 'package:pilipala/pages/favDetail/index.dart'; +import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/common/widgets/stat/danmu.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/introduction/controller.dart'; +import 'package:pilipala/utils/utils.dart'; + +class VideoIntroPanel extends StatefulWidget { + const VideoIntroPanel({Key? key}) : super(key: key); + + @override + State createState() => _VideoIntroPanelState(); +} + +class _VideoIntroPanelState extends State + with AutomaticKeepAliveClientMixin { + final VideoIntroController videoIntroController = + Get.put(VideoIntroController(), tag: Get.arguments['heroTag']); + VideoDetailData? videoDetail; + + // 添加页面缓存 + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + videoIntroController.videoDetail.listen((value) { + videoDetail = value; + }); + } + + @override + void dispose() { + videoIntroController.onClose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: videoIntroController.queryVideoIntro(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data['status']) { + // 请求成功 + // return _buildView(context, false, videoDetail); + return VideoInfo(loadingStatus: false, videoDetail: videoDetail); + } else { + // 请求错误 + return HttpError( + errMsg: snapshot.data['msg'], + fn: () => setState(() {}), + ); + } + } else { + return VideoInfo(loadingStatus: true, videoDetail: videoDetail); + } + }, + ); + } +} + +class VideoInfo extends StatefulWidget { + bool loadingStatus = false; + VideoDetailData? videoDetail; + + VideoInfo({Key? key, required this.loadingStatus, this.videoDetail}) + : super(key: key); + + @override + State createState() => _VideoInfoState(); +} + +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; + + /// 手动控制动画的控制器 + late AnimationController? _manualController; + + /// 手动控制 + late Animation? _manualAnimation; + + final FavController _favController = Get.put(FavController()); + + @override + void initState() { + super.initState(); + + /// 不设置重复,使用代码控制进度,动画时间1秒 + _manualController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 400), + ); + _manualAnimation = + Tween(begin: 0.5, end: 1.5).animate(_manualController!); + } + + showFavBottomSheet() { + Get.bottomSheet( + useRootNavigator: true, + isScrollControlled: true, + Container( + height: 450, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + AppBar( + toolbarHeight: 50, + automaticallyImplyLeading: false, + centerTitle: false, + elevation: 1, + title: Text( + '选择文件夹', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: [ + TextButton( + onPressed: () => videoIntroController.actionFavVideo(), + child: const Text('完成'), + ), + const SizedBox(width: 6), + ], + ), + Expanded( + child: Material( + child: FutureBuilder( + future: videoIntroController.queryVideoInFolder(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => ListView.builder( + itemCount: videoIntroController + .favFolderData.value.list!.length + + 1, + itemBuilder: (context, index) { + if (index == 0) { + return const SizedBox(height: 10); + } else { + return ListTile( + onTap: () => videoIntroController.onChoose( + videoIntroController.favFolderData.value + .list![index - 1].favState != + 1, + index - 1), + dense: true, + leading: + const Icon(Icons.folder_special_outlined), + minLeadingWidth: 0, + title: Text(videoIntroController.favFolderData + .value.list![index - 1].title!), + subtitle: Text( + '${videoIntroController.favFolderData.value.list![index - 1].mediaCount}个内容', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ), + trailing: Transform.scale( + scale: 0.9, + child: Checkbox( + value: videoIntroController + .favFolderData + .value + .list![index - 1] + .favState == + 1, + onChanged: (bool? checkValue) => + videoIntroController.onChoose( + checkValue!, index - 1), + ), + ), + ); + } + }, + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return Text('请求中'); + } + }, + ), + ), + ), + ], + ), + ), + persistent: false, + backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor, + ); + } + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.only(left: 12, right: 12, top: 20), + sliver: SliverToBoxAdapter( + child: !widget.loadingStatus || videoItem.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableRegion( + magnifierConfiguration: const TextMagnifierConfiguration(), + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), + child: Text( + !widget.loadingStatus + ? widget.videoDetail!.title + : videoItem['title'], + style: Theme.of(context).textTheme.titleMedium!.copyWith( + letterSpacing: 0.5, + ), + ), + ), + InkWell( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: () { + _manualController!.animateTo(isExpand ? 0 : 0.5); + setState(() { + isExpand = !isExpand; + }); + }, + child: Row( + children: [ + const SizedBox(width: 2), + StatView( + theme: 'gray', + view: !widget.loadingStatus + ? widget.videoDetail!.stat!.view + : videoItem['stat'].view, + size: 'medium', + ), + const SizedBox(width: 10), + StatDanMu( + theme: 'gray', + danmu: !widget.loadingStatus + ? widget.videoDetail!.stat!.danmaku + : videoItem['stat'].danmaku, + size: 'medium', + ), + const SizedBox(width: 10), + Text( + Utils.dateFormat( + !widget.loadingStatus + ? widget.videoDetail!.pubdate + : videoItem['pubdate'], + formatType: 'detail'), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline), + ), + const Spacer(), + RotationTransition( + turns: _manualAnimation!, + child: SizedBox( + width: 35, + height: 35, + child: IconButton( + padding: const EdgeInsets.all(2.0), + onPressed: () { + /// 0.5代表 180弧度 + _manualController! + .animateTo(isExpand ? 0 : 0.5); + setState(() { + isExpand = !isExpand; + }); + }, + icon: Icon( + FontAwesomeIcons.angleUp, + size: 15, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ), + const SizedBox(width: 10), + ], + ), + ), + // 简介 默认收起 + if (!widget.loadingStatus) + ExpandedSection( + expand: isExpand, + begin: 0.0, + end: 1.0, + child: DefaultTextStyle( + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + height: 1.5, + fontSize: + Theme.of(context).textTheme.labelMedium?.fontSize, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: SelectableRegion( + magnifierConfiguration: + const TextMagnifierConfiguration(), + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.videoDetail!.bvid!), + Text(widget.videoDetail!.desc!), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 8), + _actionGrid(context, videoIntroController), + Divider( + height: 26, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + Row( + children: [ + NetworkImgLayer( + type: 'avatar', + src: !widget.loadingStatus + ? widget.videoDetail!.owner!.face + : videoItem['owner'].face, + width: 38, + height: 38, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(!widget.loadingStatus + ? widget.videoDetail!.owner!.name + : videoItem['owner'].name), + // const SizedBox(width: 10), + Text( + widget.loadingStatus + ? '- 粉丝' + : '${Utils.numFormat(videoIntroController.userStat['follower'])}粉丝', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ), + ], + ), + const Spacer(), + AnimatedOpacity( + opacity: widget.loadingStatus ? 0 : 1, + duration: const Duration(milliseconds: 150), + child: SizedBox( + height: 36, + child: ElevatedButton( + onPressed: () {}, + child: const Text('关注'), + ), + ), + ), + ], + ), + Divider( + height: 26, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + // const SizedBox(height: 10), + ], + ) + : const Center(child: CircularProgressIndicator()), + ), + ); + } + + // 喜欢 投币 分享 + Widget _actionGrid(BuildContext context, videoIntroController) { + return LayoutBuilder(builder: (context, constraints) { + return SizedBox( + height: constraints.maxWidth / 5 * 0.8, + child: GridView.count( + primary: false, + padding: const EdgeInsets.all(0), + crossAxisCount: 5, + childAspectRatio: 1.25, + children: [ + // ActionItem( + // icon: const Icon(FontAwesomeIcons.s), + // selectIcon: const Icon(FontAwesomeIcons.s), + // onTap: () => {}, + // selectStatus: true, + // loadingStatus: false, + // text: '三连', + // ), + // Column( + // children: [], + // ), + InkWell( + onTap: () => videoIntroController.actionOneThree(), + borderRadius: StyleString.mdRadius, + child: Padding( + padding: const EdgeInsets.all(12), + child: Image.asset( + 'assets/images/logo/logo_big.png', + width: 10, + height: 10, + ), + ), + ), + 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.thumbsDown), + // selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), + // onTap: () => {}, + // 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.star), + 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() + : '-'), + ], + ), + ); + }); + } +} + +class ActionItem extends StatelessWidget { + Icon? icon; + Icon? selectIcon; + Function? onTap; + bool? loadingStatus; + String? text; + bool selectStatus = false; + + ActionItem({ + Key? key, + this.icon, + this.selectIcon, + this.onTap, + this.loadingStatus, + this.text, + required this.selectStatus, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => onTap!(), + borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 4), + selectStatus + ? Icon(selectIcon!.icon!, + size: 21, color: Theme.of(context).primaryColor) + : Icon(icon!.icon!, + size: 21, color: Theme.of(context).colorScheme.outline), + const SizedBox(height: 4), + AnimatedOpacity( + opacity: loadingStatus! ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: Text( + text ?? '', + style: TextStyle( + color: selectStatus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context).textTheme.labelSmall?.fontSize), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/video/detail/player/controller.dart b/lib/pages/video/detail/player/controller.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/pages/video/detail/player/index.dart b/lib/pages/video/detail/player/index.dart new file mode 100644 index 00000000..c5d4ff78 --- /dev/null +++ b/lib/pages/video/detail/player/index.dart @@ -0,0 +1,4 @@ +library video_player; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/player/view.dart b/lib/pages/video/detail/player/view.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/pages/video/detail/related/controller.dart b/lib/pages/video/detail/related/controller.dart new file mode 100644 index 00000000..cb9081dc --- /dev/null +++ b/lib/pages/video/detail/related/controller.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/video.dart'; + +class ReleatedController extends GetxController { + // 视频aid + String aid = Get.parameters['aid']!; + // 推荐视频列表 + List relatedVideoList = []; + + Future queryRelatedVideo() => VideoHttp.relatedVideoList(aid: aid); +} diff --git a/lib/pages/video/detail/related/index.dart b/lib/pages/video/detail/related/index.dart new file mode 100644 index 00000000..ce29b10a --- /dev/null +++ b/lib/pages/video/detail/related/index.dart @@ -0,0 +1,4 @@ +library releated_video_panel; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart new file mode 100644 index 00000000..3c79ee50 --- /dev/null +++ b/lib/pages/video/detail/related/view.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/video_card_h.dart'; +import 'package:pilipala/common/widgets/video_card_v.dart'; +import './controller.dart'; + +class RelatedVideoPanel extends StatefulWidget { + const RelatedVideoPanel({super.key}); + + @override + State createState() => _RelatedVideoPanelState(); +} + +class _RelatedVideoPanelState extends State { + final ReleatedController _releatedController = + Get.put(ReleatedController(), tag: Get.arguments['heroTag']); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _releatedController.queryRelatedVideo(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data!['status']) { + // 请求成功 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + if (index == snapshot.data['data'].length) { + return SizedBox(height: MediaQuery.of(context).padding.bottom); + } else { + return VideoCardH( + videoItem: snapshot.data['data'][index], + ); + } + }, childCount: snapshot.data['data'].length + 1)); + } else { + // 请求错误 + return const Center( + child: Text('出错了'), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), + ); + } + }, + ); + } +} diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart new file mode 100644 index 00000000..267b0354 --- /dev/null +++ b/lib/pages/video/detail/reply/controller.dart @@ -0,0 +1,117 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/reply.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/common/reply_type.dart'; +import 'package:pilipala/models/video/reply/data.dart'; +import 'package:pilipala/models/video/reply/item.dart'; + +class VideoReplyController extends GetxController { + VideoReplyController( + this.aid, + this.rpid, + this.level + ); + final ScrollController scrollController = ScrollController(); + // 视频aid 请求时使用的oid + String? aid; + // 层级 2为楼中楼 + String? level; + // rpid 请求楼中楼回复 + String? rpid; + RxList replyList = [ReplyItemModel()].obs; + // 当前页 + int currentPage = 0; + bool isLoadingMore = false; + RxBool noMore = false.obs; + RxBool autoFocus = false.obs; + // 当前回复的回复 + ReplyItemModel? currentReplyItem; + // 回复来源 + String replySource = 'main'; + // 根评论 id 回复楼中楼回复使用 + int? rPid; + // 默认回复主楼 + String replyLevel = '0'; + + Future queryReplyList({type = 'init'}) async { + isLoadingMore = true; + var res = level == '1' + ? await ReplyHttp.replyList( + oid: aid!, pageNum: currentPage + 1, type: 1) + : await ReplyHttp.replyReplyList( + oid: aid!, root: rpid!, pageNum: currentPage + 1, type: 1); + if (res['status']) { + res['data'] = ReplyData.fromJson(res['data']); + if (res['data'].replies.isNotEmpty) { + currentPage = currentPage + 1; + noMore.value = false; + } else { + if (currentPage == 0) { + } else { + noMore.value = true; + return; + } + } + if (res['data'].replies.length >= res['data'].page.count) { + noMore.value = true; + } + if (type == 'init') { + List replies = res['data'].replies; + // 添加置顶回复 + if (res['data'].upper.top != null) { + bool flag = false; + for (var i = 0; i < res['data'].topReplies.length; i++) { + if (res['data'].topReplies[i].rpid == res['data'].upper.top.rpid) { + flag = true; + } + } + if (!flag) { + replies.insert(0, res['data'].upper.top); + } + } + replies.insertAll(0, res['data'].topReplies); + res['data'].replies = replies; + replyList.value = res['data'].replies!; + } else { + replyList.addAll(res['data'].replies!); + res['data'].replies.addAll(replyList); + } + } + isLoadingMore = false; + return res; + } + + // 上拉加载 + Future onLoad() async { + queryReplyList(type: 'onLoad'); + } + + wakeUpReply() { + autoFocus.value = true; + } + + // 发表评论 + Future submitReplyAdd() async { + print('replyLevel: $replyLevel'); + // print('rpid: $rpid'); + // print('currentReplyItem!.rpid: ${currentReplyItem!.rpid}'); + + + var result = await VideoHttp.replyAdd( + type: ReplyType.video, + oid: int.parse(aid!), + root: replyLevel == '0' ? 0 : replyLevel == '1' ? currentReplyItem!.rpid : rPid, + parent: replyLevel == '0' ? 0 : replyLevel == '1' ? currentReplyItem!.rpid : currentReplyItem!.rpid, + message: replyLevel == '2' ? ' 回复 @${currentReplyItem!.member!.uname!} : 2楼31' : '2楼31', + ); + if(result['status']){ + SmartDialog.showToast(result['data']['success_toast']); + }else{ + SmartDialog.showToast(result['message']); + } + } +} diff --git a/lib/pages/video/detail/reply/index.dart b/lib/pages/video/detail/reply/index.dart new file mode 100644 index 00000000..f3d25ba4 --- /dev/null +++ b/lib/pages/video/detail/reply/index.dart @@ -0,0 +1,4 @@ +library video_reply_panel; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart new file mode 100644 index 00000000..89aa090f --- /dev/null +++ b/lib/pages/video/detail/reply/view.dart @@ -0,0 +1,280 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/skeleton/video_reply.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/models/video/reply/item.dart'; +import 'controller.dart'; +import 'widgets/reply_item.dart'; + +class VideoReplyPanel extends StatefulWidget { + int oid; + int rpid; + String? level; + VideoReplyPanel({ + this.oid = 0, + this.rpid = 0, + this.level, + super.key, + }); + + @override + State createState() => _VideoReplyPanelState(); +} + +class _VideoReplyPanelState extends State + with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { + late VideoReplyController _videoReplyController; + late AnimationController fabAnimationCtr; + late AnimationController replyAnimationCtl; + + // List? replyList; + Future? _futureBuilderFuture; + bool _isFabVisible = true; + String replyLevel = '1'; + + // 添加页面缓存 + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + replyLevel = widget.level ?? '1'; + if (widget.level != null && widget.level == '2') { + _videoReplyController = Get.put( + VideoReplyController( + widget.oid.toString(), widget.rpid.toString(), '2'), + tag: widget.rpid.toString()); + _videoReplyController.rPid = widget.rpid; + } else { + _videoReplyController = Get.put( + VideoReplyController(Get.parameters['aid']!, '', '1'), + tag: Get.arguments['heroTag']); + } + // if(replyLevel != ''){ + // _videoReplyController.replyLevel = replyLevel; + // } + print( + '_videoReplyController.replyLevel: ${_videoReplyController.replyLevel}'); + + fabAnimationCtr = AnimationController( + vsync: this, duration: const Duration(milliseconds: 300)); + replyAnimationCtl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 500)); + + _futureBuilderFuture = _videoReplyController.queryReplyList(); + _videoReplyController.scrollController.addListener( + () { + if (_videoReplyController.scrollController.position.pixels >= + _videoReplyController.scrollController.position.maxScrollExtent - + 300) { + if (!_videoReplyController.isLoadingMore) { + _videoReplyController.onLoad(); + } + } + + final ScrollDirection direction = + _videoReplyController.scrollController.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + _showFab(); + } else if (direction == ScrollDirection.reverse) { + _hideFab(); + } + }, + ); + } + + void _showFab() { + if (!_isFabVisible) { + _isFabVisible = true; + fabAnimationCtr.forward(); + } + } + + void _hideFab() { + if (_isFabVisible) { + _isFabVisible = false; + fabAnimationCtr.reverse(); + } + } + + void _showReply(source, {ReplyItemModel? replyItem, replyLevel}) async { + // source main 直接回复 floor 楼中楼回复 + if (source == 'floor') { + _videoReplyController.currentReplyItem = replyItem; + _videoReplyController.replySource = source; + _videoReplyController.replyLevel = replyLevel ?? '1'; + } else { + _videoReplyController.replyLevel = '0'; + } + + replyAnimationCtl.forward(); + await Future.delayed(const Duration(microseconds: 100)); + _videoReplyController.wakeUpReply(); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + fabAnimationCtr.dispose(); + _videoReplyController.scrollController.dispose(); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + setState(() {}); + _videoReplyController.currentPage = 0; + return await _videoReplyController.queryReplyList(); + }, + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + CustomScrollView( + controller: _videoReplyController.scrollController, + key: const PageStorageKey('评论'), + slivers: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + // 请求成功 + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == + _videoReplyController.replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context) + .padding + .bottom), + height: + MediaQuery.of(context).padding.bottom + + 60, + child: Center( + child: Obx(() => Text( + _videoReplyController.noMore.value + ? '没有更多了' + : '加载中')), + ), + ); + } else { + return ReplyItem( + replyItem: _videoReplyController + .replyList[index], + weakUpReply: (replyItem, replyLevel) => + _showReply( + 'floor', + replyItem: replyItem, + replyLevel: replyLevel, + ), + replyLevel: replyLevel); + } + }, + childCount: + _videoReplyController.replyList.length + 1, + ), + ), + ); + } else { + // 请求错误 + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoReplySkeleton(); + }, childCount: 5), + ); + } + }, + ) + ], + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 14, + right: 14, + child: SlideTransition( + position: Tween( + // begin: const Offset(0, 2), + // 评论内容为空/不足一屏 + begin: const Offset(0, 0), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: fabAnimationCtr, + curve: Curves.easeInOut, + )), + child: FloatingActionButton( + heroTag: null, + onPressed: () => _showReply('main'), + tooltip: '发表评论', + child: const Icon(Icons.reply), + ), + ), + ), + Obx( + () => Positioned( + bottom: 0, + left: 0, + right: 0, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 2), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: replyAnimationCtl, + curve: Curves.easeInOut, + )), + child: Container( + height: 100 + MediaQuery.of(context).padding.bottom, + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom), + color: Theme.of(context).colorScheme.surfaceVariant, + child: Padding( + padding: const EdgeInsets.only(left: 14, right: 14), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: _videoReplyController.autoFocus.value, + child: const TextField( + autofocus: true, + maxLines: null, + decoration: InputDecoration( + hintText: "友善评论", border: InputBorder.none), + ), + ), + TextButton( + onPressed: () => + _videoReplyController.submitReplyAdd(), + child: const Text('发送'), + ) + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart new file mode 100644 index 00000000..d063cd5a --- /dev/null +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -0,0 +1,689 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/pages/video/detail/reply/index.dart'; +import 'package:pilipala/utils/utils.dart'; + +class ReplyItem extends StatelessWidget { + ReplyItem({super.key, this.replyItem, this.weakUpReply, this.replyLevel}); + ReplyItemModel? replyItem; + Function? weakUpReply; + String? replyLevel; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 8, 0), + child: content(context), + ), + // Divider( + // height: 1, + // indent: 52, + // endIndent: 10, + // color: Theme.of(context).dividerColor.withOpacity(0.08), + // ) + ], + ), + ); + } + + Widget lfAvtar(context) { + return Container( + margin: const EdgeInsets.only(top: 5), + child: Stack( + children: [ + NetworkImgLayer( + src: replyItem!.member!.avatar, + width: 34, + height: 34, + type: 'avatar', + ), + if (replyItem!.member!.officialVerify != null && + replyItem!.member!.officialVerify!['type'] == 0) + Positioned( + right: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + color: Theme.of(context).colorScheme.background, + ), + child: Icon( + Icons.offline_bolt, + color: Theme.of(context).colorScheme.primary, + size: 16, + ), + ), + ), + ], + ) + // child: + // NetworkImgLayer( + // src: replyItem!.member!.avatar, + // width: 30, + // height: 30, + // type: 'avatar', + // ), + ); + } + + Widget content(context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 头像、昵称 + GestureDetector( + // onTap: () => + // Get.toNamed('/member/${reply.userName}', parameters: { + // 'memberAvatar': reply.avatar, + // 'heroTag': reply.userName + heroTag, + // }), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + image: replyItem!.member!.userSailing!.cardbg != null + ? DecorationImage( + fit: BoxFit.cover, + image: NetworkImage( + replyItem!.member!.userSailing!.cardbg!['image'], + ), + ) + : null, + ), + child: Stack( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + lfAvtar(context), + const SizedBox(width: 12), + Text( + replyItem!.member!.uname!, + style: TextStyle( + color: replyItem!.isUp! || + replyItem!.member!.vip!['vipType'] > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${replyItem!.member!.level}.png', + height: 11, + ), + const SizedBox(width: 6), + if (replyItem!.isUp!) UpTag(), + ], + ), + if (replyItem!.member!.userSailing!.cardbg != null && + replyItem!.member!.userSailing!.cardbg!['fan']['number'] > + 0) + Positioned( + top: 8, + left: Get.size.width / 7 * 5.6, + child: DefaultTextStyle( + style: TextStyle( + fontFamily: 'fansCard', + fontSize: 9, + color: Theme.of(context).colorScheme.primary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('NO.'), + Text( + replyItem!.member!.userSailing!.cardbg!['fan'] + ['num_desc'], + ), + ], + ), + ), + ), + ], + ), + ), + ), + // title + Container( + margin: const EdgeInsets.only(top: 0, left: 45, right: 6, bottom: 6), + child: SelectableRegion( + magnifierConfiguration: const TextMagnifierConfiguration(), + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), + child: Text.rich( + style: const TextStyle(height: 1.65), + TextSpan( + children: [ + if (replyItem!.isTop!) + WidgetSpan(child: UpTag(tagText: 'TOP')), + buildContent(context, replyItem!.content!), + ], + ), + ), + ), + ), + // 操作区域 + bottonAction(context, replyItem!.replyControl), + const SizedBox(height: 3), + if (replyItem!.replies!.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 12), + child: ReplyItemRow( + replies: replyItem!.replies, + replyControl: replyItem!.replyControl, + f_rpid: replyItem!.rpid, + ), + ), + ], + ], + ); + } + + // 感谢、回复、复制 + Widget bottonAction(context, replyControl) { + var color = Theme.of(context).colorScheme.outline; + return Row( + children: [ + const SizedBox(width: 48), + if (replyItem!.cardLabel!.isNotEmpty && + replyItem!.cardLabel!.contains('热评')) + Text( + '热评 • ', + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + Text( + Utils.dateFormat(replyItem!.ctime), + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline), + ), + if (replyItem!.replyControl != null && + replyItem!.replyControl!.location != '') + Text( + ' • ${replyItem!.replyControl!.location!}', + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline), + ), + const Spacer(), + if (replyItem!.upAction!.like!) + Icon(Icons.favorite, color: Colors.red[400], size: 18), + SizedBox( + height: 28, + width: 42, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + child: Text('回复', style: Theme.of(context) + .textTheme + .labelMedium), + onPressed: () => weakUpReply!(replyItem, replyLevel), + )), + SizedBox( + height: 32, + child: TextButton( + child: Row( + children: [ + Icon( + FontAwesomeIcons.thumbsUp, + size: 16, + color: color, + ), + const SizedBox(width: 4), + Text( + replyItem!.like.toString(), + style: TextStyle( + color: color, + fontSize: + Theme.of(context).textTheme.labelSmall!.fontSize), + ), + ], + ), + onPressed: () {}, + ), + ), + const SizedBox(width: 5) + ], + ); + } +} + +// ignore: must_be_immutable +class ReplyItemRow extends StatelessWidget { + ReplyItemRow({ + super.key, + this.replies, + this.replyControl, + this.f_rpid, + }); + List? replies; + ReplyControl? replyControl; + int? f_rpid; + + @override + Widget build(BuildContext context) { + bool isShow = replyControl!.isShow!; + int extraRow = replyControl != null && isShow ? 1 : 0; + return Container( + margin: const EdgeInsets.only(left: 42, right: 4, top: 0), + child: Material( + color: Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(6), + clipBehavior: Clip.hardEdge, + animationDuration: Duration.zero, + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: replies!.length + extraRow, + itemBuilder: (context, index) { + if (extraRow == 1 && index == replies!.length) { + // 有楼中楼回复,在最后显示 + return InkWell( + onTap: () => replyReply(context), + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Text.rich( + TextSpan( + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + ), + children: [ + if (replyControl!.upReply!) + const TextSpan(text: 'up主等人 '), + TextSpan( + text: replyControl!.entryText!, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ) + ], + ), + ), + ), + ); + } else { + return InkWell( + onTap: () {}, + child: Padding( + padding: EdgeInsets.fromLTRB( + 8, + index == 0 && (extraRow == 1 || replies!.length > 1) + ? 8 + : 5, + 8, + 5), + child: Text.rich( + overflow: extraRow == 1 + ? TextOverflow.ellipsis + : TextOverflow.visible, + maxLines: extraRow == 1 ? 2 : null, + TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () { + replyReply(context); + }, + children: [ + TextSpan( + text: replies![index].member.uname + ' ', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleSmall! + .fontSize, + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('跳转至用户主页'), + }, + ), + if (replies![index].isUp) + WidgetSpan( + child: UpTag(), + ), + buildContent(context, replies![index].content), + ], + ), + ), + ), + ); + } + }, + ), + ), + ); + } + + void replyReply(context) { + Get.bottomSheet( + barrierColor: Colors.transparent, + useRootNavigator: true, + isScrollControlled: true, + Container( + height: Get.size.height - Get.size.width * 9 / 16 - 45, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + AppBar( + automaticallyImplyLeading: false, + centerTitle: false, + elevation: 1, + title: Text( + '评论详情', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + Get.back(); + }, + ) + ], + ), + Expanded( + child: VideoReplyPanel( + oid: replies!.first.oid, + rpid: f_rpid!, + level: '2', + ), + ) + ], + ), + ), + persistent: false, + backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor, + ); + } +} + +InlineSpan buildContent(BuildContext context, content) { + if (content.emote.isEmpty && + content.atNameToMid.isEmpty && + content.jumpUrl.isEmpty && + content.vote.isEmpty && + content.pictures.isEmpty) { + return TextSpan(text: content.message, + recognizer: TapGestureRecognizer() + ..onTap = ()=> { + print('点击') + },); + } + List spanChilds = []; + // 匹配表情 + String matchEmote = content.message.splitMapJoin( + RegExp(r"\[.*?\]"), + onMatch: (Match match) { + String matchStr = match[0]!; + int size = content.emote[matchStr]['meta']['size']; + if (content.emote.isNotEmpty) { + if (content.emote.keys.contains(matchStr)) { + spanChilds.add( + WidgetSpan( + child: NetworkImgLayer( + src: content.emote[matchStr]['url'], + type: 'emote', + width: size * 20, + height: size * 20, + ), + ), + ); + } else { + spanChilds.add(TextSpan(text: matchStr)); + return matchStr; + } + } + return ''; + }, + onNonMatch: (String str) { + // 匹配@用户 + String matchMember = str; + if (content.atNameToMid.isNotEmpty) { + matchMember = str.splitMapJoin( + RegExp(r"@.*( |:)"), + onMatch: (Match match) { + if (match[0] != null) { + content.atNameToMid.forEach((key, value) { + spanChilds.add( + TextSpan( + text: '@$key ', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('跳转至用户主页'), + }, + ), + ); + }); + } + return ''; + }, + onNonMatch: (String str) { + spanChilds.add(TextSpan(text: str)); + return str; + }, + ); + } else { + matchMember = str; + } + + // 匹配 jumpUrl + String matchUrl = matchMember; + if (content.jumpUrl.isNotEmpty) { + List urlKeys = content.jumpUrl.keys.toList(); + matchUrl = matchMember.splitMapJoin( + RegExp("(?:${urlKeys.join("|")})"), + onMatch: (Match match) { + String matchStr = match[0]!; + spanChilds.add( + TextSpan( + text: content.jumpUrl[matchStr]['title'], + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('Url 点击'), + }, + ), + ); + spanChilds.add( + WidgetSpan( + child: Icon( + FontAwesomeIcons.magnifyingGlass, + size: 9, + color: Theme.of(context).colorScheme.primary, + ), + alignment: PlaceholderAlignment.top, + ), + ); + return ''; + }, + onNonMatch: (String str) { + spanChilds.add(TextSpan(text: str)); + return str; + }, + ); + } + + str = matchUrl.splitMapJoin( + RegExp(r"\d{1,2}:\d{1,2}"), + onMatch: (Match match) { + String matchStr = match[0]!; + spanChilds.add( + TextSpan( + text: ' $matchStr ', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => { + print('time 点击'), + }, + ), + ); + return ''; + }, + onNonMatch: (str) { + return str; + }, + ); + + if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) { + spanChilds.add(TextSpan(text: str)); + } + return str; + }, + ); + + // 图片渲染 + if (content.pictures.isNotEmpty) { + List picList = []; + int len = content.pictures.length; + if (len == 1) { + Map pictureItem = content.pictures.first; + picList.add(pictureItem['img_src']); + spanChilds.add(const TextSpan(text: '\n')); + spanChilds.add( + WidgetSpan( + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + return GestureDetector( + onTap: () { + Get.toNamed('/preview', + arguments: {'initialPage': 0, 'imgList': picList}); + }, + child: Padding( + padding: const EdgeInsets.only(top: 4), + child: NetworkImgLayer( + src: pictureItem['img_src'], + width: box.maxWidth / 2, + height: box.maxWidth * + 0.5 * + pictureItem['img_height'] / + pictureItem['img_width'], + ), + ), + ); + }, + ), + ), + ); + } + if (len > 1) { + List list = []; + for (var i = 0; i < len; i++) { + picList.add(content.pictures[i]['img_src']); + list.add( + LayoutBuilder( + builder: (context, BoxConstraints box) { + return GestureDetector( + onTap: () { + Get.toNamed('/preview', + arguments: {'initialPage': i, 'imgList': picList}); + }, + child: NetworkImgLayer( + src: content.pictures[i]['img_src'], + width: box.maxWidth, + height: box.maxWidth, + ), + ); + }, + ), + ); + } + spanChilds.add( + WidgetSpan( + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + double maxWidth = box.maxWidth; + double crossCount = len < 3 ? 2 : 3; + double height = maxWidth / + crossCount * + (len % crossCount == 0 + ? len ~/ crossCount + : len ~/ crossCount + 1) + + 6; + return Container( + padding: const EdgeInsets.only(top: 6), + height: height, + child: GridView( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + // 子Item排列规则 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + //横轴元素个数 + crossAxisCount: crossCount.toInt(), + //纵轴间距 + mainAxisSpacing: 4.0, + //横轴间距 + crossAxisSpacing: 4.0, + //子组件宽高长度比例 + // childAspectRatio: 1, + ), + //GridView中使用的子Widegt + children: list, + ), + ); + }, + ), + ), + ); + } + } + // spanChilds.add(TextSpan(text: matchMember)); + return TextSpan(children: spanChilds); +} + +class UpTag extends StatelessWidget { + String? tagText; + UpTag({super.key, this.tagText = 'UP'}); + @override + Widget build(BuildContext context) { + Color primary = Theme.of(context).colorScheme.primary; + return Container( + width: 24, + height: 14, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(3), + color: tagText == 'UP' ? primary : null, + border: Border.all(color: primary)), + margin: const EdgeInsets.only(right: 4), + child: Center( + child: Text( + tagText!, + style: TextStyle( + fontSize: 9, + color: tagText == 'UP' + ? Theme.of(context).colorScheme.onPrimary + : primary, + ), + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 0c1d47df..3265ea24 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1,18 +1,136 @@ +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/pages/video/detail/reply/index.dart'; +import 'package:pilipala/pages/video/detail/controller.dart'; +import 'package:pilipala/pages/video/detail/introduction/index.dart'; +import 'package:pilipala/pages/video/detail/related/index.dart'; class VideoDetailPage extends StatefulWidget { - const VideoDetailPage({super.key}); + const VideoDetailPage({Key? key}) : super(key: key); @override State createState() => _VideoDetailPageState(); } class _VideoDetailPageState extends State { + final VideoDetailController videoDetailController = + Get.put(VideoDetailController(), tag: Get.arguments['heroTag']); + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('videoDetail'), + final double statusBarHeight = MediaQuery.of(context).padding.top; + final double pinnedHeaderHeight = statusBarHeight + + kToolbarHeight + + MediaQuery.of(context).size.width * 9 / 16; + return DefaultTabController( + initialIndex: videoDetailController.tabInitialIndex, + length: videoDetailController.tabs.length, // tab的数量. + child: SafeArea( + top: false, + bottom: false, + child: Scaffold( + body: ExtendedNestedScrollView( + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + title: const Text("视频详情"), + pinned: true, + elevation: 0, + scrolledUnderElevation: 0, + forceElevated: innerBoxIsScrolled, + expandedHeight: MediaQuery.of(context).size.width * 9 / 16, + collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, + flexibleSpace: FlexibleSpaceBar( + background: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Hero( + tag: videoDetailController.heroTag, + child: NetworkImgLayer( + src: videoDetailController.videoItem['pic'], + width: maxWidth, + height: maxHeight, + ), + ); + }, + ), + ), + ), + ), + ]; + }, + pinnedHeaderSliverHeightBuilder: () { + return pinnedHeaderHeight; + }, + onlyOneScrollInBody: true, + body: Column( + children: [ + Container( + height: 45, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + width: 280, + margin: const EdgeInsets.only(left: 20), + child: Obx( + () => TabBar( + dividerColor: Colors.transparent, + tabs: videoDetailController.tabs + .map((String name) => Tab(text: name)) + .toList(), + ), + ), + ), + // 弹幕开关 + // const Spacer(), + // Flexible( + // flex: 2, + // child: Container( + // height: 50, + // ), + // ), + ], + ), + ), + Expanded( + child: TabBarView( + children: [ + Builder( + builder: (context) { + return const CustomScrollView( + key: PageStorageKey('简介'), + slivers: [ + VideoIntroPanel(), + RelatedVideoPanel(), + ], + ); + }, + ), + VideoReplyPanel() + ], + ), + ), + ], + ), + ), + ), ), ); } diff --git a/lib/pages/video/detail/widgets/expandable_section.dart b/lib/pages/video/detail/widgets/expandable_section.dart new file mode 100644 index 00000000..c3d49a80 --- /dev/null +++ b/lib/pages/video/detail/widgets/expandable_section.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +class ExpandedSection extends StatefulWidget { + final Widget child; + final bool expand; + double begin = 0.0; + double end = 1.0; + + ExpandedSection( + {this.expand = false, + required this.child, + required this.begin, + required this.end}); + + @override + _ExpandedSectionState createState() => _ExpandedSectionState(); +} + +class _ExpandedSectionState extends State + with SingleTickerProviderStateMixin { + late AnimationController expandController; + late Animation animation; + + @override + void initState() { + super.initState(); + prepareAnimations(); + _runExpandCheck(); + } + + ///Setting up the animation + // void prepareAnimations() { + // expandController = AnimationController( + // vsync: this, duration: const Duration(milliseconds: 500)); + // animation = CurvedAnimation( + // parent: expandController, + // curve: Curves.fastOutSlowIn, + // ); + // } + + void prepareAnimations() { + expandController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 400)); + Animation curve = CurvedAnimation( + parent: expandController, + curve: Curves.fastOutSlowIn, + ); + animation = Tween(begin: widget.begin, end: widget.end).animate(curve); + // animation = CurvedAnimation( + // parent: expandController, + // curve: Curves.fastOutSlowIn, + // ); + } + + void _runExpandCheck() { + if (widget.expand) { + expandController.forward(); + } else { + expandController.reverse(); + } + } + + @override + void didUpdateWidget(ExpandedSection oldWidget) { + super.didUpdateWidget(oldWidget); + _runExpandCheck(); + } + + @override + void dispose() { + expandController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizeTransition( + axisAlignment: -1.0, + sizeFactor: animation, + child: widget.child, + ); + } +} diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart new file mode 100644 index 00000000..9fe1b670 --- /dev/null +++ b/lib/pages/webview/controller.dart @@ -0,0 +1,85 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/constants.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/pages/home/index.dart'; +import 'package:pilipala/pages/mine/index.dart'; +import 'package:pilipala/utils/cookie.dart'; +import 'package:pilipala/utils/storage.dart'; +import 'package:webview_cookie_manager/webview_cookie_manager.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebviewController extends GetxController { + String url = ''; + String type = ''; + String pageTitle = ''; + final WebViewController controller = WebViewController(); + + @override + void onInit() { + super.onInit(); + url = Get.parameters['url']!; + type = Get.parameters['type']!; + pageTitle = Get.parameters['pageTitle']!; + + webviewInit(); + if (type == 'login') { + controller.clearCache(); + controller.clearLocalStorage(); + WebViewCookieManager().clearCookies(); + controller.setUserAgent(Request().headerUa('mob')); + } + } + + webviewInit() { + controller + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + // 页面加载 + onProgress: (int progress) { + // Update loading bar. + }, + onPageStarted: (String url) {}, + // 加载完成 + onPageFinished: (String url) async { + if (url.startsWith( + 'https://passport.bilibili.com/web/sso/exchange_cookie') || + url.startsWith('https://m.bilibili.com/')) { + try { + var cookies = + await WebviewCookieManager().getCookies(HttpString.baseUrl); + var apiCookies = + await WebviewCookieManager().getCookies(HttpString.baseUrl); + await SetCookie.onSet(cookies, HttpString.baseUrl); + await SetCookie.onSet(apiCookies, HttpString.baseApiUrl); + await UserHttp.userInfo(); + var result = await UserHttp.userInfo(); + print('网页登录: $result'); + if (result['status'] && result['data'].isLogin) { + SmartDialog.showToast('登录成功'); + Box user = GStrorage.user; + user.put(UserBoxKey.userLogin, true); + Get.find().userInfo.value = result['data']; + Get.find().queryRcmdFeed('onRefresh'); + Get.back(); + } + } catch (e) { + print(e); + } + } + }, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(url)); + } +} diff --git a/lib/pages/webview/index.dart b/lib/pages/webview/index.dart new file mode 100644 index 00000000..17efad94 --- /dev/null +++ b/lib/pages/webview/index.dart @@ -0,0 +1,4 @@ +library webview; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/webview/view.dart b/lib/pages/webview/view.dart new file mode 100644 index 00000000..bee73de5 --- /dev/null +++ b/lib/pages/webview/view.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'controller.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebviewPage extends StatefulWidget { + const WebviewPage({super.key}); + + @override + State createState() => _WebviewPageState(); +} + +class _WebviewPageState extends State { + final WebviewController _webviewController = Get.put(WebviewController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + title: Text( + _webviewController.pageTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: WebViewWidget(controller: _webviewController.controller), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 9ec967f8..1ac52a93 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,7 +1,13 @@ import 'package:get/get.dart'; +import 'package:pilipala/pages/fav/index.dart'; +import 'package:pilipala/pages/favDetail/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; +import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; +import 'package:pilipala/pages/webview/index.dart'; +import 'package:pilipala/pages/setting/index.dart'; +import 'package:pilipala/pages/media/index.dart'; class Routes { static final List getPages = [ @@ -11,5 +17,17 @@ class Routes { GetPage(name: '/hot', page: () => const HotPage()), // 视频详情 GetPage(name: '/video', page: () => const VideoDetailPage()), + // 图片预览 + GetPage(name: '/preview', page: () => const ImagePreview()), + // + GetPage(name: '/webview', page: () => const WebviewPage()), + // 设置 + GetPage(name: '/setting', page: () => const SettingPage()), + // + GetPage(name: '/media', page: () => const MediaPage()), + // + GetPage(name: '/fav', page: () => const FavPage()), + // + GetPage(name: '/favDetail', page: () => const FavDetailPage()), ]; } diff --git a/lib/utils/cookie.dart b/lib/utils/cookie.dart new file mode 100644 index 00000000..8d5c891b --- /dev/null +++ b/lib/utils/cookie.dart @@ -0,0 +1,26 @@ +import 'dart:io'; +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:pilipala/http/init.dart'; +import 'package:pilipala/utils/utils.dart'; + +class SetCookie { + static onSet(List cookiesList, String url) async { + // domain url + List jarCookies = []; + if (cookiesList.isNotEmpty) { + for (var i in cookiesList) { + Cookie jarCookie = Cookie(i.name, i.value); + jarCookies.add(jarCookie); + } + } + String cookiePath = await Utils.getCookiePath(); + PersistCookieJar cookieJar = PersistCookieJar( + ignoreExpires: true, + storage: FileStorage(cookiePath), + ); + await cookieJar.saveFromResponse(Uri.parse(url), jarCookies); + // 重新设置 cookie + Request.setCookie(); + return true; + } +} diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart new file mode 100644 index 00000000..24374f36 --- /dev/null +++ b/lib/utils/storage.dart @@ -0,0 +1,24 @@ +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; + +class GStrorage { + static late final Box user; + + static Future init() async { + final dir = await getApplicationDocumentsDirectory(); + final path = dir.path; + Hive.init('$path/hive'); + user = await Hive.openBox('user'); + } +} + +// 约定 key +class UserBoxKey { + static const String userName = 'userName'; + // 头像 + static const String userFace = 'userFace'; + // mid + static const String userMid = 'userMid'; + // 登录状态 + static const String userLogin = 'userLogin'; +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 2bd88366..084a6e6f 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,5 +1,6 @@ // 工具函数 import 'dart:io'; +import 'dart:math'; import 'package:get/get_utils/get_utils.dart'; import 'package:path_provider/path_provider.dart'; @@ -130,4 +131,8 @@ class Utils { } return date; } + + static String makeHeroTag(v) { + return v.toString() + Random().nextInt(9999).toString(); + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 675b7194..fe56f8d8 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 3e303c15..18366213 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 65feaf20..bb5265fd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,19 @@ import FlutterMacOS import Foundation import connectivity_plus +import device_info_plus import dynamic_color import path_provider_foundation +import share_plus import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index cc2b687d..10b7569f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,12 +1,28 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" + url: "https://pub.dev" + source: hosted + version: "59.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 + url: "https://pub.dev" + source: hosted + version: "5.11.1" args: dependency: transitive description: name: args sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.0" async: @@ -14,7 +30,7 @@ packages: description: name: async sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.10.0" boolean_selector: @@ -22,15 +38,23 @@ packages: description: name: boolean_selector sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" cached_network_image: dependency: "direct main" description: name: cached_network_image sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.3" cached_network_image_platform_interface: @@ -38,7 +62,7 @@ packages: description: name: cached_network_image_platform_interface sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" cached_network_image_web: @@ -46,7 +70,7 @@ packages: description: name: cached_network_image_web sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" characters: @@ -54,7 +78,7 @@ packages: description: name: characters sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" clock: @@ -62,7 +86,7 @@ packages: description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" collection: @@ -70,39 +94,55 @@ packages: description: name: collection sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.17.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: d73575bb66216738db892f72ba67dc478bd3b5490fbbcf43644b57645eabc822 - url: "https://pub.flutter-io.cn" + sha256: "5c7ad2d90aae958c230b27450044a29f5b0a69ea4b1792e17164b3a53de33e47" + url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" cookie_jar: dependency: "direct main" description: name: cookie_jar sha256: d1cc6516a190ba667941f722b6365d202caff3dacb38de24268b8d6ff1ec8a1d - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" crypto: dependency: transitive description: name: crypto sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" cupertino_icons: @@ -110,23 +150,47 @@ packages: description: name: cupertino_icons sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + url: "https://pub.dev" + source: hosted + version: "2.3.1" dbus: dependency: transitive description: name: dbus sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + url: "https://pub.dev" + source: hosted + version: "8.2.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dio: dependency: "direct main" description: name: dio sha256: "0894a098594263fe1caaba3520e3016d8a855caeb010a882273189cca10f11e9" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" dio_cookie_manager: @@ -134,7 +198,7 @@ packages: description: name: dio_cookie_manager sha256: b45f11c2fcbccf39c5952ab68910b3a155486c4fa730ceb4ce867c4943169ea1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" dio_http2_adapter: @@ -142,7 +206,7 @@ packages: description: name: dio_http2_adapter sha256: b06a02faaff972c4809c4ada7a2f71f6c74ce21f0feee79b357f2a9590c049d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" dynamic_color: @@ -150,15 +214,39 @@ packages: description: name: dynamic_color sha256: bbebb1b7ebed819e0ec83d4abdc2a8482d934f6a85289ffc1c6acf7589fa2aad - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.6.3" + extended_image: + dependency: "direct main" + description: + name: extended_image + sha256: "75e4b0ad0f8f63eed7935ff2506c809a670f5e6dd0f61304525879d53fc41a17" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "550743b43ab093aed35ef234500fcc7a304cbac1eca47b0cc991e07e88750758" + url: "https://pub.dev" + source: hosted + version: "3.4.2" + extended_nested_scroll_view: + dependency: "direct main" + description: + name: extended_nested_scroll_view + sha256: fc55b8f7e2c78701320d7eccda3b256387290b8498f0363d8ffd6f16760949d7 + url: "https://pub.dev" + source: hosted + version: "6.0.0" fake_async: dependency: transitive description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: @@ -166,7 +254,7 @@ packages: description: name: ffi sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" file: @@ -174,7 +262,7 @@ packages: description: name: file sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.4" flutter: @@ -187,7 +275,7 @@ packages: description: name: flutter_blurhash sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.0" flutter_cache_manager: @@ -195,7 +283,7 @@ packages: description: name: flutter_cache_manager sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.3.0" flutter_lints: @@ -203,9 +291,17 @@ packages: description: name: flutter_lints sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_smart_dialog: + dependency: "direct main" + description: + name: flutter_smart_dialog + sha256: da7ed8fe78e301e3c2cdaa533d13ed3edcf1853c1ba1a7383b481739569f7b2a + url: "https://pub.dev" + source: hosted + version: "4.9.0+6" flutter_test: dependency: "direct dev" description: flutter @@ -216,20 +312,60 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206" + url: "https://pub.dev" + source: hosted + version: "10.4.0" get: dependency: "direct main" description: name: get sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.6.5" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "65998cc4d2cd9680a3d9709d893d2f6bb15e6c1f92626c3f1fa650b4b3281521" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: name: http sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.13.5" http2: @@ -237,23 +373,39 @@ packages: description: name: http2 sha256: "58805ebc6513eed3b98ee0a455a8357e61d187bf2e0fdc1e53120770f78de258" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "1f32359bd07a064ad256d1f84ae5f973f69bc972e7287223fa198abe1d969c28" + url: "https://pub.dev" + source: hosted + version: "2.0.3" http_parser: dependency: transitive description: name: http_parser sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.2" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: be812580c7a320d3bf583af89cac6b376f170d48000aca75215a73285a3223a0 + url: "https://pub.dev" + source: hosted + version: "1.7.1" js: dependency: transitive description: name: js sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.5" lints: @@ -261,15 +413,23 @@ packages: description: name: lints sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" matcher: dependency: transitive description: name: matcher sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.13" material_color_utilities: @@ -277,7 +437,7 @@ packages: description: name: material_color_utilities sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" meta: @@ -285,15 +445,23 @@ packages: description: name: meta sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" nm: dependency: transitive description: name: nm sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.0" octo_image: @@ -301,15 +469,23 @@ packages: description: name: octo_image sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: name: path sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.8.2" path_provider: @@ -317,23 +493,23 @@ packages: description: name: path_provider sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.14" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a - url: "https://pub.flutter-io.cn" + sha256: "3e58242edc02624f2c712e3f8bea88e0e341c4ae1abd3a6ff661318a3aefd829" + url: "https://pub.dev" source: hosted - version: "2.0.25" + version: "2.0.26" path_provider_foundation: dependency: transitive description: name: path_provider_foundation sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.2" path_provider_linux: @@ -341,7 +517,7 @@ packages: description: name: path_provider_linux sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.10" path_provider_platform_interface: @@ -349,31 +525,71 @@ packages: description: name: path_provider_platform_interface sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 - url: "https://pub.flutter-io.cn" + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" pedantic: dependency: transitive description: name: pedantic sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + url: "https://pub.dev" + source: hosted + version: "9.0.8" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" + source: hosted + version: "0.1.2" petitparser: dependency: transitive description: name: petitparser sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.0" platform: @@ -381,7 +597,7 @@ packages: description: name: platform sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: @@ -389,7 +605,7 @@ packages: description: name: plugin_platform_interface sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" process: @@ -397,52 +613,92 @@ packages: description: name: process sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" rxdart: dependency: transitive description: name: rxdart sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.27.7" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + url: "https://pub.dev" + source: hosted + version: "3.2.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: b20e191de6964e98032573cecb1d2b169d96ba63fdb586d24dcd1003ba7e94f6 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: name: source_span sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - sha256: "500d6fec583d2c021f2d25a056d96654f910662c64f836cd2063167b8f1fa758" - url: "https://pub.flutter-io.cn" + sha256: e7dfb6482d5d02b661d0b2399efa72b98909e5aa7b8336e1fb37e226264ade00 + url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.7" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "963dad8c4aa2f814ce7d2d5b1da2f36f31bd1a439d8f27e3dc189bb9d26bc684" - url: "https://pub.flutter-io.cn" + sha256: "220831bf0bd5333ff2445eee35ec131553b804e6b5d47a4a37ca6f5eb66e282c" + url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" stack_trace: dependency: transitive description: name: stack_trace sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.11.0" stream_channel: @@ -450,7 +706,7 @@ packages: description: name: stream_channel sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" string_scanner: @@ -458,23 +714,23 @@ packages: description: name: string_scanner sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" - url: "https://pub.flutter-io.cn" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" term_glyph: dependency: transitive description: name: term_glyph sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: @@ -482,7 +738,7 @@ packages: description: name: test_api sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.16" typed_data: @@ -490,15 +746,79 @@ packages: description: name: typed_data sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" + source: hosted + version: "6.1.10" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" + url: "https://pub.dev" + source: hosted + version: "6.0.31" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" + source: hosted + version: "2.0.16" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + url: "https://pub.dev" + source: hosted + version: "3.0.6" uuid: dependency: transitive description: name: uuid sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.7" vector_math: @@ -506,15 +826,71 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + webview_cookie_manager: + dependency: "direct main" + description: + name: webview_cookie_manager + sha256: "425a9feac5cd2cb62a71da3dda5ac2eaf9ece5481ee8d79f3868dc5ba8223ad3" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "1a37bdbaaf5fbe09ad8579ab09ecfd473284ce482f900b5aea27cf834386a567" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "1acea8def62592123e2fbbca164ed8681a98a890bdcbb88f916d5b4a22687759" + url: "https://pub.dev" + source: hosted + version: "3.7.0" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "78715dc442b7849dbde74e92bb67de1cecf5addf95531c5fb474e72f5fe9a507" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "61f33512810bf1ee9ac89761a4b02663ff64e8227b7dc80654642acd660fd49d" + url: "https://pub.dev" + source: hosted + version: "3.4.2" win32: dependency: transitive description: name: win32 sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.4" xdg_directories: @@ -522,7 +898,7 @@ packages: description: name: xdg_directories sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" xml: @@ -530,9 +906,17 @@ packages: description: name: xml sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.2.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: dart: ">=2.19.6 <3.0.0" - flutter: ">=3.4.0-17.0.pre" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6d3b671c..47c6ee49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,9 +48,29 @@ dependencies: # 图片 cached_network_image: ^3.2.3 - + extended_image: ^7.0.2 + image_gallery_saver: ^1.7.1 + # 存储 path_provider: ^2.0.14 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + + # 设备信息 + device_info_plus: ^8.2.0 + # 权限 + permission_handler: ^10.2.0 + # 分享 + share_plus: ^6.3.1 + # webView + url_launcher: ^6.1.9 + webview_cookie_manager: ^2.0.6 + webview_flutter: ^4.2.0 + + extended_nested_scroll_view: ^6.0.0 + font_awesome_flutter: ^10.4.0 + # toast + flutter_smart_dialog: ^4.9.0+6 dev_dependencies: flutter_test: @@ -62,6 +82,22 @@ 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: + # git: + # url: https://github.com/nvi9/flutter_launcher_icons.git + # ref: e045d40 + hive_generator: ^2.0.0 + +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_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 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -76,6 +112,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/images/lv/ + - assets/images/logo/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware @@ -87,17 +125,14 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # + fonts: + - family: fansCard + fonts: + - asset: assets/fonts/fansCard.ttf + - family: ArchivoNarrow + fonts: + - asset: assets/fonts/ArchivoNarrow-BoldItalic.ttf + + # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ae0cf3ff..efd465a4 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,19 @@ #include #include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e47678f2..afce192f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus dynamic_color + permission_handler_windows + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST