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