├── .gitignore ├── .gradle ├── 6.8 │ ├── fileChanges │ │ └── last-build.bin │ ├── fileHashes │ │ └── fileHashes.lock │ └── gc.properties ├── buildOutputCleanup │ ├── buildOutputCleanup.lock │ └── cache.properties ├── checksums │ └── checksums.lock ├── configuration-cache │ └── gc.properties └── vcs-1 │ └── gc.properties ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── flutter_video_player │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── abstracts │ └── abstract_interface.dart ├── custom │ └── navigationbar.dart ├── database │ └── hv_manager.dart ├── extension │ └── extension.dart ├── http │ ├── http_config.dart │ ├── http_manager.dart │ └── http_response_model.dart ├── main.dart ├── models │ ├── models.dart │ └── user │ │ └── user_post.dart ├── pages │ ├── cache │ │ └── cache_page.dart │ ├── category │ │ ├── model │ │ │ └── category_model.dart │ │ ├── page │ │ │ └── category_page.dart │ │ ├── view │ │ │ ├── category_item_view.dart │ │ │ └── cateogry_header_view.dart │ │ └── view_model │ │ │ └── category_view_model.dart │ ├── drama_detail │ │ ├── model │ │ │ ├── play_info_model.dart │ │ │ └── video_info_model.dart │ │ ├── page │ │ │ └── drama_detail_page.dart │ │ ├── view_model │ │ │ └── drama_detail_view_model.dart │ │ └── views │ │ │ ├── small_screen_placeholder_view.dart │ │ │ ├── video_brief_detail_view.dart │ │ │ ├── video_brief_view.dart │ │ │ ├── video_episode_view.dart │ │ │ └── video_series_view.dart │ ├── drama_list │ │ └── drama_list_page.dart │ ├── haokan_video │ │ ├── haokan_home │ │ │ ├── haokan_home_model.dart │ │ │ ├── haokan_home_page.dart │ │ │ ├── haokan_home_view_model.dart │ │ │ └── haokan_tab_page.dart │ │ ├── haokan_video_detail │ │ │ └── haokan_video_detail_model.dart │ │ └── short_video │ │ │ ├── haokan_short_video_item_view.dart │ │ │ ├── haokan_short_video_model.dart │ │ │ ├── haokan_short_video_page.dart │ │ │ └── haokan_short_video_view_model.dart │ ├── home │ │ ├── model │ │ │ └── home_model.dart │ │ ├── page │ │ │ └── home_page.dart │ │ ├── view_model │ │ │ └── home_view_model.dart │ │ └── views │ │ │ ├── big_eye_view.dart │ │ │ ├── cell_big_eye.dart │ │ │ ├── cell_guide.dart │ │ │ ├── cell_list.dart │ │ │ ├── cell_multi.dart │ │ │ └── cell_single_image.dart │ ├── hot_video │ │ ├── bilibili_model.dart │ │ ├── card_view.dart │ │ ├── hot_video_page.dart │ │ └── hot_video_view_model.dart │ ├── login │ │ ├── login_input_text_view.dart │ │ ├── login_page.dart │ │ └── login_view_model.dart │ ├── mine │ │ ├── mine_page.dart │ │ └── mine_view_model.dart │ ├── player │ │ ├── controller_overlay.dart │ │ └── video_player.dart │ ├── search │ │ └── search_page.dart │ ├── setting │ │ └── setting_page.dart │ ├── splash │ │ └── splash_page.dart │ ├── tab_controller │ │ └── tab_controller.dart │ └── web │ │ └── web_page.dart ├── routes │ └── route_manager.dart ├── user │ ├── user_info_model.dart │ └── user_manager.dart └── util │ ├── device_info.dart │ ├── r_sources.dart │ ├── toast.dart │ └── util.dart ├── local.properties ├── pubspec.lock ├── pubspec.yaml ├── resource ├── image │ ├── 2.0x │ │ ├── ic_vip.png │ │ ├── pic_Avatar_h.png │ │ ├── pic_Avatar_n.png │ │ └── pic_banner_shadow.png │ ├── 3.0x │ │ ├── ic_vip.png │ │ ├── pic_Avatar_h.png │ │ ├── pic_Avatar_n.png │ │ ├── pic_banner_shadow.png │ │ └── splash_logo.png │ ├── cover_img.png │ ├── haokan_logo.png │ ├── ic_playing.gif │ └── splash_shake.png └── json │ ├── category_filters.json │ ├── category_list_page1.json │ ├── category_list_page2.json │ ├── drama_info_detail.json │ ├── home_page.json │ ├── login_send_code.json │ ├── mine_page.json │ ├── play1_info_detail.json │ ├── play2_info_detail.json │ ├── play3_info_detail.json │ ├── play4_info_detail.json │ └── user_info.json └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.gradle/6.8/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gradle/6.8/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/.gradle/6.8/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /.gradle/6.8/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/.gradle/6.8/gc.properties -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/buildOutputCleanup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/.gradle/buildOutputCleanup/buildOutputCleanup.lock -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Fri Feb 25 11:26:15 CST 2022 2 | gradle.version=6.8 3 | -------------------------------------------------------------------------------- /.gradle/checksums/checksums.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/.gradle/checksums/checksums.lock -------------------------------------------------------------------------------- /.gradle/configuration-cache/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/.gradle/configuration-cache/gc.properties -------------------------------------------------------------------------------- /.gradle/vcs-1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/.gradle/vcs-1/gc.properties -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_video_player 后面在provider_dev分支上更新 2 | 3 | # provider_dev分支 重构版 4 | 5 | # 完整的flutter项目,包含首页,好看视频,分类筛选,bilibili瀑布流漫画,抖音短视频,登录,数据库sql,hive,视频播放,全屏播放,历史记录等功能,video_player,在iOS平台上开发,好看短视频接口需要cookie,请自行抓接口添加测试 6 | ! 7 | ! 8 | ! 9 | ! 10 | ! 11 | ! 12 | ! 13 | ! 14 | 15 | 16 | ## Getting Started 17 | This project is a starting point for a Flutter application. 18 | 19 | A few resources to get you started if this is your first Flutter project: 20 | 21 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 22 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 23 | 24 | For help getting started with Flutter, view our 25 | [online documentation](https://flutter.dev/docs), which offers tutorials, 26 | samples, guidance on mobile development, and a full API reference. 27 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.example.flutter_video_player" 47 | minSdkVersion flutter.minSdkVersion 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_video_player/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_video_player 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - package_info_plus (0.4.5): 4 | - Flutter 5 | - path_provider_ios (0.0.1): 6 | - Flutter 7 | - shared_preferences_ios (0.0.1): 8 | - Flutter 9 | - video_player_avfoundation (0.0.1): 10 | - Flutter 11 | 12 | DEPENDENCIES: 13 | - Flutter (from `Flutter`) 14 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 15 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 16 | - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) 17 | - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) 18 | 19 | EXTERNAL SOURCES: 20 | Flutter: 21 | :path: Flutter 22 | package_info_plus: 23 | :path: ".symlinks/plugins/package_info_plus/ios" 24 | path_provider_ios: 25 | :path: ".symlinks/plugins/path_provider_ios/ios" 26 | shared_preferences_ios: 27 | :path: ".symlinks/plugins/shared_preferences_ios/ios" 28 | video_player_avfoundation: 29 | :path: ".symlinks/plugins/video_player_avfoundation/ios" 30 | 31 | SPEC CHECKSUMS: 32 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 33 | package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e 34 | path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 35 | shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad 36 | video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff 37 | 38 | PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 39 | 40 | COCOAPODS: 1.11.3 41 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | 剧天堂 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_video_player 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeRight 35 | UIInterfaceOrientationLandscapeLeft 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/abstracts/abstract_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_video_player/http/http_response_model.dart'; 2 | 3 | abstract class Request { 4 | Future requestData({ 5 | int pageNum = 1, 6 | Map queryParams = const {}, 7 | }); 8 | 9 | Future handleData(ResponseModel model); 10 | } 11 | -------------------------------------------------------------------------------- /lib/custom/navigationbar.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/util/util.dart'; 5 | 6 | class UINavigationBar extends StatelessWidget implements PreferredSizeWidget { 7 | UINavigationBar({ 8 | Key? key, 9 | this.title = '', 10 | this.isCenterTitle = true, 11 | this.isSecondPage = true, 12 | this.leading = const [], 13 | this.middleView, 14 | this.tailing = const [], 15 | this.color = Colors.white, 16 | this.height = 44.0, 17 | }) : super(key: key); 18 | 19 | final String? title; 20 | final Widget? middleView; 21 | final List? leading; 22 | final List? tailing; 23 | Color? color; 24 | final double? height; 25 | 26 | bool? isCenterTitle; 27 | bool? isSecondPage; 28 | EdgeInsets insets = const EdgeInsets.fromLTRB(12, 0, 12, 0); 29 | 30 | @override 31 | Size get preferredSize => const Size.fromHeight(Util.navBarHeight); 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | List views = []; 36 | 37 | if (leading?.isNotEmpty == true) { 38 | // for (var i = 0; i < widget.leading!.length; i++) { 39 | // views.add( 40 | // Positioned.fromRect( 41 | // rect: Rect.fromLTWH(widget.insets.left + i * 100, 0, 100, 40), 42 | // child: widget.leading![i], 43 | // ), 44 | // ); 45 | // } 46 | views.add( 47 | Row( 48 | mainAxisAlignment: MainAxisAlignment.start, 49 | children: leading!, 50 | ), 51 | ); 52 | } 53 | 54 | if (title?.isNotEmpty == true) { 55 | if (isCenterTitle == true) { 56 | views.add( 57 | Center( 58 | child: Text( 59 | title!, 60 | style: TextStyle( 61 | fontSize: 14, 62 | color: const Color(0xff32383a), 63 | fontWeight: 64 | isSecondPage == true ? FontWeight.w500 : FontWeight.normal, 65 | ), 66 | ), 67 | ), 68 | ); 69 | } else { 70 | views.add( 71 | Positioned( 72 | left: insets.left, 73 | child: Text( 74 | title!, 75 | style: TextStyle( 76 | color: const Color(0xffFAFbFC), 77 | fontSize: 16, 78 | fontWeight: 79 | isSecondPage == true ? FontWeight.w500 : FontWeight.normal, 80 | ), 81 | ), 82 | ), 83 | ); 84 | } 85 | } 86 | 87 | if (middleView != null) { 88 | views.add( 89 | Center( 90 | child: middleView!, 91 | ), 92 | ); 93 | } 94 | 95 | if (tailing?.isNotEmpty == true) { 96 | views.add( 97 | Row( 98 | mainAxisAlignment: MainAxisAlignment.end, 99 | children: tailing!, 100 | ), 101 | ); 102 | } 103 | return Container( 104 | color: color, 105 | child: Column( 106 | children: [ 107 | Container( 108 | height: Util.statusBarHeight, 109 | ), 110 | SizedBox( 111 | height: Util.navBarHeight, 112 | child: Stack( 113 | alignment: Alignment.center, 114 | children: views, 115 | ), 116 | ) 117 | ], 118 | ), 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/database/hv_manager.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: empty_catches, control_flow_in_finally 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:hive_flutter/hive_flutter.dart'; 5 | import 'package:flutter_video_player/user/user_info_model.dart'; 6 | import 'package:path/path.dart' as p; 7 | import 'package:path_provider/path_provider.dart'; 8 | 9 | class HiveManager { 10 | HiveManager._(); 11 | static final HiveManager _instance = HiveManager._(); 12 | static HiveManager get instance => _instance; 13 | 14 | @protected 15 | factory HiveManager() => _instance; 16 | 17 | static var boxPath = ''; 18 | final userBox = 'UserBox'; 19 | final userInfo = 'UserInfo'; 20 | 21 | static Future initHive() async { 22 | try { 23 | final directory = await getApplicationDocumentsDirectory(); 24 | boxPath = p.join(directory.path, 'JQQHive'); 25 | await Hive.initFlutter(boxPath); 26 | } catch (e) {} 27 | } 28 | 29 | ///User 30 | Future saveUserInfo(UserInfoModel model) async { 31 | try { 32 | final box = await Hive.openBox(userBox); 33 | await box.put(userInfo, model.toJson()); 34 | return true; 35 | } catch (e) { 36 | return false; 37 | } 38 | } 39 | 40 | Future getUserInfo() async { 41 | try { 42 | final box = await Hive.openBox(userBox); 43 | if (box.isEmpty) { 44 | return null; 45 | } 46 | Map? map = await box.getAt(0); 47 | if (map != null) { 48 | UserInfoModel model = UserInfoModel(map); 49 | if (model.token != null) { 50 | return model; 51 | } 52 | } 53 | } catch (e) {} 54 | return null; 55 | } 56 | 57 | Future deleteUserInfo() async { 58 | try { 59 | final box = await Hive.openBox(userBox); 60 | await box.clear(); 61 | return true; 62 | } catch (e) { 63 | return false; 64 | } 65 | } 66 | 67 | //// 68 | /// 69 | final homeBox = 'HomeBox'; 70 | final homeKey = 'HomeKey'; 71 | Future saveHomeData(UserInfoModel model) async { 72 | try { 73 | final box = await Hive.openBox(homeBox); 74 | await box.put(homeKey, model.toJson()); 75 | return true; 76 | } catch (e) { 77 | return false; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/extension/extension.dart: -------------------------------------------------------------------------------- 1 | extension StringConvert on int { 2 | String get stringify { 3 | String ret = ''; 4 | int quot = this ~/ 10; 5 | int rem = this % 10; 6 | if (quot > 0) { 7 | ret += (quot * 10).toParse; 8 | } 9 | if (rem > 0) { 10 | ret += rem.toParse; 11 | } 12 | return ret; 13 | } 14 | 15 | String get toParse { 16 | String ret = ''; 17 | switch (this) { 18 | case 0: 19 | ret = '零'; 20 | break; 21 | case 1: 22 | ret = '一'; 23 | break; 24 | case 2: 25 | ret = '二'; 26 | break; 27 | case 3: 28 | ret = '三'; 29 | break; 30 | case 4: 31 | ret = '四'; 32 | break; 33 | case 5: 34 | ret = '五'; 35 | break; 36 | case 6: 37 | ret = '六'; 38 | break; 39 | case 7: 40 | ret = '七'; 41 | break; 42 | case 8: 43 | ret = '八'; 44 | break; 45 | case 9: 46 | ret = '九'; 47 | break; 48 | case 10: 49 | ret = '十'; 50 | break; 51 | case 20: 52 | ret = '二十'; 53 | break; 54 | default: 55 | } 56 | return ret; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/http/http_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart' show rootBundle; 2 | import 'dart:convert' show jsonDecode; 3 | import 'package:flutter_video_player/util/r_sources.dart'; 4 | import 'http_response_model.dart'; 5 | 6 | enum Api { 7 | home, 8 | dramaDetail, 9 | dramaList, 10 | search, 11 | mine, 12 | sendVerifyCode, 13 | verifyCodeLogin, 14 | logout, 15 | categoryHeader, 16 | categoryPage, 17 | dramaPlay, 18 | } 19 | 20 | class HttpConfig { 21 | static Future request({ 22 | required Api api, 23 | Map queryParams = const {}, 24 | }) async { 25 | String jsp = ''; 26 | switch (api) { 27 | case Api.home: 28 | jsp = R.Jsp.homePage; 29 | break; 30 | case Api.categoryHeader: 31 | jsp = R.Jsp.categoryFilter; 32 | break; 33 | case Api.categoryPage: 34 | if (queryParams['page'] == 1) { 35 | jsp = R.Jsp.categoryPage1; 36 | } else { 37 | jsp = R.Jsp.categoryPage2; 38 | } 39 | break; 40 | case Api.dramaDetail: 41 | jsp = R.Jsp.dramaDetail; 42 | break; 43 | case Api.dramaPlay: 44 | String episodeId = queryParams['episodeId']; 45 | switch (int.parse(episodeId)) { 46 | case 1: 47 | jsp = R.Jsp.play1Detail; 48 | break; 49 | case 2: 50 | jsp = R.Jsp.play2Detail; 51 | break; 52 | case 3: 53 | jsp = R.Jsp.play3Detail; 54 | break; 55 | case 4: 56 | jsp = R.Jsp.play4Detail; 57 | break; 58 | default: 59 | } 60 | break; 61 | case Api.mine: 62 | jsp = R.Jsp.minePage; 63 | break; 64 | case Api.sendVerifyCode: 65 | jsp = R.Jsp.loginSendCode; 66 | break; 67 | case Api.verifyCodeLogin: 68 | jsp = R.Jsp.loginUserInfo; 69 | break; 70 | default: 71 | break; 72 | } 73 | 74 | if (jsp.isNotEmpty) { 75 | try { 76 | String jsonString = await rootBundle.loadString(jsp); 77 | final json = jsonDecode(jsonString); 78 | return ResponseModel.fromJson(json); 79 | } catch (e) { 80 | return null; 81 | } 82 | } 83 | return null; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/http/http_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:dio/dio.dart'; 3 | import 'http_response_model.dart'; 4 | 5 | enum Method { 6 | get('get'), 7 | post('post'), 8 | delete('delete'), 9 | put('put'); 10 | 11 | final String name; 12 | const Method(this.name); 13 | } 14 | 15 | enum NetApi { 16 | haokanHome, 17 | haokanDramaDetail, 18 | haokanShortVideoList, 19 | bilibiliHotVideo, 20 | } 21 | 22 | enum BaseUrl { 23 | bilibili('https://api.vc.bilibili.com'), 24 | haokan('https://haokan.baidu.com'), 25 | test(''); 26 | 27 | final String url; 28 | const BaseUrl(this.url); 29 | } 30 | 31 | extension ApiOption on NetApi { 32 | /// The target's base `URL`. 33 | String get url { 34 | switch (this) { 35 | case NetApi.haokanHome: 36 | case NetApi.haokanDramaDetail: 37 | case NetApi.haokanShortVideoList: 38 | return BaseUrl.haokan.url; 39 | case NetApi.bilibiliHotVideo: 40 | return BaseUrl.bilibili.url; 41 | default: 42 | } 43 | return BaseUrl.test.url; 44 | } 45 | 46 | /// The path to be appended to `baseURL` to form the full `URL`. 47 | String get path { 48 | var path = ''; 49 | switch (this) { 50 | case NetApi.haokanHome: 51 | path = '/web/video/longpage'; 52 | break; 53 | case NetApi.haokanDramaDetail: 54 | path = '/v'; 55 | break; 56 | case NetApi.haokanShortVideoList: 57 | path = '/web/video/feed'; 58 | break; 59 | case NetApi.bilibiliHotVideo: 60 | path = '/link_draw/v2/Doc/index'; 61 | break; 62 | } 63 | return path; 64 | } 65 | 66 | /// The HTTP method used in the request. 67 | Method get method { 68 | switch (this) { 69 | default: 70 | return Method.get; 71 | } 72 | } 73 | } 74 | 75 | class ResponseCallBack { 76 | ResponseModel? model; 77 | DioError? error; 78 | 79 | ResponseCallBack(this.model, this.error); 80 | } 81 | 82 | // 限定泛型的类型 83 | class HttpManager { 84 | // 私有化构造方法 85 | HttpManager._() { 86 | var options = BaseOptions( 87 | baseUrl: BaseUrl.test.url, 88 | connectTimeout: 5000, 89 | receiveTimeout: 3000, 90 | ); 91 | dio = Dio(options); 92 | // dio.interceptors.add(LogInterceptor( 93 | // requestBody: true, 94 | // responseBody: true, 95 | // error: true, 96 | // )); 97 | } 98 | 99 | static final HttpManager _instance = HttpManager._(); 100 | 101 | static HttpManager get instance => _instance; 102 | 103 | late NetApi api; 104 | late Dio dio; 105 | 106 | late int timeOffset = 0; 107 | 108 | Map handleHeader({ 109 | required NetApi req, 110 | Map queryParams = const {}, 111 | }) { 112 | api = req; 113 | dio.options.baseUrl = api.url; 114 | var map = { 115 | 'Connection': 'keep-alive', 116 | 'User-Agent': 117 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0', 118 | }; 119 | return map; 120 | } 121 | 122 | Future sendRequest({ 123 | required NetApi req, 124 | Map queryParams = const {}, 125 | }) async { 126 | try { 127 | var headers = handleHeader(req: req, queryParams: queryParams); 128 | Response response = await dio.request( 129 | req.path, 130 | queryParameters: queryParams, 131 | options: Options( 132 | headers: headers, 133 | method: api.method.name, 134 | ), 135 | ); 136 | if (response.statusCode == HttpStatus.ok) { 137 | ResponseModel model = ResponseModel.fromJson(response.data); 138 | return ResponseCallBack(model, null); 139 | } else { 140 | return ResponseCallBack( 141 | null, 142 | DioError( 143 | requestOptions: response.requestOptions, 144 | type: DioErrorType.response, 145 | ), 146 | ); 147 | } 148 | } on DioError catch (e) { 149 | return ResponseCallBack(null, e); 150 | } 151 | } 152 | 153 | static Future request({ 154 | required NetApi req, 155 | Map queryParams = const {}, 156 | }) async { 157 | return await HttpManager.instance 158 | .sendRequest(req: req, queryParams: queryParams); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/http/http_response_model.dart: -------------------------------------------------------------------------------- 1 | class ResponseModel { 2 | String? code; 3 | String? msg; 4 | final dynamic _data; 5 | 6 | ///data数据是对象,字典,map类型 7 | Map get map { 8 | if (_data is Map) { 9 | return _data; 10 | } else { 11 | return {}; 12 | } 13 | } 14 | 15 | ///data数据是数组,list类型 16 | List get list { 17 | if (_data is List) { 18 | return _data; 19 | } else { 20 | return []; 21 | } 22 | } 23 | 24 | ///data数据是字符串类型 25 | String get string { 26 | if (_data is String) { 27 | return _data; 28 | } 29 | return ''; 30 | } 31 | 32 | ResponseModel(this.code, this.msg, this._data); 33 | factory ResponseModel.fromJson(Map json) { 34 | return ResponseModel('${json['code']}', json['msg'], json['data']); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:flutter/gestures.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:flutter_video_player/database/hv_manager.dart'; 11 | import 'pages/splash/splash_page.dart'; 12 | import 'routes/route_manager.dart'; 13 | 14 | export 'package:flutter_video_player/util/r_sources.dart'; 15 | 16 | void main() { 17 | runZonedGuarded( 18 | () async { 19 | WidgetsFlutterBinding.ensureInitialized(); 20 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, 21 | overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]); 22 | //滚动性能优化 23 | // GestureBinding.instance.resamplingEnabled = true; 24 | SystemChrome.setPreferredOrientations([ 25 | DeviceOrientation.portraitUp, 26 | ]); 27 | await HiveManager.initHive(); 28 | runApp(const MyApp()); 29 | }, 30 | (error, StackTrace stack) { 31 | // ignore: avoid_print 32 | print(error);print(stack); 33 | }, 34 | zoneSpecification: ZoneSpecification( 35 | // 拦截print 36 | print: (Zone self, ZoneDelegate parent, Zone zone, String line) { 37 | parent.print(zone, "Interceptor: $line"); 38 | }, 39 | // 拦截未处理的异步错误 40 | handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, 41 | Object error, StackTrace stackTrace) { 42 | parent.print(zone, '${error.toString()} $stackTrace'); 43 | }, 44 | ), 45 | ); 46 | } 47 | 48 | class MyApp extends StatelessWidget { 49 | const MyApp({Key? key}) : super(key: key); 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return MaterialApp( 54 | home: const SplashScreen(), 55 | onGenerateRoute: RouteManager.generateRoute, 56 | showPerformanceOverlay: false, 57 | debugShowCheckedModeBanner: false, 58 | navigatorObservers: [CFNavigatorObservers()], 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/models/models.dart: -------------------------------------------------------------------------------- 1 | // 页面传递数据,临时用的 2 | class DramaCoverModel { 3 | String? dramaId; 4 | String? coverUrl; 5 | 6 | DramaCoverModel({required this.dramaId, this.coverUrl}); 7 | } 8 | 9 | class DramaPageModel { 10 | DramaModel? drama; 11 | } 12 | 13 | class DramaModel { 14 | int? dramaId; 15 | int? seasonNo; 16 | String? area; 17 | String? brief; 18 | String? cover; 19 | String? cat; 20 | String? createTime; 21 | String? enName; 22 | String? relatedName; 23 | bool? vipFlag; 24 | double? score; 25 | String? title; 26 | String? updateTime; 27 | String? year; 28 | String? serializedStatus; 29 | String? serializedTips; 30 | String? dramaType; 31 | String? catString; 32 | bool? dramaMovie; 33 | 34 | String? dramaInfo; 35 | String? dramaTypeStr; 36 | 37 | DramaModel.fromJsom(Map map) : dramaId = map['dramaId']; 38 | } 39 | -------------------------------------------------------------------------------- /lib/models/user/user_post.dart: -------------------------------------------------------------------------------- 1 | class UserPost { 2 | int? userId; 3 | int? id; 4 | String? title; 5 | String? body; 6 | 7 | UserPost({this.userId, this.id, this.title, this.body}); 8 | 9 | factory UserPost.fromJson(Map json) => UserPost( 10 | userId: json['userId'] as int?, 11 | id: json['id'] as int?, 12 | title: json['title'] as String?, 13 | body: json['body'] as String?, 14 | ); 15 | 16 | Map toJson() => { 17 | 'userId': userId, 18 | 'id': id, 19 | 'title': title, 20 | 'body': body, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /lib/pages/cache/cache_page.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/custom/navigationbar.dart'; 5 | 6 | class CacheViewPage extends StatefulWidget { 7 | const CacheViewPage({ 8 | Key? key, 9 | }) : super(key: key); 10 | 11 | @override 12 | _CacheViewPageState createState() => _CacheViewPageState(); 13 | } 14 | 15 | class _CacheViewPageState extends State { 16 | @override 17 | void initState() { 18 | super.initState(); 19 | } 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Scaffold( 24 | appBar: UINavigationBar( 25 | title: '设置', 26 | isCenterTitle: false, 27 | tailing: [ 28 | MaterialButton( 29 | padding: EdgeInsets.zero, 30 | minWidth: 42, 31 | onPressed: () { 32 | Navigator.pop(context); 33 | }, 34 | child: const Text('编辑'), 35 | ), 36 | ], 37 | ), 38 | body: Container( 39 | color: Colors.blueGrey, 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/pages/category/model/category_model.dart: -------------------------------------------------------------------------------- 1 | class AllCategoriesGroupModel { 2 | String groupType = ''; 3 | List itemArray = []; 4 | int groupIndex = 0; 5 | String drameType = ''; 6 | 7 | AllCategoriesGroupModel.fromJson(Map map) { 8 | groupType = map['filterType']; 9 | itemArray = (map['dramaFilterItemList'] as List) 10 | .map((e) => AllCategoriesItemModel.fromJson(e)) 11 | .toList() 12 | ..first.selected = true; 13 | } 14 | } 15 | 16 | class AllCategoriesItemModel { 17 | String title = ''; 18 | String idStr = ''; 19 | bool selected = false; 20 | 21 | AllCategoriesItemModel.fromJson(Map map) 22 | : title = map['displayName'], 23 | idStr = map['value']; 24 | } 25 | -------------------------------------------------------------------------------- /lib/pages/category/view/category_item_view.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable, prefer_const_constructors 2 | 3 | import 'package:flutter/material.dart'; 4 | import '../model/category_model.dart'; 5 | 6 | /* 7 | 获取 widget位置 8 | RenderObject? box = ctx.findRenderObject(); 9 | double x = box?.getTransformTo(null).getTranslation().x ?? 0; 10 | ctx.size; 11 | */ 12 | class CategoryItemView extends StatefulWidget { 13 | CategoryItemView({ 14 | Key? key, 15 | required this.model, 16 | this.onTap, 17 | this.enable = true, 18 | }) : super(key: key); 19 | 20 | AllCategoriesGroupModel model; 21 | final ValueChanged? onTap; 22 | bool enable; 23 | 24 | @override 25 | _CategoryItemViewState createState() => _CategoryItemViewState(); 26 | } 27 | 28 | class _CategoryItemViewState extends State 29 | with SingleTickerProviderStateMixin { 30 | late TabController _tabController; 31 | @override 32 | void initState() { 33 | super.initState(); 34 | _tabController = 35 | TabController(length: widget.model.itemArray.length, vsync: this); 36 | } 37 | 38 | @override 39 | void dispose() { 40 | _tabController.dispose(); 41 | super.dispose(); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | List itemArray = widget.model.itemArray; 47 | return Theme( 48 | data: Theme.of(context).copyWith( 49 | splashColor: Colors.transparent, 50 | highlightColor: Colors.transparent, 51 | ), 52 | child: TabBar( 53 | controller: _tabController, 54 | isScrollable: true, 55 | padding: EdgeInsets.symmetric(horizontal: 6), 56 | labelPadding: EdgeInsets.symmetric(horizontal: 3, vertical: 6), 57 | indicatorWeight: 0.0001, 58 | indicatorColor: Colors.white, 59 | enableFeedback: false, 60 | onTap: !widget.enable 61 | ? null 62 | : (index) { 63 | AllCategoriesItemModel e = itemArray[index]; 64 | setState(() { 65 | for (var item in itemArray) { 66 | item.selected = false; 67 | } 68 | e.selected = true; 69 | }); 70 | widget.onTap?.call(index); 71 | }, 72 | tabs: itemArray 73 | .map( 74 | (e) => Tab( 75 | height: 32, 76 | child: Container( 77 | padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), 78 | decoration: !widget.enable 79 | ? null 80 | : BoxDecoration( 81 | color: e.selected ? Color(0xfff3f6f8) : Colors.white, 82 | borderRadius: BorderRadius.circular(16), 83 | ), 84 | child: Text( 85 | e.title, 86 | style: TextStyle( 87 | color: !widget.enable 88 | ? Color(0xffcbcdd4) 89 | : (e.selected 90 | ? Color(0xfffb6060) 91 | : Color(0xff868996)), 92 | fontSize: 14, 93 | fontWeight: !widget.enable 94 | ? FontWeight.normal 95 | : (e.selected ? FontWeight.w500 : FontWeight.normal), 96 | ), 97 | ), 98 | ), 99 | ), 100 | ) 101 | .toList(), 102 | ), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/pages/category/view/cateogry_header_view.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/pages/category/view/category_item_view.dart'; 5 | import 'package:flutter_video_player/util/util.dart'; 6 | 7 | import '../view_model/category_view_model.dart'; 8 | 9 | class CategoryHeaderView extends StatefulWidget { 10 | CategoryHeaderView({ 11 | Key? key, 12 | required this.viewModel, 13 | this.filterCallback, 14 | }) : super(key: key); 15 | 16 | CategoryViewModel viewModel; 17 | VoidCallback? filterCallback; 18 | 19 | @override 20 | _CategoryHeaderViewState createState() => _CategoryHeaderViewState(); 21 | } 22 | 23 | class _CategoryHeaderViewState extends State { 24 | @override 25 | Widget build(BuildContext context) { 26 | return SizedBox( 27 | width: Util.appWidth, 28 | height: widget.viewModel.groupArray.length * 44, 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: widget.viewModel.groupArray.map((e) { 32 | return CategoryItemView( 33 | model: e, 34 | enable: 35 | !(e.drameType == 'MOVIE' && e.groupType == 'serializedStatus'), 36 | onTap: (index) { 37 | e.groupIndex = index; 38 | var key = e.groupType; 39 | var value = e.itemArray[index].idStr; 40 | widget.viewModel.filterParams[key] = value; 41 | if (e.groupType == 'dramaType') { 42 | widget.viewModel.groupArray 43 | .firstWhere((element) => 44 | element.groupType == 'serializedStatus') 45 | .drameType = 46 | (key == 'dramaType' && value == 'MOVIE') ? 'MOVIE' : ''; 47 | } 48 | widget.filterCallback?.call(); 49 | }, 50 | ); 51 | }).toList(), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/pages/category/view_model/category_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_video_player/http/http_config.dart'; 2 | import 'package:flutter_video_player/http/http_response_model.dart'; 3 | import 'package:flutter_video_player/pages/home/model/home_model.dart'; 4 | 5 | import '../model/category_model.dart'; 6 | 7 | class CategoryViewModel { 8 | List groupArray = []; 9 | List contentArray = []; 10 | Map filterParams = {}; 11 | 12 | String get filterTitle { 13 | var str0 = ''; 14 | var str1 = ''; 15 | var str2 = ''; 16 | 17 | for (var item in groupArray) { 18 | var sectionType = item.groupType; 19 | var model = item.itemArray[item.groupIndex]; 20 | if (sectionType == 'sort') { 21 | str1 = model.title; 22 | } else if (sectionType == 'area') { 23 | str0 = model.title; 24 | } else if (sectionType == 'plotType') { 25 | str2 = model.title; 26 | } 27 | } 28 | 29 | var title = ''; 30 | if (str0 == '') { 31 | title = '$str1 · $str2'; 32 | } else { 33 | title = '$str0 · $str1 · $str2'; 34 | } 35 | return title; 36 | } 37 | 38 | /// 获取分类相关筛选项 39 | Future fetchCategory() async { 40 | ResponseModel? model = await HttpConfig.request(api: Api.categoryHeader); 41 | if (model != null) { 42 | groupArray = model.list 43 | .map( 44 | (e) => AllCategoriesGroupModel.fromJson(e), 45 | ) 46 | .toList(); 47 | } 48 | } 49 | 50 | /// 获取筛选搜索结果 51 | Future fetchCategoryContent({int page = 1}) async { 52 | ResponseModel? model = 53 | await HttpConfig.request(api: Api.categoryPage, queryParams: { 54 | 'page': page, 55 | }); 56 | if (model != null) { 57 | contentArray = 58 | model.list.map((e) => SectionContentModel.fromJson(e)).toList(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/pages/drama_detail/model/play_info_model.dart: -------------------------------------------------------------------------------- 1 | import 'video_info_model.dart'; 2 | 3 | class PlayInfoModel { 4 | PlayInfo? playInfo; 5 | List? qualityList; 6 | Quality? currentQuality; 7 | 8 | PlayInfoModel.fromJson(Map json) { 9 | playInfo = PlayInfo.fromJson(json['playInfo']); 10 | try { 11 | qualityList = 12 | (((json['qualityList'] is List ? json['qualityList'] : []) as List) 13 | .map((e) => Quality.fromJson(e))).toList(); 14 | if (qualityList?.isNotEmpty == true) { 15 | currentQuality = 16 | qualityList?.firstWhere((element) => element.quality == 'LD'); 17 | } 18 | } catch (e) { 19 | // (e); 20 | } 21 | } 22 | } 23 | 24 | class Quality { 25 | Quality({ 26 | this.quality, 27 | this.qualityName, 28 | this.vipFlag, 29 | this.qualityShortName, 30 | this.qualityCode, 31 | this.qualityDescription, 32 | }); 33 | 34 | String? quality; 35 | String? qualityName; 36 | bool? vipFlag; 37 | String? qualityDescription; 38 | String? qualityShortName; 39 | String? qualityCode; 40 | 41 | factory Quality.fromJson(Map json) { 42 | return Quality( 43 | vipFlag: json["vipFlag"], 44 | qualityDescription: json["qualityDescription"], 45 | quality: json["quality"], 46 | qualityName: json["qualityName"], 47 | qualityShortName: json["qualityShortName"], 48 | qualityCode: json["qualityCode"], 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/pages/drama_detail/model/video_info_model.dart: -------------------------------------------------------------------------------- 1 | class VideoInfoModel { 2 | List? seriesList; 3 | List? episodeList; 4 | DramaInfo? drama; 5 | PlayInfo? playInfo; 6 | 7 | Series? currentSeries; 8 | Episode? currentEpisode; 9 | Episode? nextEpisode; 10 | 11 | VideoInfoModel.fromJson(Map json) { 12 | drama = DramaInfo.fromJson(json['drama']); 13 | playInfo = PlayInfo.fromJson(json['playInfo']); 14 | try { 15 | episodeList = 16 | (((json['episodeList'] is List ? json['episodeList'] : []) as List) 17 | .map((e) => Episode.fromJson(e))).toList(); 18 | seriesList = 19 | (((json['seriesList'] is List ? json['seriesList'] : []) as List) 20 | .map((e) => Series.fromJson(e))).toList(); 21 | 22 | if (seriesList?.isNotEmpty == true) { 23 | currentSeries = seriesList?.first; 24 | } 25 | if (episodeList?.isNotEmpty == true) { 26 | currentEpisode = episodeList?.firstWhere( 27 | (element) => element.episodeSid == playInfo?.episodeSid); 28 | } 29 | } catch (e) { 30 | //(e); 31 | } 32 | } 33 | } 34 | 35 | class DramaInfo { 36 | DramaInfo({ 37 | this.id, 38 | this.createTime, 39 | this.title, 40 | this.cover, 41 | this.brief, 42 | this.score, 43 | this.year, 44 | this.area, 45 | this.enName, 46 | this.cat, 47 | this.dramaType, 48 | this.serializedStatus, 49 | this.episodeCount, 50 | }); 51 | 52 | int? id; 53 | String? createTime; 54 | String? title; 55 | String? cover; 56 | String? brief; 57 | double? score; 58 | String? year; 59 | String? area; 60 | String? enName; 61 | String? cat; 62 | String? dramaType; 63 | String? serializedStatus; 64 | int? episodeCount; 65 | 66 | String get info => '$dramaType/$year/$area/$cat'; 67 | 68 | factory DramaInfo.fromJson(Map json) => DramaInfo( 69 | id: json["id"], 70 | createTime: json["createTime"], 71 | title: json["title"], 72 | cover: json["cover"], 73 | brief: json["brief"], 74 | score: json["score"].toDouble(), 75 | year: json["year"], 76 | area: json["area"], 77 | enName: json["enName"], 78 | cat: json["cat"], 79 | dramaType: json["dramaType"], 80 | serializedStatus: json["serializedStatus"], 81 | episodeCount: json["episodeCount"], 82 | ); 83 | 84 | Map toJson() => { 85 | "id": id, 86 | "createTime": createTime, 87 | "title": title, 88 | "cover": cover, 89 | "brief": brief, 90 | "score": score, 91 | "year": year, 92 | "area": area, 93 | "enName": enName, 94 | "cat": cat, 95 | "dramaType": dramaType, 96 | "serializedStatus": serializedStatus, 97 | "episodeCount": episodeCount, 98 | }; 99 | } 100 | 101 | class Series { 102 | Series({ 103 | this.dramaId, 104 | this.seasonNo, 105 | this.relatedName, 106 | this.id, 107 | }); 108 | 109 | int? dramaId; 110 | int? seasonNo; 111 | String? relatedName; 112 | int? id; 113 | 114 | bool isSelected = false; 115 | 116 | factory Series.fromJson(Map json) => Series( 117 | dramaId: json["dramaId"], 118 | seasonNo: json["seasonNo"], 119 | relatedName: json["relatedName"], 120 | id: json["id"], 121 | ); 122 | 123 | Map toJson() => { 124 | "dramaId": dramaId, 125 | "seasonNo": seasonNo, 126 | "relatedName": relatedName, 127 | "id": id, 128 | }; 129 | } 130 | 131 | class Episode { 132 | Episode({ 133 | this.episodeNo, 134 | this.text, 135 | this.episodeSid, 136 | }); 137 | 138 | int? episodeNo; 139 | String? text; 140 | int? episodeSid; 141 | bool isSelected = false; 142 | 143 | factory Episode.fromJson(Map json) => Episode( 144 | episodeNo: json["episodeNo"], 145 | text: json["text"], 146 | episodeSid: json["episodeSid"], 147 | ); 148 | 149 | Map toJson() => { 150 | "episodeNo": episodeNo, 151 | "text": text, 152 | "episodeSid": episodeSid, 153 | }; 154 | } 155 | 156 | class PlayInfo { 157 | PlayInfo({ 158 | this.mediaId, 159 | this.episodeSid, 160 | this.url, 161 | this.startingLength, 162 | this.currentQuality, 163 | }); 164 | 165 | int? mediaId; 166 | int? episodeSid; 167 | String? url; 168 | String? currentQuality; 169 | int? startingLength; 170 | 171 | factory PlayInfo.fromJson(Map json) { 172 | return PlayInfo( 173 | url: json["url"], 174 | currentQuality: json["currentQuality"], 175 | mediaId: json["mediaId"], 176 | episodeSid: json["episodeSid"], 177 | startingLength: json["startingLength"], 178 | ); 179 | } 180 | 181 | Map toJson() => { 182 | "url": url, 183 | "currentQuality": currentQuality, 184 | "mediaId": mediaId, 185 | "episodeSid": episodeSid, 186 | "startingLength": startingLength, 187 | }; 188 | } 189 | -------------------------------------------------------------------------------- /lib/pages/drama_detail/view_model/drama_detail_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_video_player/http/http_config.dart'; 2 | // import 'package:flutter_video_player/http/http_manager.dart'; 3 | import 'package:flutter_video_player/http/http_response_model.dart'; 4 | import 'package:flutter_video_player/pages/drama_detail/model/play_info_model.dart'; 5 | import 'package:flutter_video_player/pages/drama_detail/model/video_info_model.dart'; 6 | 7 | class DramaDetailViewModel { 8 | VideoInfoModel? videoInfoModel; 9 | PlayInfoModel? playInfoModel; 10 | 11 | /// 请求剧集信息 12 | Future requestData({required String dramaId}) async { 13 | // if (dramaId.length > 10) { 14 | // ResponseCallBack? data = await HttpManager.request(req: NetApi.haokanDramaDetail, queryParams: { 15 | // 'vid': dramaId, 16 | // '_format': 'json', 17 | // },); 18 | // if (data.model != null) { 19 | 20 | // } 21 | // } 22 | ResponseModel? model = await HttpConfig.request(api: Api.dramaDetail); 23 | if (model != null) { 24 | videoInfoModel = VideoInfoModel.fromJson(model.map); 25 | } 26 | return videoInfoModel; 27 | } 28 | 29 | /// 请求播放数据 30 | Future requestPlayInfo({ 31 | required String? dramaId, 32 | required int? episodeId, 33 | String? quality = 'LD', 34 | }) async { 35 | ResponseModel? model = 36 | await HttpConfig.request(api: Api.dramaPlay, queryParams: { 37 | 'dramaId': dramaId, 38 | 'episodeId': '${episodeId! - 10000}', 39 | }); 40 | if (model != null) { 41 | playInfoModel = PlayInfoModel.fromJson(model.map); 42 | } 43 | 44 | videoInfoModel?.currentEpisode = videoInfoModel?.episodeList?.firstWhere( 45 | (element) => element.episodeSid == playInfoModel?.playInfo?.episodeSid); 46 | 47 | int index = videoInfoModel?.episodeList?.indexWhere((element) => 48 | element.episodeSid == videoInfoModel?.currentEpisode?.episodeSid) ?? 49 | 0; 50 | if (videoInfoModel?.currentEpisode != null && 51 | (index + 1) < (videoInfoModel?.episodeList?.length ?? 0)) { 52 | videoInfoModel?.nextEpisode = videoInfoModel?.episodeList?[index + 1]; 53 | } else { 54 | videoInfoModel?.nextEpisode = null; 55 | } 56 | 57 | return playInfoModel; 58 | } 59 | 60 | @override 61 | bool operator ==(Object other) { 62 | if (other is DramaDetailViewModel) { 63 | return false; 64 | } 65 | return false; 66 | } 67 | 68 | @override 69 | // ignore: unnecessary_overrides 70 | int get hashCode => super.hashCode; 71 | } 72 | -------------------------------------------------------------------------------- /lib/pages/drama_detail/views/small_screen_placeholder_view.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'dart:ui' show ImageFilter; 5 | 6 | class SmallScreenPlayerPlaceHolderView extends StatelessWidget { 7 | const SmallScreenPlayerPlaceHolderView({ 8 | Key? key, 9 | required this.coverUrl, 10 | }) : super(key: key); 11 | 12 | final String coverUrl; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | var width = MediaQuery.of(context).size.width; 17 | var height = width * 0.56; 18 | return SizedBox( 19 | height: height, 20 | child: Stack( 21 | children: [ 22 | Positioned( 23 | child: Hero( 24 | tag: coverUrl, 25 | child: Image.network( 26 | coverUrl, 27 | width: width, 28 | height: height, 29 | cacheWidth: width.toInt(), 30 | cacheHeight: height.toInt(), 31 | fit: BoxFit.cover, 32 | ), 33 | ), 34 | ), 35 | Positioned( 36 | top: 0, 37 | left: 0, 38 | right: 0, 39 | height: height, 40 | child: ClipRect( 41 | child: BackdropFilter( 42 | filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), 43 | blendMode: BlendMode.modulate, 44 | child: Container( 45 | color: Colors.black.withOpacity(0.3), 46 | ), 47 | ), 48 | ), 49 | ), 50 | Center( 51 | child: ClipRRect( 52 | borderRadius: BorderRadius.circular(4), 53 | child: Image.network( 54 | coverUrl, 55 | height: height - 52, 56 | ), 57 | ), 58 | ), 59 | ], 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/pages/drama_detail/views/video_brief_detail_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_video_player/pages/drama_detail/model/video_info_model.dart'; 3 | 4 | class VideoBriefDetailView extends StatelessWidget { 5 | const VideoBriefDetailView({ 6 | Key? key, 7 | required this.model, 8 | }) : super(key: key); 9 | 10 | final VideoInfoModel? model; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | color: Colors.white, 16 | padding: const EdgeInsets.all(12), 17 | child: Column( 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | Padding( 21 | padding: const EdgeInsets.only(bottom: 12), 22 | child: Row( 23 | children: [ 24 | const Text( 25 | '简介', 26 | maxLines: 1, 27 | style: TextStyle( 28 | fontSize: 16, 29 | fontWeight: FontWeight.w500, 30 | color: Color(0xff32383a), 31 | ), 32 | ), 33 | const Spacer(), 34 | GestureDetector( 35 | child: const Icon(Icons.close), 36 | onTap: () { 37 | Navigator.pop(context); 38 | }, 39 | ) 40 | ], 41 | )), 42 | Padding( 43 | padding: const EdgeInsets.only(right: 12, bottom: 6), 44 | child: Text( 45 | model?.drama?.title ?? '剧名', 46 | maxLines: 2, 47 | style: const TextStyle( 48 | fontSize: 24, 49 | fontWeight: FontWeight.w500, 50 | color: Color(0xff3b4051), 51 | ), 52 | ), 53 | ), 54 | Padding( 55 | padding: EdgeInsets.zero, 56 | child: Text( 57 | model?.drama?.enName ?? '', 58 | maxLines: 1, 59 | style: const TextStyle( 60 | fontSize: 10, 61 | color: Color(0xffadb6c2), 62 | ), 63 | ), 64 | ), 65 | Padding( 66 | padding: const EdgeInsets.only(right: 12, top: 4, bottom: 24), 67 | child: Row( 68 | crossAxisAlignment: CrossAxisAlignment.start, 69 | children: [ 70 | Padding( 71 | padding: const EdgeInsets.only(top: 4), 72 | child: Text( 73 | model?.drama?.info ?? '', 74 | maxLines: 1, 75 | style: const TextStyle( 76 | fontSize: 10, 77 | color: Color(0xffadb6c2), 78 | ), 79 | ), 80 | ), 81 | ], 82 | ), 83 | ), 84 | Padding( 85 | padding: const EdgeInsets.only(right: 10, bottom: 28), 86 | child: Text( 87 | model?.drama?.brief ?? '', 88 | style: const TextStyle( 89 | fontSize: 14, 90 | color: Color(0xff868996), 91 | height: 1.5, 92 | ), 93 | ), 94 | ), 95 | ], 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/pages/drama_detail/views/video_episode_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_video_player/pages/drama_detail/model/video_info_model.dart'; 3 | 4 | import '../../../util/util.dart'; 5 | 6 | class ExplosidListView extends StatefulWidget { 7 | const ExplosidListView({ 8 | Key? key, 9 | required this.model, 10 | }) : super(key: key); 11 | 12 | final VideoInfoModel? model; 13 | 14 | @override 15 | _ExplosidListViewState createState() => _ExplosidListViewState(); 16 | } 17 | 18 | class _ExplosidListViewState extends State { 19 | VideoInfoModel? get model => widget.model; 20 | 21 | bool get hasText => (model?.episodeList?.first.text?.length ?? 0) > 2; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var serializedStatus = model?.drama?.serializedStatus?.isNotEmpty == true 26 | ? model?.drama?.serializedStatus 27 | : '未开播'; 28 | return Container( 29 | color: Colors.white, 30 | padding: const EdgeInsets.all(12), 31 | child: Column( 32 | crossAxisAlignment: CrossAxisAlignment.start, 33 | children: [ 34 | Padding( 35 | padding: const EdgeInsets.only(bottom: 12), 36 | child: Row( 37 | children: [ 38 | const Text( 39 | '简介', 40 | maxLines: 1, 41 | style: TextStyle( 42 | fontSize: 16, 43 | fontWeight: FontWeight.w500, 44 | color: Color(0xff32383a), 45 | ), 46 | ), 47 | const Spacer(), 48 | GestureDetector( 49 | child: const Icon(Icons.close), 50 | onTap: () { 51 | Navigator.pop(context); 52 | }, 53 | ) 54 | ], 55 | )), 56 | Padding( 57 | padding: const EdgeInsets.only(bottom: 8), 58 | child: Text( 59 | serializedStatus ?? '', 60 | maxLines: 1, 61 | style: const TextStyle( 62 | fontSize: 10, 63 | color: Color(0xffadb6c2), 64 | ), 65 | ), 66 | ), 67 | Expanded( 68 | child: GridView.builder( 69 | padding: EdgeInsets.zero, 70 | gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( 71 | maxCrossAxisExtent: hasText ? Util.appWidth - 24 : 54, 72 | mainAxisSpacing: 6, 73 | crossAxisSpacing: 6, 74 | childAspectRatio: hasText ? ((Util.appWidth - 24) / 54) : 1, 75 | ), 76 | itemCount: model?.episodeList?.length ?? 0, 77 | itemBuilder: (ctx, index) { 78 | Episode? episode = model?.episodeList?[index]; 79 | String? text = 80 | hasText ? episode?.text : '${episode?.episodeNo}'; 81 | episode?.isSelected = 82 | (model?.currentEpisode?.episodeSid == episode.episodeSid); 83 | Widget textWidget = Text( 84 | '$text', 85 | maxLines: 2, 86 | softWrap: false, 87 | overflow: TextOverflow.ellipsis, 88 | style: TextStyle( 89 | fontSize: 16, 90 | fontWeight: FontWeight.w500, 91 | color: episode?.isSelected == true 92 | ? const Color(0xffFB6060) 93 | : const Color(0xff3b4051), 94 | ), 95 | ); 96 | return Container( 97 | clipBehavior: Clip.hardEdge, 98 | padding: !hasText 99 | ? EdgeInsets.zero 100 | : const EdgeInsets.symmetric(horizontal: 20), 101 | alignment: hasText ? Alignment.centerLeft : Alignment.center, 102 | decoration: BoxDecoration( 103 | color: const Color(0xfff3f6f8), 104 | borderRadius: BorderRadius.circular(4), 105 | ), 106 | child: (episode?.isSelected == true && !hasText) 107 | ? Image.asset(R.Img.ic_playing) 108 | : textWidget, 109 | ); 110 | }, 111 | ), 112 | ), 113 | ], 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/pages/drama_list/drama_list_page.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter_video_player/custom/navigationbar.dart'; 6 | import 'package:flutter_video_player/http/http_response_model.dart'; 7 | import 'package:flutter_video_player/models/models.dart'; 8 | import 'package:flutter_video_player/pages/home/model/home_model.dart'; 9 | import 'package:flutter_video_player/pages/home/views/cell_list.dart'; 10 | 11 | import '../../abstracts/abstract_interface.dart'; 12 | 13 | class DramaListPageView extends StatefulWidget { 14 | DramaListPageView({ 15 | Key? key, 16 | required this.model, 17 | }) : super(key: key); 18 | 19 | DramaCoverModel? model; 20 | 21 | @override 22 | _DramaListPageViewState createState() => _DramaListPageViewState(); 23 | } 24 | 25 | class _DramaListPageViewState extends State with Request { 26 | late List itemArray = []; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark); 32 | requestData(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Scaffold( 38 | appBar: UINavigationBar( 39 | title: widget.model?.coverUrl, 40 | leading: [ 41 | MaterialButton( 42 | padding: EdgeInsets.zero, 43 | minWidth: 42, 44 | onPressed: () { 45 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light); 46 | Navigator.pop(context); 47 | }, 48 | child: const Icon( 49 | Icons.arrow_back_ios_new, 50 | size: 24, 51 | color: Color(0xff32383a), 52 | ), 53 | ), 54 | ], 55 | ), 56 | body: GridView.builder( 57 | padding: const EdgeInsets.only(left: 12, right: 12), 58 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 59 | crossAxisCount: 3, 60 | mainAxisSpacing: 0, 61 | crossAxisSpacing: 6, 62 | childAspectRatio: 0.564, 63 | ), 64 | itemCount: itemArray.length, 65 | itemBuilder: (ctx, index) => ListCell( 66 | model: itemArray[index], 67 | ), 68 | ), 69 | ); 70 | } 71 | 72 | @override 73 | Future handleData(ResponseModel model) async { 74 | setState(() { 75 | var list = model.map['content']; 76 | if (list is List) { 77 | for (var element in list) { 78 | itemArray.add(SectionContentModel.fromJson(element)); 79 | } 80 | } 81 | }); 82 | } 83 | 84 | @override 85 | Future requestData({ 86 | int pageNum = 1, 87 | Map queryParams = const {}, 88 | }) async { 89 | // final url = widget.model?.dramaId ?? ''; 90 | // final decoder = Uri.decodeFull(url); 91 | // final uri = Uri.parse(decoder); 92 | // final seriesId = uri.queryParameters['seriesId'] ?? ''; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/haokan_home/haokan_home_model.dart: -------------------------------------------------------------------------------- 1 | 2 | class HomeResponseModel { 3 | HomeResponseModel({ 4 | this.pageData, 5 | this.hasMore, 6 | }); 7 | 8 | final List? pageData; 9 | final int? hasMore; 10 | 11 | factory HomeResponseModel.fromJson(Map json) => HomeResponseModel( 12 | pageData: List.from(json["page_data"].map((x) => DramaItemModel.fromJson(x))), 13 | hasMore: json["has_more"], 14 | ); 15 | } 16 | 17 | class DramaItemModel { 18 | DramaItemModel({ 19 | this.videoName, 20 | this.verticalImage, 21 | this.seriesNum, 22 | this.introduction, 23 | this.firstEpisodes, 24 | this.sumPlayCnt, 25 | this.horizontalImage, 26 | this.isFinish, 27 | this.previewUrlHttp, 28 | this.sumPlayCntText, 29 | }); 30 | 31 | final String? videoName; 32 | final String? verticalImage; 33 | final String? seriesNum; 34 | final String? introduction; 35 | final String? firstEpisodes; 36 | final String? sumPlayCnt; 37 | final String? horizontalImage; 38 | final String? isFinish; 39 | final String? previewUrlHttp; 40 | final String? sumPlayCntText; 41 | 42 | factory DramaItemModel.fromJson(Map json) => DramaItemModel( 43 | videoName: json["videoName"], 44 | verticalImage: json["verticalImage"], 45 | seriesNum: json["seriesNum"], 46 | introduction: json["introduction"], 47 | firstEpisodes: json["firstEpisodes"], 48 | sumPlayCnt: json["sumPlayCnt"], 49 | horizontalImage: json["horizontalImage"], 50 | isFinish: json["isFinish"], 51 | previewUrlHttp: json["previewUrlHttp"], 52 | sumPlayCntText: json["sumPlayCntText"], 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/haokan_home/haokan_home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../../util/util.dart'; 3 | import 'haokan_home_view_model.dart'; 4 | import 'haokan_tab_page.dart'; 5 | 6 | class HaoKanHomePage extends StatelessWidget { 7 | const HaoKanHomePage({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return DefaultTabController( 12 | length: DramaType.values.length, 13 | child: Scaffold( 14 | // backgroundColor: const Color(0xff2d2d2d), 15 | appBar: AppBar( 16 | toolbarHeight: Util.navBarHeight, 17 | backgroundColor: const Color(0xff2d2d2d), 18 | leadingWidth: 200, 19 | elevation: 0.1, 20 | title: Row( 21 | children: [ 22 | Expanded( 23 | child: TabBar( 24 | isScrollable: true, 25 | indicatorColor: const Color(0xFFF93759), 26 | indicatorWeight: 6, 27 | indicatorPadding: kTabLabelPadding, 28 | enableFeedback: false, 29 | splashFactory: NoSplash.splashFactory, 30 | overlayColor: MaterialStateProperty.resolveWith( 31 | (Set states) { 32 | return states.contains(MaterialState.focused) 33 | ? null 34 | : Colors.transparent; 35 | }, 36 | ), 37 | labelColor: const Color(0xffF93759), 38 | labelStyle: const TextStyle( 39 | fontSize: 15, 40 | fontWeight: FontWeight.bold, 41 | ), 42 | unselectedLabelColor: const Color(0xff8d8d8d), 43 | unselectedLabelStyle: const TextStyle( 44 | fontSize: 12, 45 | ), 46 | tabs: DramaType.values 47 | .map( 48 | (e) => Tab(text: e.name), 49 | ) 50 | .toList(), 51 | ), 52 | ), 53 | ], 54 | ), 55 | ), 56 | body: TabBarView( 57 | children: DramaType.values 58 | .map( 59 | (e) => HaoKanHomeTabPage(type: e), 60 | ) 61 | .toList(), 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/haokan_home/haokan_home_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../http/http_manager.dart'; 2 | import 'haokan_home_model.dart'; 3 | 4 | 5 | enum DramaType { 6 | ///全部 7 | all(0, '全部'), 8 | 9 | ///古装剧 10 | costume(1, '古装剧'), 11 | 12 | ///家庭剧 13 | family(2, '家庭剧'), 14 | 15 | ///爱情剧 16 | love(3, '爱情剧'), 17 | 18 | ///悬疑剧 19 | crux(4, '悬疑剧'), 20 | 21 | ///武侠剧 22 | kungfu(5, '武侠剧'), 23 | 24 | ///喜剧 25 | comedy(6, '喜剧'), 26 | 27 | ///战争剧 28 | war(7, '战争剧'); 29 | 30 | final int value; 31 | final String name; 32 | 33 | const DramaType(this.value, this.name); 34 | } 35 | 36 | class HaoKanHomeViewModel { 37 | late List pageModelList = []; 38 | 39 | int pageNumber = 1; 40 | 41 | Future requestData({ 42 | required DramaType type, 43 | int pageNum = 1, 44 | }) async { 45 | ResponseCallBack data = await HttpManager.request( 46 | req: NetApi.haokanHome, 47 | queryParams: { 48 | 'rn': 20, 49 | 'pn': pageNum, 50 | 'type': type.value, 51 | }, 52 | ); 53 | pageNumber = pageNum + 1; 54 | 55 | if (pageNum == 1) { 56 | pageModelList.clear(); 57 | } 58 | 59 | if (data.model == null) { 60 | return []; 61 | } 62 | 63 | HomeResponseModel resModel = 64 | HomeResponseModel.fromJson(data.model!.map['response']); 65 | pageModelList.addAll(resModel.pageData ?? []); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/haokan_home/haokan_tab_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 3 | import '../../../models/models.dart'; 4 | import '../../../routes/route_manager.dart'; 5 | import '../../home/model/home_model.dart'; 6 | import '../../home/views/cell_list.dart'; 7 | import 'haokan_home_model.dart'; 8 | import 'haokan_home_view_model.dart'; 9 | 10 | class HaoKanHomeTabPage extends StatefulWidget { 11 | const HaoKanHomeTabPage({ 12 | Key? key, 13 | required this.type, 14 | }) : super(key: key); 15 | 16 | final DramaType type; 17 | 18 | @override 19 | State createState() => _HaoKanHomeTabPageState(); 20 | } 21 | 22 | class _HaoKanHomeTabPageState extends State 23 | with AutomaticKeepAliveClientMixin { 24 | late HaoKanHomeViewModel viewModel; 25 | 26 | late final RefreshController _controller; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | viewModel = HaoKanHomeViewModel(); 32 | _controller = RefreshController(initialRefresh: false); 33 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 34 | viewModel.requestData(type: widget.type).then((value) { 35 | if (mounted) { 36 | setState(() {}); 37 | } 38 | }); 39 | }); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | _controller.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | super.build(context); 51 | return SmartRefresher( 52 | enablePullUp: true, 53 | controller: _controller, 54 | onRefresh: () async { 55 | viewModel.requestData(type: widget.type, pageNum: 1).then((value) { 56 | if (mounted) { 57 | _controller.refreshCompleted(); 58 | setState(() {}); 59 | } 60 | }); 61 | }, 62 | onLoading: () async { 63 | viewModel 64 | .requestData(type: widget.type, pageNum: viewModel.pageNumber) 65 | .then((value) { 66 | if (mounted) { 67 | _controller.loadComplete(); 68 | setState(() {}); 69 | } 70 | }); 71 | }, 72 | child: GridView.builder( 73 | padding: const EdgeInsets.all(12), 74 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 75 | crossAxisCount: 3, 76 | mainAxisSpacing: 0, 77 | crossAxisSpacing: 6, 78 | childAspectRatio: 0.564, 79 | ), 80 | itemCount: viewModel.pageModelList.length, 81 | itemBuilder: (ctx, index) { 82 | DramaItemModel model = viewModel.pageModelList[index]; 83 | return GestureDetector( 84 | onTap: () { 85 | Navigator.pushNamed( 86 | context, 87 | RouteManager.dramaDetail, 88 | arguments: DramaCoverModel( 89 | dramaId: model.firstEpisodes, 90 | coverUrl: model.verticalImage, 91 | ), 92 | ); 93 | }, 94 | child: ListCell( 95 | model: SectionContentModel( 96 | int.tryParse('${model.firstEpisodes}'), 97 | model.videoName, 98 | model.verticalImage, 99 | '${model.seriesNum}集${model.isFinish == '1' ? '全' : ' 连载中'}', 100 | ), 101 | ), 102 | ); 103 | }, 104 | ), 105 | ); 106 | } 107 | 108 | @override 109 | bool get wantKeepAlive => true; 110 | } 111 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/haokan_video_detail/haokan_video_detail_model.dart: -------------------------------------------------------------------------------- 1 | class Data { 2 | Data({ 3 | this.apiData, 4 | }); 5 | 6 | final ApiData? apiData; 7 | 8 | factory Data.fromJson(Map json) => Data( 9 | apiData: ApiData.fromJson(json["apiData"]), 10 | ); 11 | } 12 | 13 | class ApiData { 14 | ApiData({ 15 | this.curVideoMeta, 16 | this.longVideoSideBar, 17 | }); 18 | 19 | final CurVideoMeta? curVideoMeta; 20 | final LongVideoSideBar? longVideoSideBar; 21 | 22 | factory ApiData.fromJson(Map json) => ApiData( 23 | curVideoMeta: CurVideoMeta.fromJson(json["curVideoMeta"]), 24 | longVideoSideBar: LongVideoSideBar.fromJson(json["longVideoSideBar"]), 25 | ); 26 | } 27 | 28 | class CurVideoMeta { 29 | CurVideoMeta({ 30 | this.id, 31 | this.title, 32 | this.poster, 33 | this.fmplaycnt, 34 | this.date, 35 | this.timeLength, 36 | this.duration, 37 | this.playurl, 38 | this.clarityUrl, 39 | this.isLongVideo, 40 | this.dramaInfo, 41 | this.allowPlay, 42 | }); 43 | 44 | final String? id; 45 | final String? title; 46 | final String? poster; 47 | final String? fmplaycnt; 48 | final String? date; 49 | final String? timeLength; 50 | final int? duration; 51 | final String? playurl; 52 | final List? clarityUrl; 53 | final bool? isLongVideo; 54 | final DramaInfo? dramaInfo; 55 | final int? allowPlay; 56 | 57 | factory CurVideoMeta.fromJson(Map json) => CurVideoMeta( 58 | id: json["id"], 59 | title: json["title"], 60 | poster: json["poster"], 61 | fmplaycnt: json["fmplaycnt"], 62 | date: json["date"], 63 | timeLength: json["time_length"], 64 | duration: json["duration"], 65 | playurl: json["playurl"], 66 | clarityUrl: List.from( 67 | json["clarityUrl"].map((x) => ClarityUrl.fromJson(x))), 68 | isLongVideo: json["isLongVideo"], 69 | dramaInfo: DramaInfo.fromJson(json["dramaInfo"]), 70 | allowPlay: json["allow_play"], 71 | ); 72 | } 73 | 74 | ///当前播放信息模型 75 | class ClarityUrl { 76 | ClarityUrl({ 77 | this.key, 78 | this.rank, 79 | this.title, 80 | this.url, 81 | this.videoBps, 82 | this.vodVideoHw, 83 | this.videoSize, 84 | this.vodMoovSize, 85 | this.codecType, 86 | }); 87 | 88 | final String? key; 89 | final int? rank; 90 | final String? title; 91 | final String? url; 92 | final int? videoBps; 93 | final String? vodVideoHw; 94 | final int? videoSize; 95 | final int? vodMoovSize; 96 | final String? codecType; 97 | 98 | factory ClarityUrl.fromJson(Map json) => ClarityUrl( 99 | key: json["key"], 100 | rank: json["rank"], 101 | title: json["title"], 102 | url: json["url"], 103 | videoBps: json["videoBps"], 104 | vodVideoHw: json["vodVideoHW"], 105 | videoSize: json["videoSize"], 106 | vodMoovSize: json["vodMoovSize"], 107 | codecType: json["codec_type"], 108 | ); 109 | 110 | Map toJson() => { 111 | "key": key, 112 | "rank": rank, 113 | "title": title, 114 | "url": url, 115 | "videoBps": videoBps, 116 | "vodVideoHW": vodVideoHw, 117 | "videoSize": videoSize, 118 | "vodMoovSize": vodMoovSize, 119 | "codec_type": codecType, 120 | }; 121 | } 122 | 123 | /// 影视信息模型 124 | class DramaInfo { 125 | DramaInfo({ 126 | this.videoName, 127 | this.seriesNum, 128 | this.verticalImage, 129 | this.sumPlayCnt, 130 | this.typeValue, 131 | this.typeName, 132 | this.introduction, 133 | this.currentNum, 134 | this.isFinish, 135 | }); 136 | 137 | final String? videoName; 138 | final String? seriesNum; 139 | final String? verticalImage; 140 | final String? sumPlayCnt; 141 | final String? typeValue; 142 | final String? typeName; 143 | final String? introduction; 144 | final String? currentNum; 145 | final String? isFinish; 146 | 147 | factory DramaInfo.fromJson(Map json) => DramaInfo( 148 | videoName: json["videoName"], 149 | seriesNum: json["seriesNum"], 150 | verticalImage: json["verticalImage"], 151 | sumPlayCnt: json["sumPlayCnt"], 152 | typeValue: json["typeValue"], 153 | typeName: json["typeName"], 154 | introduction: json["introduction"], 155 | currentNum: json["currentNum"], 156 | isFinish: json["isFinish"], 157 | ); 158 | } 159 | 160 | ///选集信息模型 161 | class LongVideoSideBar { 162 | LongVideoSideBar({ 163 | required this.episodes, 164 | }); 165 | 166 | final List episodes; 167 | 168 | factory LongVideoSideBar.fromJson(Map json) => 169 | LongVideoSideBar( 170 | episodes: List.from( 171 | json["episodes"].map((x) => Episode.fromJson(x))), 172 | ); 173 | } 174 | 175 | ///集信息模型 176 | class Episode { 177 | Episode({ 178 | this.no, 179 | this.vid, 180 | }); 181 | 182 | final String? no; 183 | final String? vid; 184 | 185 | factory Episode.fromJson(Map json) => Episode( 186 | no: json["no"], 187 | vid: json["vid"], 188 | ); 189 | } 190 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/short_video/haokan_short_video_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HaoKanShortVideoModel { 4 | HaoKanShortVideoModel({ 5 | this.videos, 6 | }); 7 | 8 | final List? videos; 9 | 10 | factory HaoKanShortVideoModel.fromJson(Map json) => 11 | HaoKanShortVideoModel( 12 | videos: List.from( 13 | json["videos"].map( 14 | (x) => ShortVideoItem.fromJson(x), 15 | ), 16 | ), 17 | ); 18 | } 19 | 20 | class ShortVideoItem { 21 | ShortVideoItem({ 22 | this.id, 23 | this.title, 24 | this.posterSmall, 25 | this.posterBig, 26 | this.posterPc, 27 | this.sourceName, 28 | this.playUrl, 29 | this.duration, 30 | this.url, 31 | this.showTag, 32 | this.publishTime, 33 | this.isPayColumn, 34 | this.like, 35 | this.comment, 36 | this.playcnt, 37 | this.fmplaycnt, 38 | this.fmplaycnt2, 39 | this.outstandTag, 40 | this.previewUrlHttp, 41 | this.thirdId, 42 | this.vip, 43 | this.authorAvatar, 44 | }); 45 | 46 | final String? id; 47 | final String? title; 48 | final String? posterSmall; 49 | final String? posterBig; 50 | final String? posterPc; 51 | final String? sourceName; 52 | final String? playUrl; 53 | final String? duration; 54 | final String? url; 55 | final int? showTag; 56 | final String? publishTime; 57 | final int? isPayColumn; 58 | final String? like; 59 | final String? comment; 60 | final String? playcnt; 61 | final String? fmplaycnt; 62 | final String? fmplaycnt2; 63 | final String? outstandTag; 64 | final String? previewUrlHttp; 65 | final String? thirdId; 66 | final int? vip; 67 | final String? authorAvatar; 68 | 69 | Size get size { 70 | List? list = posterSmall?.split(','); 71 | String? ws = 72 | list?.firstWhere((element) => element.startsWith('w_')).substring(2) ?? 73 | '1'; 74 | String? hs = 75 | list?.firstWhere((element) => element.startsWith('h_')).substring(2) ?? 76 | '1'; 77 | double w = double.tryParse(ws) ?? 1; 78 | double h = double.tryParse(hs) ?? 1; 79 | return Size(w, h); 80 | } 81 | 82 | double get aspectRatio => size.width / size.height; 83 | 84 | factory ShortVideoItem.fromJson(Map json) => ShortVideoItem( 85 | id: json["id"], 86 | title: json["title"], 87 | posterSmall: json["poster_small"], 88 | posterBig: json["poster_big"], 89 | posterPc: json["poster_pc"], 90 | sourceName: json["source_name"], 91 | playUrl: json["play_url"], 92 | duration: json["duration"], 93 | url: json["url"], 94 | showTag: json["show_tag"], 95 | publishTime: json["publish_time"], 96 | isPayColumn: json["is_pay_column"], 97 | like: json["like"], 98 | comment: json["comment"], 99 | playcnt: json["playcnt"], 100 | fmplaycnt: json["fmplaycnt"], 101 | fmplaycnt2: json["fmplaycnt_2"], 102 | outstandTag: json["outstand_tag"], 103 | previewUrlHttp: json["previewUrlHttp"], 104 | thirdId: json["third_id"], 105 | vip: json["vip"], 106 | authorAvatar: json["author_avatar"], 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/short_video/haokan_short_video_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'haokan_short_video_item_view.dart'; 3 | import 'haokan_short_video_view_model.dart'; 4 | 5 | class HaoKanShortVideoPage extends StatefulWidget { 6 | const HaoKanShortVideoPage({ 7 | super.key, 8 | required this.active, 9 | }); 10 | 11 | final bool active; 12 | 13 | @override 14 | State createState() => _HaoKanShortVideoPageState(); 15 | } 16 | 17 | class _HaoKanShortVideoPageState extends State { 18 | late final HaoKanShortVideoViewModel viewModel; 19 | late final PageController _pageController; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | viewModel = HaoKanShortVideoViewModel(); 25 | _pageController = PageController(); 26 | _pageController.addListener(() { 27 | if (_pageController.position.pixels == 28 | _pageController.position.maxScrollExtent) { 29 | viewModel.requestData(pageNum: viewModel.pageNumber).then((value) { 30 | if (mounted) { 31 | setState(() {}); 32 | } 33 | }); 34 | } 35 | }); 36 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 37 | viewModel.requestData().then((value) { 38 | if (mounted) { 39 | setState(() {}); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | @override 46 | void dispose() { 47 | _pageController.dispose(); 48 | super.dispose(); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Scaffold( 54 | backgroundColor: Colors.black, 55 | body: RefreshIndicator( 56 | onRefresh: () async { 57 | viewModel.requestData().then((value) { 58 | if (mounted) { 59 | setState(() {}); 60 | } 61 | }); 62 | }, 63 | child: PageView.builder( 64 | itemCount: viewModel.videoList.length, 65 | scrollDirection: Axis.vertical, 66 | physics: const ClampingScrollPhysics(), 67 | controller: _pageController, 68 | itemBuilder: (context, index) { 69 | return HaoKanShortVideoItemView( 70 | model: viewModel.videoList[index], 71 | active: widget.active, 72 | ); 73 | }, 74 | ), 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/pages/haokan_video/short_video/haokan_short_video_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../../http/http_manager.dart'; 2 | import 'haokan_short_video_model.dart'; 3 | 4 | class HaoKanShortVideoViewModel { 5 | late List videoList = []; 6 | 7 | int pageNumber = 1; 8 | 9 | // https://haokan.baidu.com/web/video/feed?tab=yingshi_new&act=pcFeed&pd=pc&num=20&shuaxin_id=9653361786441 10 | Future requestData({ 11 | int pageNum = 1, 12 | }) async { 13 | ResponseCallBack data = await HttpManager.request( 14 | req: NetApi.haokanShortVideoList, 15 | queryParams: { 16 | 'tab': 'recommend', 17 | 'act': 'pcFeed', 18 | 'pd': 'pc', 19 | 'num': 10, 20 | }, 21 | ); 22 | pageNumber = pageNum + 1; 23 | if (pageNum == 1) { 24 | videoList.clear(); 25 | } 26 | 27 | if (data.model == null) { 28 | return []; 29 | } 30 | 31 | HaoKanShortVideoModel resModel = 32 | HaoKanShortVideoModel.fromJson(data.model!.map['response']); 33 | videoList.addAll(resModel.videos ?? []); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/pages/home/model/home_model.dart: -------------------------------------------------------------------------------- 1 | enum SectionType { 2 | bigEye, 3 | guide, 4 | mutilEntry, 5 | singleImage, 6 | } 7 | 8 | class HomePageModel { 9 | ///cell类型 10 | SectionType? type; 11 | 12 | ///cell内容 13 | List itemArray; 14 | 15 | ///cell标题 16 | String title; 17 | 18 | String? redirectUrl; 19 | 20 | HomePageModel({ 21 | required this.type, 22 | required this.itemArray, 23 | this.title = '', 24 | this.redirectUrl, 25 | }); 26 | } 27 | 28 | class HomeDataModel { 29 | BannerTop? top; 30 | List? sections; 31 | bool isEnd = false; 32 | 33 | HomeDataModel.fromJson(Map map) { 34 | if (map.isEmpty) { 35 | return; 36 | } 37 | if (map['top'] != null) { 38 | top = BannerTop.fromJson(map['top']); 39 | } 40 | if (map['isEnd'] != null) { 41 | isEnd = map['isEnd']; 42 | } 43 | 44 | sections = 45 | (map['sections'] as List).map((e) => SectionModel.fromJson(e)).toList(); 46 | } 47 | } 48 | 49 | class BannerTop { 50 | List? eyeList; 51 | List? guideList; 52 | 53 | BannerTop.fromJson(Map map) { 54 | if (map.isEmpty) { 55 | return; 56 | } 57 | // [{id: 6800, name: 华灯初上 第一季, title: 小酒馆妈妈桑的爱恨情仇,谁杀了谁, imgUrl: http://img.juquanquanapp.com/img/img/20211224/o_bf0eba1ea4b1430f9a6f707889ad4ce4.jpg, targetUrl: 38331, type: season, sequence: 1, redirectUrl: jgjg.jqq://season?seasonId=38331&isMovie=false, position: jqq_eye, filmSubtitle: 好口碑高人气新剧, filmTitle: 华灯初上, filmScore: 8.1}] 58 | // [{id: 6642, name: 美丽的他, title: 自闭少年恋上清冷美少年, imgUrl: http://img.juquanquanapp.com/img/img/20211119/o_93bfe0455e8c4b79a369ed4a0ad427fb.jpg, targetUrl: 38242, type: season, sequence: 1, redirectUrl: jgjg.jqq://season?seasonId=38242&isMovie=false, position: jqq_eye, filmSubtitle: 深陷一见钟情, filmTitle: 美丽的他, filmScore: 8.8}] 59 | // [{id: 6799, name: 那年,我们的夏天, title: 《魔女》再组CP,这颗糖我死磕到底, imgUrl: http://img.juquanquanapp.com/img/img/20211224/o_b4f33018f2bc4c74811988d5e3e5e1c4.jpg, targetUrl: 38217, type: season, sequence: 1, redirectUrl: jgjg.jqq://season?seasonId=38217&isMovie=false, position: jqq_eye, filmSubtitle: 崔宇植×金多美, filmTitle: 那年,我们, filmScore: 9.0}] 60 | // [{id: 6802, name: 猎魔人 第二季, title: 杰洛特再度华丽回归!, imgUrl: http://img.juquanquanapp.com/img/img/20211224/o_30fd136b7d454780b29da16cbdf4d030.jpg, targetUrl: 38253, type: season, sequence: 1, redirectUrl: jgjg.jqq://season?seasonId=38253&isMovie=false, position: jqq_eye, filmSubtitle: 巫师, filmTitle: 猎魔人, filmScore: 8.3}] 61 | List guides = map['guide']; 62 | List eyes = 63 | map['eye'] + (guides.length <= 2 ? guides : guides.sublist(2)); 64 | eyeList = eyes.map((e) => BigEyeModel.fromJson(e)).toList(); 65 | guideList = (guides.map((e) => GuideModel.fromJson(e))).toList(); 66 | } 67 | } 68 | 69 | class BigEyeModel { 70 | int? id; 71 | String? imgUrl; 72 | String? name; 73 | String? title; 74 | String? position; 75 | String? redirectUrl; 76 | String? type; 77 | String? targetUrl; 78 | String? filmSubtitle; 79 | String? filmTitle; 80 | 81 | BigEyeModel.fromJson(Map map) 82 | : id = map['id'], 83 | imgUrl = map['imgUrl'], 84 | name = map['name'], 85 | position = map['position'], 86 | redirectUrl = map['redirectUrl'], 87 | type = map['type'], 88 | title = map['title'], 89 | filmTitle = map['filmTitle'], 90 | filmSubtitle = map['filmSubtitle']; 91 | 92 | @override 93 | String toString() { 94 | return 'title:$title, subTitle:$title, filmTitle:$filmTitle, filmSubtitle:$filmSubtitle'; 95 | } 96 | } 97 | 98 | class GuideModel { 99 | int? id; 100 | double? filmScore; 101 | String? filmSubtitle; 102 | String? filmTitle; 103 | String? imgUrl; 104 | String? name; 105 | String? position; 106 | String? redirectUrl; 107 | String? type; 108 | String? targetUrl; 109 | 110 | GuideModel.fromJson(Map map) 111 | : id = map['id'], 112 | imgUrl = map['imgUrl'], 113 | name = map['name'], 114 | position = map['position'], 115 | redirectUrl = map['redirectUrl'], 116 | type = map['type'], 117 | filmTitle = map['filmTitle'], 118 | filmSubtitle = map['filmSubtitle'], 119 | filmScore = map['filmScore']; 120 | } 121 | 122 | class SectionModel { 123 | String? moreText; 124 | String? name; 125 | int? position; 126 | List? sectionContents; 127 | String? sectionType; 128 | int? sequence; 129 | String? targetId; 130 | 131 | SectionModel.fromJson(Map map) { 132 | moreText = map['moreText']; 133 | name = map['name']; 134 | position = map['position']; 135 | sectionType = map['sectionType']; 136 | sequence = map['sequence']; 137 | targetId = map['targetId']; 138 | 139 | sectionContents = (map['sectionContents'] as List) 140 | .map((e) => SectionContentModel.fromJson(e)) 141 | .toList(); 142 | } 143 | } 144 | 145 | class SectionContentModel { 146 | String? coverUrl; 147 | int? dramaId; 148 | String? dramaType; 149 | String? icon; 150 | String? score; 151 | String? subTitle; 152 | String? targetId; 153 | String? targetType; 154 | String? title; 155 | String? pictureHeight; 156 | String? pictureWidth; 157 | bool? vipFlag; 158 | 159 | SectionContentModel( 160 | this.dramaId, 161 | this.title, 162 | this.coverUrl, 163 | this.score, 164 | ); 165 | 166 | SectionContentModel.fromJson(Map map) { 167 | coverUrl = map['coverUrl']; 168 | dramaId = map['dramaId']; 169 | dramaType = map['dramaType']; 170 | icon = map['icon']; 171 | score = '${map['score']}'; 172 | subTitle = map['subTitle']; 173 | targetId = map['targetId']; 174 | targetType = map['targetType']; 175 | title = map['title']; 176 | pictureHeight = map['pictureHeight']; 177 | pictureWidth = map['pictureWidth']; 178 | vipFlag = map['vipFlag']; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/pages/home/view_model/home_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_video_player/abstracts/abstract_interface.dart'; 2 | import 'package:flutter_video_player/http/http_config.dart'; 3 | import 'package:flutter_video_player/http/http_response_model.dart'; 4 | 5 | import '../model/home_model.dart'; 6 | 7 | class HomeViewModel implements Request { 8 | late List pageModelList = []; 9 | 10 | @override 11 | Future handleData(ResponseModel data) async { 12 | var model = HomeDataModel.fromJson(data.map); 13 | pageModelList.clear(); 14 | if (model.top != null) { 15 | BannerTop top = model.top!; 16 | if (top.eyeList?.isNotEmpty == true) { 17 | pageModelList.add( 18 | HomePageModel( 19 | type: SectionType.bigEye, 20 | itemArray: top.eyeList!, 21 | ), 22 | ); 23 | } 24 | if (top.guideList != null) { 25 | pageModelList.add( 26 | HomePageModel( 27 | type: SectionType.guide, 28 | itemArray: top.guideList!, 29 | ), 30 | ); 31 | } 32 | } 33 | if (model.sections != null) { 34 | model.sections?.forEach( 35 | (e) { 36 | if (e.sectionContents?.isEmpty == true) { 37 | return; 38 | } 39 | if (e.sectionType == 'SINGLE_IMAGE') { 40 | pageModelList.add(HomePageModel( 41 | type: SectionType.singleImage, 42 | itemArray: e.sectionContents ?? [], 43 | title: e.name ?? '', 44 | redirectUrl: e.targetId, 45 | )); 46 | } else if (e.sectionType == 'VIDEO' || 47 | e.sectionType == 'VIDEO_AUTO') { 48 | pageModelList.add(HomePageModel( 49 | type: SectionType.mutilEntry, 50 | itemArray: e.sectionContents ?? [], 51 | title: e.name ?? '', 52 | redirectUrl: e.targetId, 53 | )); 54 | } 55 | }, 56 | ); 57 | } 58 | return model.top?.eyeList?.first; 59 | } 60 | 61 | @override 62 | Future requestData({ 63 | int pageNum = 1, 64 | Map queryParams = const {}, 65 | }) async { 66 | ResponseModel? model = await HttpConfig.request(api: Api.home); 67 | if (model != null) { 68 | return await handleData(model); 69 | } 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/pages/home/views/big_eye_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' show ImageFilter; 2 | 3 | import 'package:extended_image/extended_image.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_video_player/util/util.dart'; 6 | 7 | class HomeBigEyeView extends StatelessWidget { 8 | HomeBigEyeView({ 9 | Key? key, 10 | this.imgUrl, 11 | }) : super(key: key); 12 | 13 | final String? imgUrl; 14 | set imageUrl(String? url) { 15 | if (url?.isEmpty == true) { 16 | return; 17 | } 18 | imgUrlNotify.value = url; 19 | } 20 | 21 | late final ValueNotifier imgUrlNotify = 22 | ValueNotifier(imgUrl); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final size = MediaQuery.of(context).size; 27 | final width = size.width; 28 | final height = width / Util.bigEyeImgRatio; 29 | return SizedBox( 30 | height: size.height, 31 | child: Stack( 32 | children: [ 33 | Positioned( 34 | top: 0, 35 | left: 0, 36 | right: 0, 37 | height: height, 38 | child: ValueListenableBuilder( 39 | valueListenable: imgUrlNotify, 40 | builder: (context, url, child) { 41 | if (url?.isEmpty == true) { 42 | return const SizedBox.shrink(); 43 | } 44 | return ExtendedImage.network( 45 | url ?? '', 46 | fit: BoxFit.cover, 47 | width: width, 48 | height: height, 49 | cacheWidth: width.toInt(), 50 | cacheHeight: height.toInt(), 51 | cacheMaxAge: const Duration(days: 7), 52 | enableLoadState: false, 53 | filterQuality: FilterQuality.none, 54 | ); 55 | }, 56 | ), 57 | ), 58 | Positioned( 59 | child: Container( 60 | height: 120, 61 | decoration: const BoxDecoration( 62 | gradient: LinearGradient( 63 | begin: Alignment.topCenter, 64 | end: Alignment.bottomCenter, 65 | colors: [ 66 | Color(0xFF383B4B), 67 | Color(0x99383B4B), 68 | Color(0x00383B4B), 69 | ], 70 | stops: [0.60, 0.79, 1], 71 | ), 72 | ), 73 | ), 74 | ), 75 | Positioned( 76 | top: 0, 77 | left: 0, 78 | right: 0, 79 | height: height, 80 | child: ClipRect( 81 | child: BackdropFilter( 82 | filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), 83 | child: Container( 84 | color: Colors.black45.withOpacity(0), 85 | ), 86 | ), 87 | ), 88 | ), 89 | Positioned( 90 | top: height - 200, 91 | left: 0, 92 | right: 0, 93 | height: 200, 94 | child: Container( 95 | decoration: const BoxDecoration( 96 | gradient: LinearGradient( 97 | begin: Alignment.topCenter, 98 | end: Alignment.bottomCenter, 99 | colors: [Color(0x00FFFFFF), Colors.white], 100 | stops: [0, 0.72], 101 | ), 102 | ), 103 | ), 104 | ), 105 | ], 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/pages/home/views/cell_guide.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:extended_image/extended_image.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_video_player/models/models.dart'; 6 | import 'package:flutter_video_player/routes/route_manager.dart'; 7 | 8 | import '../model/home_model.dart'; 9 | 10 | class GuideViewCell extends StatefulWidget { 11 | GuideViewCell({ 12 | Key? key, 13 | required this.itemArray, 14 | }) : super(key: key); 15 | 16 | List? itemArray; 17 | 18 | @override 19 | _GuideViewCellState createState() => _GuideViewCellState(); 20 | } 21 | 22 | class _GuideViewCellState extends State { 23 | @override 24 | Widget build(BuildContext context) { 25 | return SizedBox( 26 | height: 200, 27 | child: GridView.count( 28 | padding: const EdgeInsets.only( 29 | left: 12, 30 | right: 12, 31 | ), 32 | scrollDirection: Axis.horizontal, 33 | crossAxisSpacing: 0, 34 | mainAxisSpacing: 6, 35 | crossAxisCount: 1, 36 | childAspectRatio: 200 / 160, 37 | children: ((widget.itemArray ?? []).map( 38 | (e) => GestureDetector( 39 | child: GuideViewItemCell(model: e), 40 | onTap: () { 41 | Navigator.pushNamed( 42 | context, 43 | RouteManager.dramaDetail, 44 | arguments: DramaCoverModel( 45 | dramaId: '${e.id}', 46 | coverUrl: e.imgUrl, 47 | ), 48 | ); 49 | }, 50 | ), 51 | )).toList(), 52 | ), 53 | ); 54 | } 55 | } 56 | 57 | class GuideViewItemCell extends StatefulWidget { 58 | GuideViewItemCell({ 59 | Key? key, 60 | required this.model, 61 | }) : super(key: key); 62 | 63 | GuideModel? model; 64 | @override 65 | _GuideViewItemCellState createState() => _GuideViewItemCellState(); 66 | } 67 | 68 | class _GuideViewItemCellState extends State { 69 | @override 70 | Widget build(BuildContext context) { 71 | num score = widget.model?.filmScore ?? 0; 72 | List stars = List.generate( 73 | (score.ceil() ~/ 2), 74 | (index) => const Icon( 75 | Icons.star, 76 | color: Color(0xfffb6060), 77 | size: 10, 78 | ), 79 | ); 80 | 81 | return SizedBox( 82 | child: Column( 83 | children: [ 84 | Container( 85 | alignment: Alignment.topLeft, 86 | padding: EdgeInsets.zero, 87 | clipBehavior: Clip.hardEdge, 88 | decoration: BoxDecoration( 89 | borderRadius: BorderRadius.circular(4), 90 | ), 91 | child: ExtendedImage.network( 92 | widget.model?.imgUrl ?? '', 93 | fit: BoxFit.cover, 94 | width: 160, 95 | height: 120, 96 | cacheWidth: 160, 97 | cacheHeight: 120, 98 | cacheMaxAge: const Duration(days: 7), 99 | enableLoadState: false, 100 | ), 101 | ), 102 | const SizedBox( 103 | height: 6, 104 | ), 105 | Align( 106 | alignment: Alignment.topLeft, 107 | child: Text( 108 | widget.model?.filmTitle ?? '', 109 | maxLines: 1, 110 | overflow: TextOverflow.ellipsis, 111 | style: const TextStyle( 112 | color: Color(0xff32383A), 113 | fontSize: 14, 114 | ), 115 | ), 116 | ), 117 | const SizedBox( 118 | height: 4, 119 | ), 120 | Align( 121 | alignment: Alignment.topLeft, 122 | child: Text( 123 | widget.model?.filmSubtitle ?? '', 124 | maxLines: 1, 125 | overflow: TextOverflow.ellipsis, 126 | style: const TextStyle( 127 | color: Color(0xff868996), 128 | fontSize: 10, 129 | ), 130 | ), 131 | ), 132 | const SizedBox( 133 | height: 6, 134 | ), 135 | Row( 136 | crossAxisAlignment: CrossAxisAlignment.end, 137 | children: [ 138 | Text( 139 | '${widget.model?.filmScore ?? ''}', 140 | maxLines: 1, 141 | overflow: TextOverflow.ellipsis, 142 | style: const TextStyle( 143 | color: Color(0xffFB6060), 144 | fontSize: 10 * 4 / 3, 145 | // fontFamily: 'Bebas', 146 | ), 147 | ), 148 | ...stars, //展开操作符 ... 能够把 list、set、map 字面量里的元素插入到一个集合中。一个对象是否可用于展开操作符取决于是否继承了Iterable,Map集合例外,对 map 进行展开操作 实际上是 调用了 Map 的 entries.iterator(),如果被展开的对象可能为 null,需要在展开操作符后面加上 ? 号 (...?) 149 | ], 150 | ), 151 | ], 152 | ), 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/pages/home/views/cell_list.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:extended_image/extended_image.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_video_player/util/r_sources.dart'; 6 | 7 | import '../model/home_model.dart'; 8 | 9 | class ListCell extends StatelessWidget { 10 | const ListCell({ 11 | Key? key, 12 | this.model, 13 | }) : super(key: key); 14 | final SectionContentModel? model; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final width = (MediaQuery.of(context).size.width - 36) / 3.0; 19 | final height = width * 1.4; 20 | return SizedBox( 21 | width: width, 22 | child: Column( 23 | children: [ 24 | Stack( 25 | children: [ 26 | Container( 27 | clipBehavior: Clip.hardEdge, 28 | padding: EdgeInsets.zero, 29 | decoration: const BoxDecoration( 30 | borderRadius: BorderRadius.all( 31 | Radius.circular(4), 32 | ), 33 | ), 34 | child: Hero( 35 | tag: model!.coverUrl!, 36 | child: ExtendedImage.network( 37 | model?.coverUrl ?? '', 38 | fit: BoxFit.cover, 39 | width: width, 40 | height: height, 41 | cacheWidth: width.toInt(), 42 | cacheHeight: height.toInt(), 43 | cacheMaxAge: const Duration(days: 7), 44 | enableLoadState: false, 45 | ), 46 | ), 47 | ), 48 | Positioned( 49 | left: 0, 50 | top: 0, 51 | child: Visibility( 52 | visible: model?.vipFlag == true, 53 | child: Image.asset( 54 | R.Img.ic_vip, 55 | ), 56 | ), 57 | ), 58 | Positioned( 59 | left: 0, 60 | right: 0, 61 | bottom: 0, 62 | child: Container( 63 | height: 28, 64 | clipBehavior: Clip.hardEdge, 65 | decoration: BoxDecoration( 66 | borderRadius: const BorderRadius.only( 67 | bottomLeft: Radius.circular(4), 68 | bottomRight: Radius.circular(4), 69 | ), 70 | gradient: LinearGradient( 71 | begin: Alignment.topCenter, 72 | end: Alignment.bottomCenter, 73 | colors: [ 74 | Colors.black.withAlpha(0), 75 | Colors.black.withAlpha(153), 76 | ], 77 | ), 78 | ), 79 | ), 80 | ), 81 | Positioned( 82 | right: 4, 83 | bottom: 4, 84 | child: Text( 85 | '${model?.score}', 86 | textScaleFactor: 1.0, 87 | style: const TextStyle( 88 | color: Colors.white, 89 | fontSize: 13, 90 | ), 91 | ), 92 | ), 93 | ], 94 | ), 95 | Container( 96 | alignment: Alignment.topLeft, 97 | margin: const EdgeInsets.only(top: 6), 98 | child: Text( 99 | model?.title ?? '', 100 | maxLines: 1, 101 | overflow: TextOverflow.ellipsis, 102 | textScaleFactor: 1.0, 103 | style: const TextStyle( 104 | color: Color(0xFF32383A), 105 | fontSize: 14, 106 | ), 107 | ), 108 | ), 109 | ], 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/pages/home/views/cell_multi.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/models/models.dart'; 5 | import 'package:flutter_video_player/routes/route_manager.dart'; 6 | 7 | import '../model/home_model.dart'; 8 | import 'cell_list.dart'; 9 | 10 | class MultiItemCell extends StatefulWidget { 11 | MultiItemCell({ 12 | Key? key, 13 | this.model, 14 | }) : super(key: key); 15 | 16 | HomePageModel? model; 17 | 18 | @override 19 | _MultiItemCellState createState() => _MultiItemCellState(); 20 | } 21 | 22 | class _MultiItemCellState extends State { 23 | @override 24 | Widget build(BuildContext context) { 25 | return Column( 26 | children: [ 27 | SizedBox( 28 | height: (widget.model?.itemArray ?? []).isNotEmpty ? 38 : 0, 29 | child: GestureDetector( 30 | child: Row( 31 | children: [ 32 | Padding( 33 | padding: const EdgeInsets.only(left: 12), 34 | child: Text( 35 | widget.model?.title ?? '', 36 | maxLines: 1, 37 | style: const TextStyle( 38 | fontSize: 16, 39 | fontWeight: FontWeight.w500, 40 | ), 41 | ), 42 | ), 43 | const Spacer(), 44 | const Padding( 45 | padding: EdgeInsets.only(right: 4), 46 | child: Text( 47 | '更多', 48 | maxLines: 1, 49 | style: TextStyle( 50 | fontSize: 10, 51 | fontWeight: FontWeight.w500, 52 | color: Color(0xffadb6c2), 53 | ), 54 | ), 55 | ), 56 | const Padding( 57 | padding: EdgeInsets.only( 58 | right: 10, 59 | ), 60 | child: Icon( 61 | Icons.arrow_forward_ios, 62 | size: 10, 63 | ), 64 | ), 65 | ], 66 | ), 67 | onTap: () { 68 | Navigator.pushNamed( 69 | context, 70 | RouteManager.dramaList, 71 | arguments: DramaCoverModel( 72 | coverUrl: widget.model?.title, 73 | dramaId: widget.model?.redirectUrl, 74 | ), 75 | ); 76 | }, 77 | ), 78 | ), 79 | SizedBox( 80 | child: Wrap( 81 | direction: Axis.horizontal, 82 | spacing: 6, 83 | runSpacing: 6, 84 | children: ((widget.model?.itemArray as List) 85 | .map( 86 | (e) => (GestureDetector( 87 | child: ListCell( 88 | model: e, 89 | ), 90 | onTap: () { 91 | Navigator.pushNamed( 92 | context, 93 | RouteManager.dramaDetail, 94 | arguments: DramaCoverModel( 95 | dramaId: '${e.dramaId}', 96 | coverUrl: e.coverUrl, 97 | ), 98 | ); 99 | }, 100 | )), 101 | ) 102 | .toList()), 103 | ), 104 | ), 105 | const SizedBox( 106 | height: 16, 107 | ), 108 | ], 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/pages/home/views/cell_single_image.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/models/models.dart'; 5 | import 'package:flutter_video_player/routes/route_manager.dart'; 6 | 7 | import '../model/home_model.dart'; 8 | 9 | class SingleImageViewCell extends StatelessWidget { 10 | SingleImageViewCell({ 11 | Key? key, 12 | required this.model, 13 | }) : super(key: key); 14 | 15 | SectionContentModel? model; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return GestureDetector( 20 | child: Container( 21 | clipBehavior: Clip.hardEdge, 22 | margin: const EdgeInsets.fromLTRB(12, 0, 12, 16), 23 | decoration: BoxDecoration( 24 | color: Colors.red, 25 | borderRadius: BorderRadius.circular(4), 26 | ), 27 | child: Image.network( 28 | model?.icon ?? '', 29 | fit: BoxFit.fitWidth, 30 | ), 31 | ), 32 | onTap: () { 33 | Navigator.pushNamed( 34 | context, 35 | RouteManager.dramaDetail, 36 | arguments: DramaCoverModel( 37 | dramaId: '${model?.dramaId}', 38 | coverUrl: model?.coverUrl, 39 | ), 40 | ); 41 | }, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/pages/hot_video/bilibili_model.dart: -------------------------------------------------------------------------------- 1 | class RecommendModel { 2 | RecommendModel({ 3 | this.user, 4 | this.item, 5 | }); 6 | 7 | final User? user; 8 | final Item? item; 9 | 10 | factory RecommendModel.fromJson(Map json) => RecommendModel( 11 | user: User.fromJson(json["user"]), 12 | item: Item.fromJson(json["item"]), 13 | ); 14 | } 15 | 16 | class Item { 17 | Item({ 18 | this.docId, 19 | this.posterUid, 20 | this.pictures, 21 | this.title, 22 | this.category, 23 | this.uploadTime, 24 | this.alreadyLiked, 25 | this.alreadyVoted, 26 | }); 27 | 28 | final int? docId; 29 | final int? posterUid; 30 | final List? pictures; 31 | final String? title; 32 | final String? category; 33 | final int? uploadTime; 34 | final int? alreadyLiked; 35 | final int? alreadyVoted; 36 | 37 | factory Item.fromJson(Map json) => Item( 38 | docId: json["doc_id"], 39 | posterUid: json["poster_uid"], 40 | pictures: List.from( 41 | json["pictures"].map((x) => Picture.fromJson(x))), 42 | title: json["title"], 43 | category: json["category"], 44 | uploadTime: json["upload_time"], 45 | alreadyLiked: json["already_liked"], 46 | alreadyVoted: json["already_voted"], 47 | ); 48 | } 49 | 50 | class Picture { 51 | Picture({ 52 | this.imgSrc, 53 | this.imgWidth, 54 | this.imgHeight, 55 | this.imgSize, 56 | }); 57 | 58 | final String? imgSrc; 59 | final int? imgWidth; 60 | final int? imgHeight; 61 | final int? imgSize; 62 | 63 | factory Picture.fromJson(Map json) => Picture( 64 | imgSrc: json["img_src"], 65 | imgWidth: json["img_width"], 66 | imgHeight: json["img_height"], 67 | imgSize: json["img_size"], 68 | ); 69 | } 70 | 71 | class User { 72 | User({ 73 | this.uid, 74 | this.headUrl, 75 | this.name, 76 | }); 77 | 78 | final int? uid; 79 | final String? headUrl; 80 | final String? name; 81 | 82 | factory User.fromJson(Map json) => User( 83 | uid: json["uid"], 84 | headUrl: json["head_url"], 85 | name: json["name"], 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /lib/pages/hot_video/card_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_video_player/util/util.dart'; 4 | import 'bilibili_model.dart'; 5 | 6 | class CardView extends StatelessWidget { 7 | const CardView({ 8 | Key? key, 9 | required this.model, 10 | }) : super(key: key); 11 | 12 | final RecommendModel model; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | Picture? picture = model.item?.pictures?.first; 17 | int imgWidth = picture?.imgWidth ?? 1; 18 | int imgHeight = picture?.imgHeight ?? 1; 19 | final w = Util.appWidth / 2.0 - 15; 20 | final h = w / imgWidth * imgHeight; 21 | 22 | return Container( 23 | clipBehavior: Clip.hardEdge, 24 | padding: EdgeInsets.zero, 25 | decoration: const BoxDecoration( 26 | color: Colors.white, 27 | borderRadius: BorderRadius.all(Radius.circular(4)), 28 | ), 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | ExtendedImage.network( 33 | model.item?.pictures?.first.imgSrc ?? '', 34 | fit: BoxFit.fitWidth, 35 | cacheMaxAge: const Duration(hours: 1), 36 | enableLoadState: false, 37 | width: w, 38 | height: h, 39 | cacheWidth: w.toInt(), 40 | cacheHeight: h.toInt(), 41 | filterQuality: FilterQuality.medium, 42 | ), 43 | Padding( 44 | padding: const EdgeInsets.symmetric( 45 | horizontal: 12, 46 | vertical: 10, 47 | ), 48 | child: Text( 49 | model.item?.title ?? '', 50 | maxLines: 1, 51 | style: const TextStyle( 52 | fontSize: 14, 53 | color: Color(0xff333333), 54 | ), 55 | ), 56 | ), 57 | SizedBox( 58 | width: MediaQuery.of(context).size.width / 2 - 20, 59 | child: Stack( 60 | children: [ 61 | Container( 62 | width: 24, 63 | height: 24, 64 | margin: const EdgeInsets.only( 65 | left: 12, 66 | right: 10, 67 | bottom: 5, 68 | ), 69 | decoration: const BoxDecoration( 70 | borderRadius: BorderRadius.all( 71 | Radius.circular(12), 72 | ), 73 | ), 74 | clipBehavior: Clip.hardEdge, 75 | child: Image.network( 76 | model.user?.headUrl ?? '', 77 | fit: BoxFit.cover, 78 | cacheWidth: 24, 79 | cacheHeight: 24, 80 | ), 81 | ), 82 | Positioned( 83 | left: 46, 84 | right: 10, 85 | height: 24, 86 | child: Align( 87 | alignment: Alignment.centerLeft, 88 | child: Text( 89 | model.user?.name ?? '', 90 | maxLines: 1, 91 | style: const TextStyle( 92 | fontSize: 14, 93 | color: Color(0xff999999), 94 | overflow: TextOverflow.ellipsis, 95 | ), 96 | ), 97 | ), 98 | ), 99 | ], 100 | ), 101 | ), 102 | ], 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/pages/hot_video/hot_video_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_video_player/util/util.dart'; 4 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 5 | import 'package:waterfall_flow/waterfall_flow.dart'; 6 | import 'card_view.dart'; 7 | import 'hot_video_view_model.dart'; 8 | 9 | class HotVideoPage extends StatefulWidget { 10 | const HotVideoPage({Key? key}) : super(key: key); 11 | 12 | @override 13 | State createState() => _HotVideoPageState(); 14 | } 15 | 16 | class _HotVideoPageState extends State { 17 | late HotVideoViewModel viewModel; 18 | 19 | late final RefreshController _controller; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | viewModel = HotVideoViewModel(); 25 | _controller = RefreshController(initialRefresh: true); 26 | } 27 | 28 | @override 29 | void dispose() { 30 | _controller.dispose(); 31 | super.dispose(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | toolbarHeight: Util.navBarHeight, 39 | title: const Text( 40 | 'Bilibili', 41 | style: TextStyle(color: Colors.black), 42 | ), 43 | backgroundColor: Colors.white, 44 | elevation: 0.5, 45 | ), 46 | body: SmartRefresher( 47 | enablePullUp: true, 48 | controller: _controller, 49 | onRefresh: () async { 50 | viewModel.requestData().then((value) { 51 | if (mounted) { 52 | _controller.refreshCompleted(); 53 | setState(() {}); 54 | } 55 | }); 56 | }, 57 | onLoading: () async { 58 | viewModel.requestData(pageNum: viewModel.pageNumber).then((value) { 59 | if (mounted) { 60 | _controller.loadComplete(); 61 | setState(() {}); 62 | } 63 | }); 64 | }, 65 | child: WaterfallFlow.builder( 66 | padding: const EdgeInsets.all(12.0), 67 | gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount( 68 | crossAxisCount: 2, 69 | crossAxisSpacing: 6.0, 70 | mainAxisSpacing: 12.0, 71 | collectGarbage: (List garbages) { 72 | for (var index in garbages) { 73 | final provider = ExtendedNetworkImageProvider( 74 | viewModel.pageModelList[index].item?.pictures?.first.imgSrc ?? 75 | '', 76 | ); 77 | provider.evict(); 78 | } 79 | }, 80 | lastChildLayoutTypeBuilder: (index) => 81 | index == viewModel.pageModelList.length 82 | ? LastChildLayoutType.foot 83 | : LastChildLayoutType.none, 84 | ), 85 | itemCount: viewModel.pageModelList.length, 86 | itemBuilder: (BuildContext context, int index) { 87 | return CardView( 88 | model: viewModel.pageModelList[index], 89 | ); 90 | }, 91 | ), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/pages/hot_video/hot_video_view_model.dart: -------------------------------------------------------------------------------- 1 | import '../../abstracts/abstract_interface.dart'; 2 | import 'bilibili_model.dart'; 3 | import '../../http/http_manager.dart'; 4 | import '../../http/http_response_model.dart'; 5 | 6 | class HotVideoViewModel implements Request { 7 | late List pageModelList = []; 8 | 9 | int pageNumber = 0; 10 | 11 | @override 12 | Future> handleData(ResponseModel data) async { 13 | pageModelList.clear(); 14 | List items = data.map['items']; 15 | pageModelList.addAll(items.map((e) => RecommendModel.fromJson(e))); 16 | return pageModelList; 17 | } 18 | 19 | @override 20 | Future requestData({ 21 | int pageNum = 0, 22 | Map queryParams = const {}, 23 | }) async { 24 | ResponseCallBack data = await HttpManager.request( 25 | req: NetApi.bilibiliHotVideo, 26 | queryParams: { 27 | 'page_size': 20, 28 | 'page_num': pageNum, 29 | 'type': 'recommend', 30 | }, 31 | ); 32 | pageNumber = pageNum + 1; 33 | 34 | if (pageNum == 0) { 35 | pageModelList.clear(); 36 | } 37 | 38 | if (data.model == null) { 39 | return []; 40 | } 41 | 42 | List items = data.model!.map['items']; 43 | pageModelList.addAll(items.map((e) => RecommendModel.fromJson(e))); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/pages/login/login_input_text_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class LoginInputTextView extends StatefulWidget { 4 | const LoginInputTextView({ 5 | Key? key, 6 | this.controller, 7 | this.placeholder, 8 | this.maxLength, 9 | this.onChanged, 10 | }) : super(key: key); 11 | 12 | final TextEditingController? controller; 13 | final String? placeholder; 14 | final int? maxLength; 15 | final ValueChanged? onChanged; 16 | 17 | @override 18 | _LoginInputTextViewState createState() => _LoginInputTextViewState(); 19 | } 20 | 21 | class _LoginInputTextViewState extends State { 22 | late bool hasInput = false; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return CupertinoTextField( 27 | controller: widget.controller, 28 | cursorHeight: 18, 29 | cursorColor: const Color(0xfffb6060), 30 | clearButtonMode: OverlayVisibilityMode.editing, 31 | maxLength: widget.maxLength, 32 | padding: 33 | hasInput ? EdgeInsets.zero : const EdgeInsets.symmetric(vertical: 6), 34 | textAlignVertical: TextAlignVertical.top, 35 | textAlign: TextAlign.left, 36 | decoration: null, 37 | style: const TextStyle( 38 | fontSize: 24, 39 | fontWeight: FontWeight.w500, 40 | letterSpacing: 2, 41 | color: Color(0xff32383a), 42 | ), 43 | placeholder: widget.placeholder, 44 | placeholderStyle: const TextStyle( 45 | fontSize: 16, 46 | fontWeight: FontWeight.normal, 47 | color: Color(0xffadb6c2), 48 | height: 1.5, 49 | ), 50 | onChanged: (value) { 51 | setState(() { 52 | hasInput = value.isNotEmpty; 53 | }); 54 | widget.onChanged?.call(value); 55 | }, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/pages/login/login_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_video_player/http/http_config.dart'; 2 | import 'package:flutter_video_player/http/http_response_model.dart'; 3 | import 'package:flutter_video_player/user/user_info_model.dart'; 4 | import 'package:flutter_video_player/user/user_manager.dart'; 5 | 6 | class LoginViewModel { 7 | ///发送验证码 8 | Future sendVerifyCode(String mobile) async { 9 | ResponseModel? data = await HttpConfig.request( 10 | api: Api.sendVerifyCode, 11 | queryParams: { 12 | 'mobile': mobile, 13 | 'countryCode': '+86', 14 | }, 15 | ); 16 | return data?.code == '0000'; 17 | } 18 | 19 | /* 验证码登录 */ 20 | Future verifyCodeLogin(String mobile, String code) async { 21 | ResponseModel? data = await HttpConfig.request( 22 | api: Api.verifyCodeLogin, 23 | queryParams: { 24 | 'mobile': mobile, 25 | 'captchaSms': code, 26 | 'countryCode': '+86', 27 | }, 28 | ); 29 | if (data != null) { 30 | UserInfoModel model = UserInfoModel.fromJson(data.map); 31 | bool saved = await UserManger.saveUserInfo(model); 32 | return data.code == '0000' && saved; 33 | } 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/pages/mine/mine_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_video_player/abstracts/abstract_interface.dart'; 2 | import 'package:flutter_video_player/http/http_config.dart'; 3 | import 'package:flutter_video_player/http/http_response_model.dart'; 4 | import 'package:flutter_video_player/pages/home/model/home_model.dart'; 5 | import 'package:flutter_video_player/user/user_manager.dart'; 6 | import 'package:flutter_video_player/util/r_sources.dart'; 7 | 8 | class MineViewModel implements Request { 9 | String get avatar { 10 | return UserManger.instance.isLogin 11 | ? R.Img.pic_Avatar_h 12 | : R.Img.pic_Avatar_n; 13 | } 14 | 15 | String get nickName { 16 | return UserManger.instance.isLogin 17 | ? (UserManger().userModel?.nickName ?? '') 18 | : '登录/注册'; 19 | } 20 | 21 | String get phoneText { 22 | return UserManger.instance.isLogin 23 | ? (UserManger().userModel?.mobile ?? '00000000000') 24 | .replaceRange(3, 7, ' **** ') 25 | : '快来开启剧圈圈煲剧之旅'; 26 | } 27 | 28 | late List funcList; 29 | 30 | MineViewModel() { 31 | funcList = []; 32 | } 33 | 34 | @override 35 | Future handleData(ResponseModel data) async { 36 | var model = HomeDataModel.fromJson(data.map); 37 | funcList.clear(); 38 | if (model.sections != null) { 39 | funcList = model.sections!.first.sectionContents ?? []; 40 | } 41 | } 42 | 43 | @override 44 | Future requestData({ 45 | int pageNum = 1, 46 | Map queryParams = const {}, 47 | }) async { 48 | ResponseModel? model = await HttpConfig.request(api: Api.mine); 49 | if (model != null) { 50 | await handleData(model); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/pages/player/video_player.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable, camel_case_types 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_video_player/pages/drama_detail/model/video_info_model.dart'; 5 | import 'package:flutter_video_player/pages/drama_detail/page/drama_detail_page.dart'; 6 | import 'package:video_player/video_player.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'controller_overlay.dart'; 9 | 10 | final overlayKey = GlobalKey(debugLabel: 'overlayKey'); 11 | 12 | class VideoPlayerLayer extends StatefulWidget 13 | implements ControllerOverlayCallback { 14 | VideoPlayerLayer({ 15 | Key? key, 16 | this.smallScreenCallback, 17 | this.fullScreenCallback, 18 | this.nextSourceCallback, 19 | }) : super(key: key); 20 | 21 | @override 22 | _VideoPlayerLayerState createState() => _VideoPlayerLayerState(); 23 | 24 | @override 25 | VoidCallback? smallScreenCallback; 26 | 27 | @override 28 | VoidCallback? fullScreenCallback; 29 | 30 | @override 31 | VoidCallback? nextSourceCallback; 32 | } 33 | 34 | class _VideoPlayerLayerState extends State { 35 | bool isFullScreen = false; 36 | 37 | late PlayerControllerOverlay overlay; 38 | VideoPlayerController? oldController; 39 | late VideoPlayerController newController; 40 | 41 | int? _oldPlayInfoId; 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | } 47 | 48 | @override 49 | void didChangeDependencies() { 50 | super.didChangeDependencies(); 51 | 52 | PlayInfo? info = 53 | DramaPlayInfoWidget.of(context)?.viewModel.playInfoModel?.playInfo; 54 | int? playId = info?.episodeSid; 55 | if (_oldPlayInfoId == playId) { 56 | return; 57 | } 58 | try { 59 | if (oldController != null) { 60 | oldController?.pause(); 61 | oldController?.dispose(); 62 | oldController = null; 63 | } 64 | } catch (e) { 65 | //(e); 66 | // print(e); 67 | } 68 | 69 | String playUrl = info?.url ?? ''; 70 | if (playUrl.isNotEmpty) { 71 | newController = VideoPlayerController.network(playUrl); 72 | _oldPlayInfoId = info?.episodeSid; 73 | oldController = newController; 74 | } 75 | } 76 | 77 | @override 78 | void dispose() { 79 | try { 80 | if (oldController?.value.isPlaying == true) { 81 | oldController?.pause(); 82 | } 83 | // ignore: empty_catches 84 | } catch (e) {} 85 | 86 | if (newController.value.isPlaying) { 87 | newController.pause(); 88 | } 89 | newController.dispose(); 90 | super.dispose(); 91 | } 92 | 93 | Widget get playerView { 94 | return Center( 95 | child: AspectRatio( 96 | aspectRatio: newController.value.aspectRatio, 97 | child: VideoPlayer(newController), 98 | ), 99 | ); 100 | } 101 | 102 | PlayerControllerOverlay get controllerOverlay { 103 | return PlayerControllerOverlay( 104 | key: overlayKey, 105 | player: newController, 106 | isFullScreen: isFullScreen, 107 | smallScreenCallback: () { 108 | SystemChrome.setPreferredOrientations([ 109 | DeviceOrientation.portraitUp, 110 | ]); 111 | setState(() { 112 | isFullScreen = false; 113 | }); 114 | widget.smallScreenCallback?.call(); 115 | }, 116 | fullScreenCallback: () { 117 | SystemChrome.setPreferredOrientations([ 118 | DeviceOrientation.landscapeRight, 119 | DeviceOrientation.landscapeLeft, 120 | ]); 121 | setState(() { 122 | isFullScreen = true; 123 | }); 124 | widget.fullScreenCallback?.call(); 125 | }, 126 | nextSourceCallback: widget.nextSourceCallback, 127 | ); 128 | } 129 | 130 | @override 131 | Widget build(BuildContext context) { 132 | DramaPlayInfoWidget.of(context)?.viewModel; 133 | return FutureBuilder( 134 | future: started(), 135 | builder: (BuildContext ctx, AsyncSnapshot snapshot) { 136 | if (snapshot.connectionState == ConnectionState.done && 137 | newController.value.isInitialized) { 138 | return Container( 139 | color: Colors.black, 140 | child: Stack( 141 | children: [ 142 | playerView, 143 | controllerOverlay, 144 | ], 145 | ), 146 | ); 147 | } 148 | return Container( 149 | color: const Color(0xff000000), 150 | ); 151 | }, 152 | ); 153 | } 154 | 155 | Future started() async { 156 | try { 157 | if (!newController.value.isInitialized) { 158 | await newController.initialize(); 159 | } 160 | await newController.play(); 161 | return true; 162 | } catch (e) { 163 | return false; 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/pages/search/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../util/util.dart'; 5 | 6 | class SearchPage extends StatefulWidget { 7 | const SearchPage({Key? key}) : super(key: key); 8 | 9 | @override 10 | _SearchPageState createState() => _SearchPageState(); 11 | } 12 | 13 | class _SearchPageState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Column( 17 | children: [ 18 | const CupertinoNavigationBar( 19 | backgroundColor: Colors.white, 20 | middle: Text('搜索'), 21 | border: Border( 22 | bottom: BorderSide( 23 | color: Color(0xFFB2B8C2), 24 | width: 0.5, 25 | style: BorderStyle.solid, 26 | ), 27 | ), 28 | ), 29 | Container( 30 | constraints: BoxConstraints( 31 | maxHeight: Util.instance.contentHeight(), 32 | maxWidth: MediaQuery.of(context).size.width, 33 | ), 34 | padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), 35 | ), 36 | ], 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/pages/setting/setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_video_player/util/util.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | class SettiingViewPage extends StatefulWidget { 6 | const SettiingViewPage({ 7 | Key? key, 8 | }) : super(key: key); 9 | 10 | @override 11 | _SettingViewPageState createState() => _SettingViewPageState(); 12 | } 13 | 14 | class _SettingViewPageState extends State { 15 | late final SharedPreferences prefs; 16 | bool _canCellarPlay = false; 17 | bool _canCellarDownload = false; 18 | 19 | final items = [ 20 | '允许移动网络播放', 21 | '允许移动网络下载', 22 | '清理缓存', 23 | 'APP版本', 24 | ]; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | } 30 | 31 | Future initData() async { 32 | prefs = await SharedPreferences.getInstance(); 33 | _canCellarPlay = prefs.getBool('canCellarPlay') ?? false; 34 | _canCellarDownload = prefs.getBool('canCellarDownload') ?? false; 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return FutureBuilder( 40 | future: initData(), 41 | builder: (context, snap) { 42 | return Scaffold( 43 | appBar: AppBar( 44 | toolbarHeight: Util.navBarHeight, 45 | backgroundColor: Colors.white, 46 | title: const Text( 47 | '设置', 48 | style: TextStyle(color: Colors.black), 49 | ), 50 | elevation: 0.5, 51 | leading: MaterialButton( 52 | padding: EdgeInsets.zero, 53 | minWidth: 42, 54 | onPressed: () { 55 | Navigator.pop(context); 56 | }, 57 | child: const Icon( 58 | Icons.arrow_back_ios_new, 59 | size: 24, 60 | color: Color(0xff32383a), 61 | ), 62 | ), 63 | ), 64 | body: Container( 65 | margin: const EdgeInsets.only(bottom: 100), 66 | child: ListView.separated( 67 | itemBuilder: (context, index) { 68 | if (index == 0) { 69 | return SwitchListTile( 70 | title: Text(items[index]), 71 | value: _canCellarPlay, 72 | onChanged: (onChanged) async { 73 | await prefs.setBool('canCellarPlay', onChanged); 74 | setState(() { 75 | _canCellarPlay = onChanged; 76 | }); 77 | }, 78 | ); 79 | } 80 | if (index == 1) { 81 | return SwitchListTile( 82 | title: Text(items[index]), 83 | value: _canCellarDownload, 84 | onChanged: (onChanged) async { 85 | await prefs.setBool('canCellarDownload', onChanged); 86 | setState(() { 87 | _canCellarDownload = onChanged; 88 | }); 89 | }, 90 | ); 91 | } 92 | return ListTile( 93 | title: Text(items[index]), 94 | contentPadding: const EdgeInsets.only(left: 16, right: 30), 95 | trailing: 96 | index == 2 ? const Text('10M') : const Text('1.0.0'), 97 | ); 98 | }, 99 | separatorBuilder: (context, index) { 100 | return const Divider( 101 | indent: 18, 102 | ); 103 | }, 104 | itemCount: items.length, 105 | ), 106 | ), 107 | ); 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/pages/splash/splash_page.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/routes/route_manager.dart'; 5 | import 'package:flutter_video_player/user/user_manager.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | import '../../util/r_sources.dart'; 9 | 10 | class SplashScreen extends StatefulWidget { 11 | const SplashScreen({Key? key}) : super(key: key); 12 | @override 13 | _SplashScreenState createState() => _SplashScreenState(); 14 | } 15 | 16 | class _SplashScreenState extends State 17 | with SingleTickerProviderStateMixin { 18 | late AnimationController _controller; 19 | late Animation _animation; 20 | 21 | Future get isFirstLaunch async { 22 | SharedPreferences prefs = await SharedPreferences.getInstance(); 23 | bool ret = prefs.getBool('FirstLaunch') ?? true; 24 | if (ret) { 25 | prefs.setBool('FirstLaunch', false); 26 | } 27 | return ret; 28 | } 29 | 30 | @override 31 | initState() { 32 | super.initState(); 33 | UserManger.instance.getLocalLogin(); 34 | _controller = AnimationController( 35 | vsync: this, 36 | duration: Duration(milliseconds: 1500), 37 | ); 38 | _animation = Tween(begin: 1.0, end: 1.2).animate(_controller); 39 | _animation.addStatusListener((status) { 40 | if (status == AnimationStatus.completed) { 41 | Navigator.of(context).pushNamedAndRemoveUntil( 42 | RouteManager.root, 43 | (route) => false, 44 | ); 45 | } 46 | }); 47 | _controller.forward(); 48 | } 49 | 50 | @override 51 | void dispose() { 52 | _controller.dispose(); 53 | super.dispose(); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return FutureBuilder( 59 | future: isFirstLaunch, 60 | builder: (context, AsyncSnapshot snap) { 61 | if (snap.data == false) { 62 | return ScaleTransition( 63 | scale: _animation, 64 | child: Image.asset( 65 | R.Img.splash_shake, 66 | scale: 1.5, 67 | fit: BoxFit.cover, 68 | ), 69 | ); 70 | } 71 | return Container( 72 | color: Colors.white, 73 | ); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/pages/web/web_page.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_be_immutable 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/custom/navigationbar.dart'; 5 | 6 | class WebViewPage extends StatefulWidget { 7 | WebViewPage({ 8 | Key? key, 9 | this.url, 10 | }) : super(key: key); 11 | 12 | String? url; 13 | @override 14 | _WebViewPageState createState() => _WebViewPageState(); 15 | } 16 | 17 | class _WebViewPageState extends State { 18 | @override 19 | void initState() { 20 | super.initState(); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | appBar: UINavigationBar( 27 | title: '', 28 | leading: [ 29 | MaterialButton( 30 | padding: EdgeInsets.zero, 31 | minWidth: 42, 32 | onPressed: () { 33 | Navigator.pop(context); 34 | }, 35 | child: const Icon( 36 | Icons.arrow_back_ios_new, 37 | size: 24, 38 | color: Color(0xff32383a), 39 | ), 40 | ), 41 | ], 42 | ), 43 | body: Container(), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/routes/route_manager.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unnecessary_overrides 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_video_player/pages/cache/cache_page.dart'; 5 | import 'package:flutter_video_player/pages/mine/mine_page.dart'; 6 | import 'package:flutter_video_player/pages/setting/setting_page.dart'; 7 | import 'package:flutter_video_player/pages/web/web_page.dart'; 8 | 9 | import '../models/models.dart'; 10 | import '../pages/drama_detail/page/drama_detail_page.dart'; 11 | import '../pages/drama_list/drama_list_page.dart'; 12 | import '../pages/haokan_video/short_video/haokan_short_video_page.dart'; 13 | import '../pages/login/login_page.dart'; 14 | import '../pages/tab_controller/tab_controller.dart'; 15 | 16 | class RouteManager { 17 | static const root = '/root'; 18 | static const dramaDetail = '/dramaDetail'; 19 | static const dramaList = '/dramaList'; 20 | static const login = '/login'; 21 | static const fullScreenPlayer = '/fullScreenPlayer'; 22 | static const webPage = '/webPage'; 23 | static const mine = '/mine'; 24 | static const setting = '/setting'; 25 | static const cache = '/cache'; 26 | static const category = '/category'; 27 | static const shortVideo = '/shortVideo'; 28 | 29 | static Route generateRoute(RouteSettings settings) { 30 | return MaterialPageRoute(builder: (context) { 31 | switch (settings.name) { 32 | case root: 33 | return const RootTabViewController(); 34 | case dramaDetail: 35 | return DramaDetailPageView( 36 | model: (settings.arguments is DramaCoverModel) 37 | ? (settings.arguments as DramaCoverModel) 38 | : null, 39 | ); 40 | case dramaList: 41 | return DramaListPageView( 42 | model: (settings.arguments is DramaCoverModel) 43 | ? (settings.arguments as DramaCoverModel) 44 | : null, 45 | ); 46 | case login: 47 | return const LoginPageView(); 48 | case fullScreenPlayer: 49 | // var isVc = settings.arguments.runtimeType is VideoPlayerLayer; 50 | return Container(); 51 | case webPage: 52 | return WebViewPage( 53 | url: settings.arguments as String, 54 | ); 55 | case mine: 56 | return const MinePageView(); 57 | case setting: 58 | return const SettiingViewPage(); 59 | case cache: 60 | return const CacheViewPage(); 61 | case shortVideo: 62 | return const HaoKanShortVideoPage(active: true,); 63 | default: 64 | return const Scaffold(); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | class CFNavigatorObservers extends NavigatorObserver { 71 | @override 72 | void didPush(Route route, Route? previousRoute) { 73 | super.didPush(route, previousRoute); 74 | } 75 | 76 | @override 77 | void didPop(Route route, Route? previousRoute) { 78 | super.didPop(route, previousRoute); 79 | } 80 | 81 | @override 82 | void didRemove(Route route, Route? previousRoute) { 83 | super.didRemove(route, previousRoute); 84 | } 85 | 86 | @override 87 | void didReplace({Route? newRoute, Route? oldRoute}) { 88 | super.didReplace(newRoute: newRoute, oldRoute: oldRoute); 89 | } 90 | 91 | @override 92 | void didStartUserGesture(Route route, Route? previousRoute) { 93 | super.didStartUserGesture(route, previousRoute); 94 | } 95 | 96 | @override 97 | void didStopUserGesture() { 98 | super.didStopUserGesture(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/user/user_info_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive/hive.dart'; 2 | 3 | @HiveType(typeId: 0) 4 | class UserInfoModel extends HiveObject { 5 | @HiveField(0) 6 | int? userId; 7 | 8 | @HiveField(1) 9 | String? nickName; 10 | 11 | @HiveField(2) 12 | String? mobile; 13 | 14 | @HiveField(3) 15 | String? token; 16 | 17 | // 网络数据,完整数据 18 | UserInfoModel.fromJson(Map map) { 19 | if (map.isEmpty) { 20 | return; 21 | } 22 | Map user = map['user']; 23 | userId = user['userId']; 24 | nickName = user['nickName']; 25 | mobile = user['mobile']; 26 | token = map['token']; 27 | } 28 | 29 | // 本地数据, 简化数据 30 | UserInfoModel(Map user) { 31 | userId = user['userId']; 32 | nickName = user['nickName']; 33 | mobile = user['mobile']; 34 | token = user['token']; 35 | } 36 | 37 | Map toJson() { 38 | return { 39 | 'userId': userId, 40 | 'nickName': nickName, 41 | 'mobile': mobile, 42 | 'token': token, 43 | }; 44 | } 45 | } 46 | 47 | class UserInfoAdapter extends TypeAdapter { 48 | @override 49 | final typeId = 0; 50 | 51 | @override 52 | UserInfoModel read(BinaryReader reader) { 53 | dynamic va = reader.read(typeId); 54 | if (va is Map && va.isNotEmpty) { 55 | return UserInfoModel(va); 56 | } 57 | 58 | return UserInfoModel({}); 59 | } 60 | 61 | @override 62 | void write(BinaryWriter writer, UserInfoModel obj) { 63 | writer.write(obj.userId); 64 | writer.write(obj.nickName); 65 | writer.write(obj.mobile); 66 | writer.write(obj.token); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/user/user_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_video_player/database/hv_manager.dart'; 2 | 3 | import 'user_info_model.dart'; 4 | 5 | class UserManger { 6 | UserManger._(); 7 | 8 | static final UserManger _instance = UserManger._(); 9 | static UserManger get instance => _instance; 10 | factory UserManger() => _instance; 11 | 12 | bool _isLogin = false; 13 | bool get isLogin => _isLogin; 14 | set setIsLogin(bool value) { 15 | _isLogin = value; 16 | if (!value) { 17 | UserManger.instance.userModel = null; 18 | } 19 | } 20 | 21 | UserInfoModel? userModel; 22 | 23 | getLocalLogin() async { 24 | UserInfoModel? model = await getUserInfo(); 25 | setIsLogin = model != null; 26 | userModel = model; 27 | } 28 | 29 | ///保存用户信息 30 | static Future saveUserInfo(UserInfoModel model) async { 31 | UserManger.instance.setIsLogin = true; 32 | UserManger.instance.userModel = model; 33 | return HiveManager().saveUserInfo(model); 34 | } 35 | 36 | /// 获取用户信息 37 | static Future getUserInfo() async { 38 | return await HiveManager().getUserInfo(); 39 | } 40 | 41 | /// 删除用户信息 42 | static Future deleteUserInfo() async { 43 | UserManger.instance.setIsLogin = false; 44 | return await HiveManager().deleteUserInfo(); 45 | } 46 | 47 | /// 退出登录 48 | logout() async { 49 | UserInfoModel? model = await getUserInfo(); 50 | if (model == null || model.token == null) { 51 | return; 52 | } 53 | await deleteUserInfo(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/util/device_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:package_info_plus/package_info_plus.dart'; 2 | 3 | class Device { 4 | static late final PackageInfo info; 5 | static final Device _instance = Device._(); 6 | static Device get instance => _instance; 7 | 8 | Device._() { 9 | _initPackageInfo(); 10 | } 11 | 12 | Future _initPackageInfo() async { 13 | info = await PackageInfo.fromPlatform(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/util/r_sources.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: file_names, non_constant_identifier_names, constant_identifier_names 2 | 3 | typedef R = ResourceRef; 4 | 5 | /// 资源名管理器 6 | /// 图片 7 | class Imgs { 8 | Imgs._(); 9 | static final instance = Imgs._(); 10 | 11 | ///gif 12 | final ic_playing = 'resource/image/ic_playing.gif'; 13 | 14 | ///png 15 | final ic_vip = 'resource/image/ic_vip.png'; 16 | final pic_banner_shadow = 'resource/image/pic_banner_shadow.png'; 17 | final pic_Avatar_n = 'resource/image/pic_Avatar_n.png'; 18 | final pic_Avatar_h = 'resource/image/pic_Avatar_h.png'; 19 | final splas_logo = 'resource/image/splas_logo.png'; 20 | 21 | final cover_img = 'resource/image/cover_img.png'; 22 | 23 | final haokan_logo = 'resource/image/haokan_logo.png'; 24 | 25 | final splash_shake = 'resource/image/splash_shake.png'; 26 | } 27 | 28 | /// 字符串 29 | class Strs { 30 | Strs._(); 31 | static final instance = Strs._(); 32 | 33 | final firstLaunch = 'FirstLaunch'; 34 | 35 | final home = '首页'; 36 | final haokanVideo = '好看'; 37 | final hotVideo = 'Hot'; 38 | final category = '分类'; 39 | final hotComment = '漫画'; 40 | final mine = '我的'; 41 | } 42 | 43 | class JsonPath { 44 | JsonPath._(); 45 | static final instance = JsonPath._(); 46 | 47 | final homePage = 'resource/json/home_page.json'; 48 | final minePage = 'resource/json/mine_page.json'; 49 | 50 | final categoryFilter = 'resource/json/category_filters.json'; 51 | final categoryPage1 = 'resource/json/category_list_page1.json'; 52 | final categoryPage2 = 'resource/json/category_list_page2.json'; 53 | 54 | final dramaDetail = 'resource/json/drama_info_detail.json'; 55 | 56 | final play1Detail = 'resource/json/play1_info_detail.json'; 57 | final play2Detail = 'resource/json/play2_info_detail.json'; 58 | final play3Detail = 'resource/json/play3_info_detail.json'; 59 | final play4Detail = 'resource/json/play4_info_detail.json'; 60 | 61 | final loginSendCode = 'resource/json/login_send_code.json'; 62 | final loginUserInfo = 'resource/json/user_info.json'; 63 | } 64 | 65 | class ResourceRef { 66 | ResourceRef._(); 67 | 68 | static final Img = Imgs.instance; 69 | static final Str = Strs.instance; 70 | static final Jsp = JsonPath.instance; 71 | } 72 | -------------------------------------------------------------------------------- /lib/util/toast.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Toast { 4 | static OverlayEntry? _overlayEntry; 5 | static DateTime _startedTime = DateTime.now(); 6 | 7 | static show(BuildContext context, String msg) async { 8 | _startedTime = DateTime.now(); 9 | OverlayState? state = Overlay.of(context); 10 | if (_overlayEntry == null) { 11 | _overlayEntry = OverlayEntry( 12 | builder: (BuildContext context) { 13 | return Positioned( 14 | top: MediaQuery.of(context).size.height / 2 - 20, 15 | width: MediaQuery.of(context).size.width, 16 | child: Row( 17 | children: [ 18 | const Spacer(), 19 | Container( 20 | alignment: Alignment.center, 21 | decoration: BoxDecoration( 22 | color: Colors.black.withOpacity(0.7), 23 | borderRadius: BorderRadius.circular(8), 24 | ), 25 | margin: const EdgeInsets.symmetric(horizontal: 40), 26 | padding: const EdgeInsets.all(18), 27 | child: AnimatedOpacity( 28 | opacity: 1, 29 | duration: const Duration(milliseconds: 400), 30 | child: Text( 31 | msg, 32 | textAlign: TextAlign.center, 33 | style: const TextStyle( 34 | fontSize: 15, 35 | color: Colors.white, 36 | fontWeight: FontWeight.w500, 37 | decoration: TextDecoration.none, 38 | ), 39 | ), 40 | ), 41 | ), 42 | const Spacer(), 43 | ], 44 | ), 45 | ); 46 | }, 47 | ); 48 | } else {} 49 | if (_overlayEntry?.mounted == false) { 50 | state.insert(_overlayEntry!); 51 | } else { 52 | _overlayEntry?.markNeedsBuild(); 53 | } 54 | 55 | await Future.delayed(const Duration(seconds: 2)); 56 | if (DateTime.now().difference(_startedTime).inSeconds >= 2) { 57 | if (_overlayEntry != null && _overlayEntry?.mounted == true) { 58 | _overlayEntry?.remove(); 59 | _overlayEntry = null; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/util/util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | export 'r_sources.dart'; 3 | 4 | class Util { 5 | Util._(); 6 | 7 | static final Util _instance = Util._(); 8 | static Util get instance => _instance; 9 | factory Util() => _instance; 10 | 11 | /// 屏幕像素倍数 12 | static final double _devicePixelRatio = 13 | WidgetsBinding.instance.window.devicePixelRatio; 14 | 15 | /// 屏幕物理像素大小 16 | static Size get _size => 17 | WidgetsBinding.instance.window.physicalSize / _devicePixelRatio; 18 | 19 | static bool get isPhoneX => WidgetsBinding.instance.window.padding.top > 60; 20 | 21 | /// 图片宽高比 22 | static const imgRatio = 0.75; 23 | 24 | /// banner的图片比例 25 | static const bigEyeImgRatio = 351.0 / 468.0; 26 | 27 | /// 状态栏高度,44 : 20, 部分X机型上,高度会判断为47,这里写死 28 | static double get statusBarHeight => isPhoneX ? 44 : 20; 29 | 30 | /// 导航条高度 44 31 | static const double navBarHeight = 44; 32 | 33 | /// 导航栏整体高度 34 | static double get navgationBarHeight => statusBarHeight + 44; 35 | 36 | /// 底部导航栏高度 83 : 49 37 | static double get tabBarHeight => isPhoneX ? 83 : 49; 38 | 39 | /// 屏幕宽度 40 | static double get appWidth => _size.width; 41 | 42 | /// 屏幕高度 43 | static double get appHeight => _size.height; 44 | 45 | /// 内容显示区域的高度 46 | double contentHeight({ 47 | bool showNavBar = true, 48 | bool showTabBar = true, 49 | }) { 50 | double height = appHeight; 51 | if (showNavBar) { 52 | height -= navgationBarHeight; 53 | } 54 | if (showTabBar) { 55 | height -= tabBarHeight; 56 | } 57 | // -1 分割线高度 58 | return height - 1; 59 | } 60 | 61 | static bool isMobile(String mobile) { 62 | if (mobile.isEmpty || mobile.length != 11) { 63 | return false; 64 | } 65 | const reg = r'^1[0-9]\d{9}$'; 66 | return RegExp(reg).hasMatch(mobile); 67 | } 68 | 69 | static bool isVerifyCode(String code) { 70 | if (code.isEmpty || code.length != 6) { 71 | return false; 72 | } 73 | const reg = r'\d{6}$'; 74 | return RegExp(reg).hasMatch(code); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Fri Feb 25 11:24:59 CST 2022 8 | sdk.dir=/Users/a1111/Library/Android/sdk 9 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_video_player 2 | description: flutter、video、player 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.17.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | #刷新控件 33 | pull_to_refresh: ^2.0.0 34 | #请求库 35 | dio: ^4.0.3 36 | #加密库 37 | crypto: ^3.0.1 38 | #视频播放器 39 | video_player: ^2.2.19 40 | #滑动选项 41 | flutter_slidable: ^1.1.0 42 | #网页 43 | # webview_flutter: ^3.0.0 44 | # 机型等信息 45 | package_info_plus: ^1.3.0 46 | # NSUserDefaults on iOS, SharedPreferences on Android 47 | shared_preferences: ^2.0.11 48 | # 滑动组件 49 | flutter_swiper_null_safety: ^1.0.2 50 | # 路径 51 | path_provider: ^2.0.8 52 | # hive数据库 53 | hive: ^2.0.5 54 | hive_flutter: ^1.1.0 55 | hive_generator: ^1.1.2 56 | 57 | provider: ^6.0.2 58 | 59 | #图片处理库 60 | extended_image: ^6.0.3 61 | waterfall_flow: any 62 | 63 | # The following adds the Cupertino Icons font to your application. 64 | # Use with the CupertinoIcons class for iOS style icons. 65 | cupertino_icons: ^1.0.2 66 | 67 | dev_dependencies: 68 | flutter_test: 69 | sdk: flutter 70 | 71 | # The "flutter_lints" package below contains a set of recommended lints to 72 | # encourage good coding practices. The lint set provided by the package is 73 | # activated in the `analysis_options.yaml` file located at the root of your 74 | # package. See that file for information about deactivating specific lint 75 | # rules and activating additional ones. 76 | flutter_lints: ^1.0.0 77 | 78 | # For information on the generic Dart part of this file, see the 79 | # following page: https://dart.dev/tools/pub/pubspec 80 | 81 | # The following section is specific to Flutter. 82 | flutter: 83 | 84 | # The following line ensures that the Material Icons font is 85 | # included with your application, so that you can use the icons in 86 | # the material Icons class. 87 | uses-material-design: true 88 | 89 | # To add assets to your application, add an assets section, like this: 90 | assets: 91 | - resource/json/ 92 | - resource/image/pic_banner_shadow.png 93 | - resource/image/ic_vip.png 94 | - resource/image/ic_playing.gif 95 | - resource/image/pic_Avatar_n.png 96 | - resource/image/pic_Avatar_h.png 97 | - resource/image/splash_logo.png 98 | - resource/image/cover_img.png 99 | - resource/image/haokan_logo.png 100 | - resource/image/splash_shake.png 101 | 102 | # An image asset can refer to one or more resolution-specific "variants", see 103 | # https://flutter.dev/assets-and-images/#resolution-aware. 104 | 105 | # For details regarding adding assets from package dependencies, see 106 | # https://flutter.dev/assets-and-images/#from-packages 107 | 108 | # To add custom fonts to your application, add a fonts section here, 109 | # in this "flutter" section. Each entry in this list should have a 110 | # "family" key with the font family name, and a "fonts" key with a 111 | # list giving the asset and other descriptors for the font. For 112 | # example: 113 | # fonts: 114 | # - family: Schyler 115 | # fonts: 116 | # - asset: fonts/Schyler-Regular.ttf 117 | # - asset: fonts/Schyler-Italic.ttf 118 | # style: italic 119 | # - family: Trajan Pro 120 | # fonts: 121 | # - asset: fonts/TrajanPro.ttf 122 | # - asset: fonts/TrajanPro_Bold.ttf 123 | # weight: 700 124 | # 125 | # For details regarding fonts from package dependencies, 126 | # see https://flutter.dev/custom-fonts/#from-packages 127 | -------------------------------------------------------------------------------- /resource/image/2.0x/ic_vip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/2.0x/ic_vip.png -------------------------------------------------------------------------------- /resource/image/2.0x/pic_Avatar_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/2.0x/pic_Avatar_h.png -------------------------------------------------------------------------------- /resource/image/2.0x/pic_Avatar_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/2.0x/pic_Avatar_n.png -------------------------------------------------------------------------------- /resource/image/2.0x/pic_banner_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/2.0x/pic_banner_shadow.png -------------------------------------------------------------------------------- /resource/image/3.0x/ic_vip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/3.0x/ic_vip.png -------------------------------------------------------------------------------- /resource/image/3.0x/pic_Avatar_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/3.0x/pic_Avatar_h.png -------------------------------------------------------------------------------- /resource/image/3.0x/pic_Avatar_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/3.0x/pic_Avatar_n.png -------------------------------------------------------------------------------- /resource/image/3.0x/pic_banner_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/3.0x/pic_banner_shadow.png -------------------------------------------------------------------------------- /resource/image/3.0x/splash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/3.0x/splash_logo.png -------------------------------------------------------------------------------- /resource/image/cover_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/cover_img.png -------------------------------------------------------------------------------- /resource/image/haokan_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/haokan_logo.png -------------------------------------------------------------------------------- /resource/image/ic_playing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/ic_playing.gif -------------------------------------------------------------------------------- /resource/image/splash_shake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadudi/flutter_video_player/96f1df7e4a40c384b8a5af660f2cc9c83acd0b9a/resource/image/splash_shake.png -------------------------------------------------------------------------------- /resource/json/category_filters.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "recordsTotal": null, 6 | "data": [ 7 | { 8 | "filterType": "sort", 9 | "dramaFilterItemList": [ 10 | { 11 | "displayName": "最热播放", 12 | "value": "hot" 13 | }, 14 | { 15 | "displayName": "最近更新", 16 | "value": "new" 17 | }, 18 | { 19 | "displayName": "最受好评", 20 | "value": "score" 21 | }, 22 | { 23 | "displayName": "本季新剧", 24 | "value": "season_new" 25 | } 26 | ] 27 | }, 28 | { 29 | "filterType": "dramaType", 30 | "dramaFilterItemList": [ 31 | { 32 | "displayName": "全部", 33 | "value": "" 34 | }, 35 | { 36 | "displayName": "电视剧", 37 | "value": "TV" 38 | }, 39 | { 40 | "displayName": "电影", 41 | "value": "MOVIE" 42 | }, 43 | { 44 | "displayName": "综艺", 45 | "value": "VARIETY" 46 | }, 47 | { 48 | "displayName": "脱口秀", 49 | "value": "TALK" 50 | }, 51 | { 52 | "displayName": "纪录片", 53 | "value": "DOCUMENTARY" 54 | }, 55 | { 56 | "displayName": "动漫", 57 | "value": "COMIC" 58 | }, 59 | { 60 | "displayName": "迷你剧", 61 | "value": "MINISERIES" 62 | }, 63 | { 64 | "displayName": "网剧", 65 | "value": "SETI" 66 | } 67 | ] 68 | }, 69 | { 70 | "filterType": "area", 71 | "dramaFilterItemList": [ 72 | { 73 | "displayName": "全部", 74 | "value": "" 75 | }, 76 | { 77 | "displayName": "美国", 78 | "value": "美国" 79 | }, 80 | { 81 | "displayName": "英国", 82 | "value": "英国" 83 | }, 84 | { 85 | "displayName": "韩国", 86 | "value": "韩国" 87 | }, 88 | { 89 | "displayName": "日本", 90 | "value": "日本" 91 | }, 92 | { 93 | "displayName": "泰国", 94 | "value": "泰国" 95 | }, 96 | { 97 | "displayName": "内地", 98 | "value": "中国大陆" 99 | }, 100 | { 101 | "displayName": "中国香港", 102 | "value": "中国香港" 103 | }, 104 | { 105 | "displayName": "中国台湾", 106 | "value": "中国台湾" 107 | }, 108 | { 109 | "displayName": "其他", 110 | "value": "其他" 111 | } 112 | ] 113 | }, 114 | { 115 | "filterType": "plotType", 116 | "dramaFilterItemList": [ 117 | { 118 | "displayName": "全部", 119 | "value": "" 120 | }, 121 | { 122 | "displayName": "剧情", 123 | "value": "剧情" 124 | }, 125 | { 126 | "displayName": "喜剧", 127 | "value": "喜剧" 128 | }, 129 | { 130 | "displayName": "动作", 131 | "value": "动作" 132 | }, 133 | { 134 | "displayName": "冒险", 135 | "value": "冒险" 136 | }, 137 | { 138 | "displayName": "爱情", 139 | "value": "爱情" 140 | }, 141 | { 142 | "displayName": "歌舞", 143 | "value": "歌舞" 144 | }, 145 | { 146 | "displayName": "恐怖", 147 | "value": "恐怖" 148 | }, 149 | { 150 | "displayName": "科幻", 151 | "value": "科幻" 152 | }, 153 | { 154 | "displayName": "奇幻", 155 | "value": "奇幻" 156 | }, 157 | { 158 | "displayName": "动画", 159 | "value": "动画" 160 | }, 161 | { 162 | "displayName": "悬疑", 163 | "value": "悬疑" 164 | }, 165 | { 166 | "displayName": "惊悚", 167 | "value": "惊悚" 168 | }, 169 | { 170 | "displayName": "犯罪", 171 | "value": "犯罪" 172 | }, 173 | { 174 | "displayName": "同性", 175 | "value": "同性" 176 | }, 177 | { 178 | "displayName": "传记", 179 | "value": "传记" 180 | }, 181 | { 182 | "displayName": "历史", 183 | "value": "历史" 184 | }, 185 | { 186 | "displayName": "战争", 187 | "value": "战争" 188 | }, 189 | { 190 | "displayName": "灾难", 191 | "value": "灾难" 192 | }, 193 | { 194 | "displayName": "美食", 195 | "value": "美食" 196 | }, 197 | { 198 | "displayName": "真人秀", 199 | "value": "真人秀" 200 | } 201 | ] 202 | }, 203 | { 204 | "filterType": "year", 205 | "dramaFilterItemList": [ 206 | { 207 | "displayName": "全部", 208 | "value": "" 209 | }, 210 | { 211 | "displayName": "2021", 212 | "value": "2021" 213 | }, 214 | { 215 | "displayName": "2020", 216 | "value": "2020" 217 | }, 218 | { 219 | "displayName": "2019", 220 | "value": "2019" 221 | }, 222 | { 223 | "displayName": "2018", 224 | "value": "2018" 225 | }, 226 | { 227 | "displayName": "2017", 228 | "value": "2017" 229 | }, 230 | { 231 | "displayName": "2016", 232 | "value": "2016" 233 | }, 234 | { 235 | "displayName": "2015", 236 | "value": "2015" 237 | }, 238 | { 239 | "displayName": "2014-2011", 240 | "value": "2014-2011" 241 | }, 242 | { 243 | "displayName": "2010-2000", 244 | "value": "2010-2000" 245 | }, 246 | { 247 | "displayName": "90年代", 248 | "value": "90年代" 249 | }, 250 | { 251 | "displayName": "80年代", 252 | "value": "80年代" 253 | } 254 | ] 255 | }, 256 | { 257 | "filterType": "serializedStatus", 258 | "dramaFilterItemList": [ 259 | { 260 | "displayName": "全部", 261 | "value": "" 262 | }, 263 | { 264 | "displayName": "连载", 265 | "value": "0" 266 | }, 267 | { 268 | "displayName": "完结", 269 | "value": "1" 270 | }, 271 | { 272 | "displayName": "未开播", 273 | "value": "2" 274 | } 275 | ] 276 | } 277 | ] 278 | } -------------------------------------------------------------------------------- /resource/json/drama_info_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "data": { 6 | "seriesList": null, 7 | "drama": { 8 | "id": 333333, 9 | "createTime": "2021年8月19日", 10 | "title": "神医喜来乐 来源于好看网 非商业用途 侵权删", 11 | "cover": "https://pic.rmb.bdstatic.com/baidu-rmb-video-cover-1/2021-7/1626194565491/6f3d7a62247e.jpg@s_0,w_800,h_1000,q_80", 12 | "score": 7.4, 13 | "year": "2022", 14 | "area": "中国", 15 | "enName": "播放:54.7万", 16 | "cat": "古装剧", 17 | "dramaType": "电视剧", 18 | "serializedStatus": "已完结 共35集 这里测试只放4集", 19 | "brief":"清末,沧州的“一笑堂”郎中喜来乐治病救人,乐善好施,本与视无争,偏偏的,京城的靖王爷的格格得了重病被太医王天和宣布为绝症,靖王爷亲信鲁正明将喜来乐推荐到京城为格格治病,喜来乐用裸体熏浴法将牙关紧闭、汤水不进的格格救活,赢得靖王爷欢喜,但却引起王太医切齿嫉恨,于是千方百计对喜来乐加以谋害。喜来乐本来就恋着在沧州开饭馆的情人,年轻漂亮的寡妇赛西施,更不愿在京城这是非之地与王太医纠缠,他几番谢绝靖王爷挽留美意,靖王爷和鲁正明终于用计将他留在京城,并把“一笑堂”搬到京城来开业。", 20 | "episodeCount": 4 21 | }, 22 | "episodeList": [ 23 | { 24 | "episodeNo": 1, 25 | "text": "", 26 | "episodeSid": 10001 27 | }, 28 | { 29 | "episodeNo": 2, 30 | "text": "", 31 | "episodeSid": 10002 32 | }, 33 | { 34 | "episodeNo": 3, 35 | "text": "", 36 | "episodeSid": 10003 37 | }, 38 | { 39 | "episodeNo": 4, 40 | "text": "", 41 | "episodeSid": 10004 42 | } 43 | ], 44 | "playInfo": { 45 | "url": "https://vd4.bdstatic.com/mda-mhhu5st92hmp1an5/default/hksr/1639922751/mda-mhhu5st92hmp1an5.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645698585-0-0-49364b64cb374e0a3c6751d47930bd38&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=3585533028&vid=2015598530162537812&abtest=17451_1-3000216_1", 46 | "currentQuality": "LD", 47 | "mediaId": 11111, 48 | "episodeSid": 10001, 49 | "startingLength": 0 50 | }, 51 | "qualityList": [ 52 | { 53 | "quality": "FD", 54 | "qualityName": "标清" 55 | }, 56 | { 57 | "quality": "LD", 58 | "qualityName": "高清" 59 | }, 60 | { 61 | "quality": "AI_720", 62 | "qualityName": "超清" 63 | }, 64 | { 65 | "quality": "AI_1080", 66 | "qualityName": "蓝光" 67 | } 68 | ] 69 | } 70 | } -------------------------------------------------------------------------------- /resource/json/login_send_code.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "0000", 3 | "msg": "Success", 4 | "data": "" 5 | } -------------------------------------------------------------------------------- /resource/json/mine_page.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "recordsTotal": null, 6 | "data": { 7 | "bannerTop": [], 8 | "sections": [ 9 | { 10 | "id": 4358, 11 | "display": "SCROLL", 12 | "moreText": "", 13 | "name": "其他", 14 | "position": 23, 15 | "sectionType": "MAGIC_CUBE", 16 | "sequence": 1, 17 | "targetId": null, 18 | "targetType": null, 19 | "displayTitle": "1", 20 | "sectionContents": [ 21 | { 22 | "title": "我的收藏", 23 | "icon": "http://img.juquanquanapp.com/img/img/20211130/o_07a2f3f668464fd398cfc48a65c60754.png", 24 | "targetType": "ASSIGN_PAGE", 25 | "targetId": "" 26 | }, 27 | { 28 | "title": "设置", 29 | "icon": "http://img.juquanquanapp.com/img/img/20210915/o_aa7d7ab579174a43843b919634c56f59.png", 30 | "targetType": "ASSIGN_PAGE", 31 | "targetId": "" 32 | }, 33 | { 34 | "title": "问题反馈", 35 | "icon": "http://img.juquanquanapp.com/img/img/20210915/o_b627939ccc564971969dc8882de6bac9.png", 36 | "targetType": "H5", 37 | "targetId": "http%3A%2F%2Fmobile.juquanquanapp.com%2FcontactCustomerService" 38 | }, 39 | { 40 | "title": "儿童隐私政策", 41 | "icon": "http://img.juquanquanapp.com/img/img/20210915/o_9ae0cb77f0384a3ea64ea97c032a102c.png", 42 | "targetType": "H5", 43 | "targetId": "https%3A%2F%2Fmobile.juquanquanapp.com%2Fagree%2FCHILDREN_PRIVACY_JQQ" 44 | }, 45 | { 46 | "title": "隐私政策", 47 | "icon": "http://img.juquanquanapp.com/img/img/20210915/o_56566a1281264d9bbec66ff286f08e5f.png", 48 | "targetType": "H5", 49 | "targetId": "https%3A%2F%2Fmobile.juquanquanapp.com%2Fagree%2FCONCEAL_JQQ_IOS" 50 | }, 51 | { 52 | "title": "用户协议", 53 | "icon": "http://img.juquanquanapp.com/img/img/20210915/o_a39d6f2d352d4f3db0159b700a5635de.png", 54 | "targetType": "H5", 55 | "targetId": "https%3A%2F%2Fmobile.juquanquanapp.com%2Fagree%2FUSERAGREE_JQQ_IOS" 56 | } 57 | ], 58 | "startTime": null, 59 | "endTime": null 60 | } 61 | ] 62 | } 63 | } -------------------------------------------------------------------------------- /resource/json/play1_info_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "data": { 6 | "playInfo": { 7 | "title": "神医喜来乐第1集", 8 | "url": "https://vd4.bdstatic.com/mda-mhhu5st92hmp1an5/default/hksr/1639922751/mda-mhhu5st92hmp1an5.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645698585-0-0-49364b64cb374e0a3c6751d47930bd38&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=3585533028&vid=2015598530162537812&abtest=17451_1-3000216_1", 9 | "currentQuality": "LD", 10 | "episodeSid": 10001, 11 | "startingLength": 0 12 | }, 13 | "qualityList": [ 14 | { 15 | "quality": "FD", 16 | "qualityName": "标清", 17 | "url": "https://vd4.bdstatic.com/mda-mhhu5st92hmp1an5/default/hksr/1639922751/mda-mhhu5st92hmp1an5.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645698585-0-0-49364b64cb374e0a3c6751d47930bd38&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=3585533028&vid=2015598530162537812&abtest=17451_1-3000216_1" 18 | }, 19 | { 20 | "quality": "LD", 21 | "qualityName": "高清", 22 | "url": "https://vd4.bdstatic.com/mda-mhhu5st92hmp1an5/hd/hksr/1639922751/mda-mhhu5st92hmp1an5.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645698585-0-0-a58203b02781cb5a14fbb82860773399&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=3585533028&vid=2015598530162537812&abtest=17451_1-3000216_1" 23 | }, 24 | { 25 | "quality": "AI_720", 26 | "qualityName": "超清", 27 | "url": "https://vd4.bdstatic.com/mda-mhhu5st92hmp1an5/sc/hksr/1639922751/mda-mhhu5st92hmp1an5.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645698585-0-0-1533a66c3606a63c4c979853abf0ff95&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=3585533028&vid=2015598530162537812&abtest=17451_1-3000216_1" 28 | }, 29 | { 30 | "quality": "AI_1080", 31 | "qualityName": "蓝光", 32 | "url": "https://vd4.bdstatic.com/mda-mhhu5st92hmp1an5/1080p/hksr/1639922751/mda-mhhu5st92hmp1an5.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645698585-0-0-d833821a4a0d79de5c6a0134efd386e3&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=3585533028&vid=2015598530162537812&abtest=17451_1-3000216_1" 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /resource/json/play2_info_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "data": { 6 | "playInfo": { 7 | "title": "神医喜来乐第2集", 8 | "url": "https://vd3.bdstatic.com/mda-mhhu5t08rqr1bc8m/default/hksr/1639923626/mda-mhhu5t08rqr1bc8m.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699433-0-0-31d2f1aaf827cc9e6af7e14959285789&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0833412090&vid=5177033977559325844&abtest=17451_1-3000216_1", 9 | "currentQuality": "LD", 10 | "episodeSid": 10002, 11 | "startingLength": 0 12 | }, 13 | "qualityList": [ 14 | { 15 | "quality": "FD", 16 | "qualityName": "标清", 17 | "url": "https://vd3.bdstatic.com/mda-mhhu5t08rqr1bc8m/default/hksr/1639923626/mda-mhhu5t08rqr1bc8m.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699433-0-0-31d2f1aaf827cc9e6af7e14959285789&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0833412090&vid=5177033977559325844&abtest=17451_1-3000216_1" 18 | }, 19 | { 20 | "quality": "LD", 21 | "qualityName": "高清", 22 | "url": "https://vd3.bdstatic.com/mda-mhhu5t08rqr1bc8m/hd/hksr/1639923626/mda-mhhu5t08rqr1bc8m.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699433-0-0-6cb591562c18deaa9cb22db176501266&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0833412090&vid=5177033977559325844&abtest=17451_1-3000216_1" 23 | }, 24 | { 25 | "quality": "AI_720", 26 | "qualityName": "超清", 27 | "url": "https://vd3.bdstatic.com/mda-mhhu5t08rqr1bc8m/sc/hksr/1639923626/mda-mhhu5t08rqr1bc8m.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699433-0-0-b8ef6e5b3bad90ce6c440d89cfdad67a&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0833412090&vid=5177033977559325844&abtest=17451_1-3000216_1" 28 | }, 29 | { 30 | "quality": "AI_1080", 31 | "qualityName": "蓝光", 32 | "url": "https://vd3.bdstatic.com/mda-mhhu5t08rqr1bc8m/1080p/hksr/1639923626/mda-mhhu5t08rqr1bc8m.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699433-0-0-5d6ea26809fb378f7f890d80185cd79a&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0833412090&vid=5177033977559325844&abtest=17451_1-3000216_1" 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /resource/json/play3_info_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "data": { 6 | "playInfo": { 7 | "title": "神医喜来乐第3集", 8 | "url": "https://vd4.bdstatic.com/mda-mhhu5tawjkb7613d/default/hksr/1639923920/mda-mhhu5tawjkb7613d.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699080-0-0-5af792314f2a20ff5728715f7a7a378d&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0480703955&vid=17713599438673069285&abtest=17451_1-3000216_1", 9 | "currentQuality": "LD", 10 | "episodeSid": 10003, 11 | "startingLength": 0 12 | }, 13 | "qualityList": [ 14 | { 15 | "quality": "FD", 16 | "qualityName": "标清", 17 | "url": "https://vd4.bdstatic.com/mda-mhhu5tawjkb7613d/default/hksr/1639923920/mda-mhhu5tawjkb7613d.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699080-0-0-5af792314f2a20ff5728715f7a7a378d&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0480703955&vid=17713599438673069285&abtest=17451_1-3000216_1" 18 | }, 19 | { 20 | "quality": "LD", 21 | "qualityName": "高清", 22 | "url": "https://vd4.bdstatic.com/mda-mhhu5tawjkb7613d/hd/hksr/1639923920/mda-mhhu5tawjkb7613d.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699080-0-0-55706a687ca93d3df096614f23036e3a&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0480703955&vid=17713599438673069285&abtest=17451_1-3000216_1" 23 | }, 24 | { 25 | "quality": "AI_720", 26 | "qualityName": "超清", 27 | "url": "https://vd4.bdstatic.com/mda-mhhu5tawjkb7613d/sc/hksr/1639923920/mda-mhhu5tawjkb7613d.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699080-0-0-9b151e019cc22b786da56671296c602a&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0480703955&vid=17713599438673069285&abtest=17451_1-3000216_1" 28 | }, 29 | { 30 | "quality": "AI_1080", 31 | "qualityName": "蓝光", 32 | "url": "https://vd4.bdstatic.com/mda-mhhu5tawjkb7613d/1080p/hksr/1639923920/mda-mhhu5tawjkb7613d.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699080-0-0-0415fe6e89f515141f722fc29a63ef89&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0480703955&vid=17713599438673069285&abtest=17451_1-3000216_1" 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /resource/json/play4_info_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "data": { 6 | "playInfo": { 7 | "title": "神医喜来乐第3集", 8 | "url": "https://vd3.bdstatic.com/mda-mhhu5tfh9gk4rjer/default/hksr/1639924225/mda-mhhu5tfh9gk4rjer.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699599-0-0-cb7485b9c9000d9f5220f335ad70959f&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0999124611&vid=14538582800946936017&abtest=17451_1-3000216_1", 9 | "currentQuality": "LD", 10 | "episodeSid": 10004, 11 | "startingLength": 0 12 | }, 13 | "qualityList": [ 14 | { 15 | "quality": "FD", 16 | "qualityName": "标清", 17 | "url": "https://vd3.bdstatic.com/mda-mhhu5tfh9gk4rjer/default/hksr/1639924225/mda-mhhu5tfh9gk4rjer.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699599-0-0-cb7485b9c9000d9f5220f335ad70959f&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0999124611&vid=14538582800946936017&abtest=17451_1-3000216_1" 18 | }, 19 | { 20 | "quality": "LD", 21 | "qualityName": "高清", 22 | "url": "https://vd3.bdstatic.com/mda-mhhu5tfh9gk4rjer/hd/hksr/1639924225/mda-mhhu5tfh9gk4rjer.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699599-0-0-b6b713873eaa3dad0526c9c78018a203&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0999124611&vid=14538582800946936017&abtest=17451_1-3000216_1" 23 | }, 24 | { 25 | "quality": "AI_720", 26 | "qualityName": "超清", 27 | "url": "https://vd3.bdstatic.com/mda-mhhu5tfh9gk4rjer/sc/hksr/1639924225/mda-mhhu5tfh9gk4rjer.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699599-0-0-a627624cdb9cacb59a01d433ad873d39&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0999124611&vid=14538582800946936017&abtest=17451_1-3000216_1" 28 | }, 29 | { 30 | "quality": "AI_1080", 31 | "qualityName": "蓝光", 32 | "url": "https://vd3.bdstatic.com/mda-mhhu5tfh9gk4rjer/1080p/hksr/1639924225/mda-mhhu5tfh9gk4rjer.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1645699599-0-0-d5c09228b501ced667209d852527859a&bcevod_channel=searchbox_feed&cd=0&pd=1&pt=3&logid=0999124611&vid=14538582800946936017&abtest=17451_1-3000216_1" 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /resource/json/user_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": "", 3 | "code": "0000", 4 | "msg": "Success", 5 | "data": { 6 | "user": { 7 | "userId": 10000000000001, 8 | "pwd": "123456", 9 | "nickName": "与君共勉", 10 | "mobile": "1999999999" 11 | }, 12 | "token": "1111111111xxxxxxx0000000000" 13 | } 14 | } -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_video_player/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------