15 Commits

Author SHA1 Message Date
Juraldinio e9b5a88c27 Remove wrong commoted xcframework 2023-09-21 20:21:00 +03:00
Juraldinio 81092cf8e9 xcframework from build machine 2023-09-21 18:39:43 +03:00
Андрей Геращенко 64ae8f989c PSDK-983: Обновлен фреймворк Nutplayer 2023-09-21 18:28:48 +03:00
Андрей Геращенко d9da0e1ad2 PSDK-983: Исправил проблему импорта video_player_controller'a 2023-09-21 16:19:49 +03:00
Андрей Геращенко 8d9aa9159f PSDK-983: Добавил параметр в pubspec 2023-09-21 14:44:12 +03:00
Андрей Геращенко 5a41db2360 PSDK-983: Промежуточный коммит 2023-09-21 11:31:51 +03:00
Андрей Геращенко 7ce68d2768 Merge branch 'develop' into feature/PSDK-983/init_methods 2023-09-20 15:26:46 +03:00
Андрей Геращенко 80e603a1f9 PSDK-983: Добавил создание контекста и регистрацию сущности платформы 2023-09-20 15:26:04 +03:00
Андрей Геращенко e1bd9d8af7 PSDK-983: Удалил лишнее со стороны флаттера 2023-09-20 12:59:02 +03:00
Андрей Геращенко 9887ed6d11 PSDK-983: Правки со стороны платформы иос 2023-09-20 12:54:46 +03:00
Андрей Геращенко 9d0cfc59c6 PSDK-969: Переделано отображение проигрывателя 2023-09-20 02:41:18 +03:00
Андрей Геращенко 5c99970036 PSDK-983: Исправил идентификатор проигрывателя 2023-09-20 02:20:01 +03:00
Андрей Геращенко 93f9bd34aa PSDK-983: Добавлен запуск проигрывателя 2023-09-20 02:01:02 +03:00
Андрей Геращенко 544cb03e20 PSDK-983: Рефакторинг под новые методы универсального интерфейса 2023-09-20 01:25:11 +03:00
Андрей Геращенко 58df4000f8 PSDK-983: Промежуточный коммит, созданы методы и прокинуты связи, в процессе рефакторинг старого кода согласно новым требованиям 2023-09-19 21:57:28 +03:00
194 changed files with 5611 additions and 28939 deletions
+37 -60
View File
@@ -1,18 +1,21 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
Build:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_success
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_success
allow_failure: false
# Build template
.BuildApplication: &BuildApplication
when: on_success
allow_failure: false
tags:
- macos
- flutter
stage: build
Build:
<<: *BuildApplication
only:
refs:
- develop
- tags
artifacts:
paths:
- ./Result
@@ -22,14 +25,11 @@ Build:
- ./toolchain/build_platforms.sh nut_player device
ManualBuild:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME =~ /^feature/
when: manual
allow_failure: false
tags:
- macos
- flutter
stage: build
<<: *BuildApplication
when: manual
only:
refs:
- /^feature/
artifacts:
paths:
- ./Result
@@ -39,13 +39,8 @@ ManualBuild:
- ./toolchain/build_platforms.sh nut_player device
Deploy:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_success
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_success
allow_failure: false
when: on_success
allow_failure: false
tags:
- macos
- flutter
@@ -59,6 +54,10 @@ Deploy:
IPA_FILE: NutPlayerFlutter.ipa
IPA_PATH: ./Result
stage: deploy
only:
refs:
- develop
- tags
script:
- chmod +x ./toolchain/deploy_iOS.sh
- chmod +x ./toolchain/deploy_Android.sh
@@ -66,10 +65,8 @@ Deploy:
- ./toolchain/deploy_Android.sh nut_player
Deploy_Feature:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME =~ /^feature/
when: manual
allow_failure: false
when: manual
allow_failure: false
tags:
- macos
- flutter
@@ -83,6 +80,9 @@ Deploy_Feature:
IPA_FILE: NutPlayerFlutter.ipa
IPA_PATH: ./Result
stage: deploy
only:
refs:
- /^feature/
script:
- chmod +x ./toolchain/deploy_iOS.sh
- chmod +x ./toolchain/deploy_Android.sh
@@ -90,16 +90,14 @@ Deploy_Feature:
- ./toolchain/deploy_iOS.sh nut_player ${IPA_DEPLOY_TARGET} ${IPA_PATH} ${IPA_FILE} iOS
notifyMessengerFail:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_failure
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_failure
allow_failure: false
when: on_failure
needs:
- job: Deploy
artifacts: false
only:
refs:
- develop
- tags
stage: notify
tags:
- macos
@@ -109,16 +107,14 @@ notifyMessengerFail:
- ./toolchain/notification.sh FAIL
notifyMessengerSuccess:
rules:
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME == "develop"
when: on_success
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG != null
when: on_success
allow_failure: false
when: on_success
needs:
- job: Deploy
artifacts: false
only:
refs:
- develop
- tags
stage: notify
tags:
- macos
@@ -126,22 +122,3 @@ notifyMessengerSuccess:
script:
- chmod +x ./toolchain/notification.sh
- ./toolchain/notification.sh SUCCESS
#ManualUpdateApplications:
# rules:
# - if: $CI_COMMIT_REF_NAME == "develop"
# when: manual
# allow_failure: false
# - if: $CI_COMMIT_TAG != null
# when: manual
# allow_failure: false
# tags:
# - macos
# - flutter
# stage: notify
# script:
# - |
# curl -X "POST" "https://gitlab.nut.team/api/v4/projects/574/trigger/pipeline" \
# -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
# --data-urlencode "token=$K8S_TOKEN_UPDATE_CONFRONTARE_APPLICATION" \
# --data-urlencode "ref=develop"
+14 -11
View File
@@ -3,43 +3,46 @@ variables:
# Linting stage
Lint:
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
when: always
stage: lint
when: always
tags:
- macos
- flutter
only:
refs:
- merge_requests
script:
- chmod +x ./toolchain/linting.sh
- ./toolchain/linting.sh nut_player
# Testing stage
Tests:
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
when: on_success
allow_failure: false
stage: test
when: on_success
allow_failure: false
tags:
- macos
- flutter
only:
refs:
- merge_requests
script:
- chmod +x ./toolchain/testing.sh
- ./toolchain/testing.sh nut_player
# Build Application
mrBuildApplication:
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
when: on_success
allow_failure: false
when: on_success
allow_failure: false
tags:
- macos
- flutter
stage: build
needs:
- job: Tests
only:
refs:
- merge_requests
script:
- chmod +x ./toolchain/build_platforms.sh
- ./toolchain/build_platforms.sh nut_player simulator
-4
View File
@@ -1,8 +1,5 @@
before_script:
- export PATH="$PATH:$FLUTTER_PATH"
- echo "CI_PIPELINE_SOURCE = $CI_PIPELINE_SOURCE"
- echo "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME = $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
- echo "CI_DEFAULT_BRANCH = $CI_DEFAULT_BRANCH"
stages:
- lint
@@ -14,4 +11,3 @@ stages:
include:
- '.before-merge-request.yml'
- '.after-merge-request.yml'
- '.triggered.yml'
-72
View File
@@ -1,72 +0,0 @@
UpdateDependentLibraries:
rules:
- if: $CI_PIPELINE_SOURCE == "trigger" && ($CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "feature/autoupdate")
when: on_success
allow_failure: false
tags:
- macos
- flutter
stage: build
script:
- |
echo "♻️ Update library for nut_player"
echo $LIBRARY_TYPE
echo $LIBRARY_PATH
if [ -z "$LIBRARY_PATH" ]; then
echo "⛔️ Path not passed."
exit 1
fi
echo "💈 Download library"
if [ "$LIBRARY_TYPE" == "ios" ]; then
wget --header "PRIVATE-TOKEN: $K8S_TOKEN_LIBRARY_IOS" "$LIBRARY_PATH" -O artifact.zip
echo "📦 Unzip library"
unzip artifact.zip
echo "📲 Copy library"
cp -R ios/Result/NutPlayer/NutPlayer.xcframework nut_player_ios/ios/Vendors
echo "🗑️ Remove temporary"
rm artifact.zip
rm -rf ios
echo "⚙️ Add to Git new library"
git add -A && git commit -m "Update iOS library [CI/CD]"
git status
project_url=$(echo $CI_PROJECT_URL | sed 's/https:\/\///')
git remote set-url origin https://oauth2:$K8S_TOKEN_UPLOAD_LIBRARY@$project_url
git push origin HEAD:$CI_COMMIT_REF_NAME
fi
if [ "$LIBRARY_TYPE" == "android" ]; then
wget --header "PRIVATE-TOKEN: $K8S_TOKEN_LIBRARY_ANDROID" "$LIBRARY_PATH" -O artifact.zip
echo "📦 Unzip library"
unzip artifact.zip
echo "🤖 Copy library"
cp -R android/artifacts/*.aar nut_player_android/android/libs
echo "🗑️ Remove temporary"
rm artifact.zip
rm -rf android
echo "⚙️ Add to Git new library"
git add -A && git commit -m "Update Android library [CI/CD]"
git status
project_url=$(echo $CI_PROJECT_URL | sed 's/https:\/\///')
git remote set-url origin https://oauth2:$K8S_TOKEN_UPLOAD_LIBRARY@$project_url
git push origin HEAD:$CI_COMMIT_REF_NAME
fi
+11 -31
View File
@@ -5,42 +5,27 @@ plugins {
id 'com.google.gms.google-services'
}
def flutterSdkVersions = new Properties()
def flutterSdkVersionsFile = rootProject.file('flutterSdkVersions.properties')
if (flutterSdkVersionsFile.exists()) {
flutterSdkVersionsFile.withReader('UTF-8') { reader ->
flutterSdkVersions.load(reader)
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = flutterSdkVersions.getProperty('flutter.versionCode')
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = flutterSdkVersions.getProperty('flutter.versionName')
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def flutterCompileSdkVersion = flutterSdkVersions.getProperty('flutter.compileSdkVersion')
if (flutterCompileSdkVersion == null) {
flutterCompileSdkVersion = 1
}
def flutterTargetSdkVersion = flutterSdkVersions.getProperty('flutter.targetSdkVersion')
if (flutterTargetSdkVersion == null) {
flutterTargetSdkVersion = 1
}
def flutterMinSdkVersion = flutterSdkVersions.getProperty('flutter.minSdkVersion')
if (flutterMinSdkVersion == null) {
flutterMinSdkVersion = 1
}
android {
namespace "tech.nut.nutplayer.flutter"
compileSdkVersion flutterCompileSdkVersion.toInteger()
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
@@ -61,8 +46,8 @@ android {
applicationId "tech.nut.nutplayer.flutter"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutterMinSdkVersion
targetSdkVersion flutterTargetSdkVersion
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -72,8 +57,6 @@ android {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
shrinkResources false
minifyEnabled false
}
}
}
@@ -82,7 +65,4 @@ flutter {
source '../..'
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:32.3.1"))
implementation(platform("com.google.firebase:firebase-storage-ktx"))
}
dependencies {}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -1,10 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="nut_player_example"
android:networkSecurityConfig="@xml/network_security_config"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -32,5 +30,4 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -1,11 +0,0 @@
<?xml version ="1.0" encoding ="utf-8"?><!-- Learn More about how to use App Actions: https://developer.android.com/guide/actions/index.html -->
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<!-- Trust preinstalled CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
+2 -6
View File
@@ -6,9 +6,9 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.3'
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.15'
classpath 'com.google.gms:google-services:+'
}
}
@@ -16,10 +16,6 @@ allprojects {
repositories {
google()
mavenCentral()
maven {
// [required] aar plugin// [required] aar plugin
url "${project(':nut_player_android').projectDir}/build"
}
}
}
@@ -1,5 +0,0 @@
flutter.versionName=1.0
flutter.versionCode=1
flutter.compileSdkVersion=33
flutter.minSdkVersion=21
flutter.targetSdkVersion=33
@@ -1,7 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableDexingArtifactTransform=false
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

+2 -137
View File
@@ -1,163 +1,28 @@
PODS:
- Firebase/CoreOnly (10.15.0):
- FirebaseCore (= 10.15.0)
- Firebase/Crashlytics (10.15.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 10.15.0)
- Firebase/Storage (10.15.0):
- Firebase/CoreOnly
- FirebaseStorage (~> 10.15.0)
- firebase_core (2.17.0):
- Firebase/CoreOnly (= 10.15.0)
- Flutter
- firebase_crashlytics (3.3.7):
- Firebase/Crashlytics (= 10.15.0)
- firebase_core
- Flutter
- firebase_storage (11.2.8):
- Firebase/Storage (= 10.15.0)
- firebase_core
- Flutter
- FirebaseAppCheckInterop (10.16.0)
- FirebaseAuthInterop (10.16.0)
- FirebaseCore (10.15.0):
- FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/Logger (~> 7.8)
- FirebaseCoreExtension (10.16.0):
- FirebaseCore (~> 10.0)
- FirebaseCoreInternal (10.16.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseCrashlytics (10.15.0):
- FirebaseCore (~> 10.5)
- FirebaseInstallations (~> 10.0)
- FirebaseSessions (~> 10.5)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.8)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (~> 2.1)
- FirebaseInstallations (10.16.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseSessions (10.16.0):
- FirebaseCore (~> 10.5)
- FirebaseCoreExtension (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.10)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesSwift (~> 2.1)
- FirebaseStorage (10.15.0):
- FirebaseAppCheckInterop (~> 10.0)
- FirebaseAuthInterop (~> 10.0)
- FirebaseCore (~> 10.0)
- FirebaseCoreExtension (~> 10.0)
- GTMSessionFetcher/Core (< 4.0, >= 2.1)
- Flutter (1.0.0)
- GoogleDataTransport (9.2.5):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Environment (7.11.5):
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.11.5):
- GoogleUtilities/Environment
- "GoogleUtilities/NSData+zlib (7.11.5)"
- GoogleUtilities/UserDefaults (7.11.5):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (3.1.1)
- integration_test (0.0.1):
- Flutter
- nanopb (2.30909.0):
- nanopb/decode (= 2.30909.0)
- nanopb/encode (= 2.30909.0)
- nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0)
- nut_player_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- PromisesObjC (2.3.1)
- PromisesSwift (2.3.1):
- PromisesObjC (= 2.3.1)
- screen_brightness_ios (0.1.0):
- Flutter
DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_storage (from `.symlinks/plugins/firebase_storage/ios`)
- Flutter (from `Flutter`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- nut_player_ios (from `.symlinks/plugins/nut_player_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
SPEC REPOS:
trunk:
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseSessions
- FirebaseStorage
- GoogleDataTransport
- GoogleUtilities
- GTMSessionFetcher
- nanopb
- PromisesObjC
- PromisesSwift
EXTERNAL SOURCES:
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_storage:
:path: ".symlinks/plugins/firebase_storage/ios"
Flutter:
:path: Flutter
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
nut_player_ios:
:path: ".symlinks/plugins/nut_player_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
SPEC CHECKSUMS:
Firebase: 66043bd4579e5b73811f96829c694c7af8d67435
firebase_core: 28e84c2a4fcf6a50ef83f47b145ded8c1fa331e4
firebase_crashlytics: 36b8a72e23437dbb69bd97102661ce31b6721be5
firebase_storage: 5aa5cd1cfc03814e3417b6718d805e5d1804f990
FirebaseAppCheckInterop: 82358cff9f33452dd44259e88eea5e562500b1cb
FirebaseAuthInterop: b79fab8ce80e685145eee5f973d9ad5210e19d44
FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e
FirebaseCoreExtension: 2dbc745b337eb99d2026a7a309ae037bd873f45e
FirebaseCoreInternal: 26233f705cc4531236818a07ac84d20c333e505a
FirebaseCrashlytics: a83f26fb922a3fe181eb738fb4dcf0c92bba6455
FirebaseInstallations: b822f91a61f7d1ba763e5ccc9d4f2e6f2ed3b3ee
FirebaseSessions: 96e7781e545929cde06dd91088ddbb0841391b43
FirebaseStorage: 1d4be239ea32fb3c0f3680a6f2b706d6cabe37f2
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2
GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084
GTMSessionFetcher: e8647203b65cee28c5f73d0f473d096653945e72
integration_test: 13825b8a9334a850581300559b8839134b124670
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
nut_player_ios: c6964e0278d1a01a40929de47bbc7b8bf5bbc8d8
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
nut_player_ios: 4b33be9feaf24c953526324edf4c2e9e7f1ab5f3
PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5
COCOAPODS: 1.13.0
COCOAPODS: 1.12.1
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
02000B77C8E488D819458EB8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF805F226A1CA116AFCD23B /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
@@ -14,9 +15,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9D6C432C130B5C4CCD4F030E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C45B8549278BCFD201CE0AFC /* Pods_RunnerTests.framework */; };
A4D9603085CECA05CC013C25 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0FD33782B308F981743B1D9A /* Pods_Runner.framework */; };
B173FFAC2ACEB6A8009C6CB7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B173FFAB2ACEB6A8009C6CB7 /* GoogleService-Info.plist */; };
E6DD470C591A4891FD8CC5D5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -43,18 +42,18 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0FD33782B308F981743B1D9A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5BBC6444966502149BA91788 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
6385803DFF5615AB5A426D76 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
41C5B4FECBC1AB22501D4D1F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
6B6C4FD9C0BBEBB441A65F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
6FF805F226A1CA116AFCD23B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8CEF516E13842BF4F217306E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -62,11 +61,10 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AE4F8A509E834A1E6019B4A1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
B173FFAB2ACEB6A8009C6CB7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
BF64510E1DC112A547CBFBC5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
C45B8549278BCFD201CE0AFC /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
ED0594A0BE28CA6FB21AC6AE /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
A8D3A5B8216DAA0326943AC2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
F2D2F836E458DFB68D190B87 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
FA5F9924AD21CAB1263D7189 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
FE9E9C027F59BE61D9D85175 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -74,7 +72,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A4D9603085CECA05CC013C25 /* Pods_Runner.framework in Frameworks */,
02000B77C8E488D819458EB8 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -82,22 +80,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9D6C432C130B5C4CCD4F030E /* Pods_RunnerTests.framework in Frameworks */,
E6DD470C591A4891FD8CC5D5 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
182DEA855A45E43DC6B6F1B3 /* Frameworks */ = {
isa = PBXGroup;
children = (
0FD33782B308F981743B1D9A /* Pods_Runner.framework */,
C45B8549278BCFD201CE0AFC /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@@ -109,16 +98,25 @@
40FDE73F8AC767B1B188CE85 /* Pods */ = {
isa = PBXGroup;
children = (
BF64510E1DC112A547CBFBC5 /* Pods-Runner.debug.xcconfig */,
5BBC6444966502149BA91788 /* Pods-Runner.release.xcconfig */,
8CEF516E13842BF4F217306E /* Pods-Runner.profile.xcconfig */,
6385803DFF5615AB5A426D76 /* Pods-RunnerTests.debug.xcconfig */,
AE4F8A509E834A1E6019B4A1 /* Pods-RunnerTests.release.xcconfig */,
ED0594A0BE28CA6FB21AC6AE /* Pods-RunnerTests.profile.xcconfig */,
FA5F9924AD21CAB1263D7189 /* Pods-Runner.debug.xcconfig */,
A8D3A5B8216DAA0326943AC2 /* Pods-Runner.release.xcconfig */,
F2D2F836E458DFB68D190B87 /* Pods-Runner.profile.xcconfig */,
6B6C4FD9C0BBEBB441A65F3A /* Pods-RunnerTests.debug.xcconfig */,
41C5B4FECBC1AB22501D4D1F /* Pods-RunnerTests.release.xcconfig */,
FE9E9C027F59BE61D9D85175 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
54C6C191361639295A13AFCC /* Frameworks */ = {
isa = PBXGroup;
children = (
6FF805F226A1CA116AFCD23B /* Pods_Runner.framework */,
91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -138,7 +136,7 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
40FDE73F8AC767B1B188CE85 /* Pods */,
182DEA855A45E43DC6B6F1B3 /* Frameworks */,
54C6C191361639295A13AFCC /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -154,7 +152,6 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
B173FFAB2ACEB6A8009C6CB7 /* GoogleService-Info.plist */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -174,7 +171,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
12508058F119E39A22113F76 /* [CP] Check Pods Manifest.lock */,
4EFE922145174E30D57418A9 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
CE669F37B5D4B4901DDD5C29 /* Frameworks */,
@@ -193,15 +190,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
FA61F88D205F0748E04DA12A /* [CP] Check Pods Manifest.lock */,
C6C9A172B337EB12851E53A3 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
E82AFA085809CE8304618B2B /* [CP] Embed Pods Frameworks */,
5C8120DFBB6562B7085522ED /* [firebase_crashlytics] Crashlytics Upload Symbols */,
944C5DFBA06E9AB19BABF039 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -265,7 +261,6 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
B173FFAC2ACEB6A8009C6CB7 /* GoogleService-Info.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
@@ -274,7 +269,23 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
12508058F119E39A22113F76 /* [CP] Check Pods Manifest.lock */ = {
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4EFE922145174E30D57418A9 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -296,61 +307,7 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
5C8120DFBB6562B7085522ED /* [firebase_crashlytics] Crashlytics Upload Symbols */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\"",
"\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/\"",
"\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"",
"\"$(BUILT_PRODUCTS_DIR)/$(EXECUTABLE_PATH)\"",
"\"$(PROJECT_DIR)/firebase_app_id_file.json\"",
);
name = "[firebase_crashlytics] Crashlytics Upload Symbols";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --flutter-project \"$PROJECT_DIR/firebase_app_id_file.json\" ";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
E82AFA085809CE8304618B2B /* [CP] Embed Pods Frameworks */ = {
944C5DFBA06E9AB19BABF039 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -367,7 +324,22 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
FA61F88D205F0748E04DA12A /* [CP] Check Pods Manifest.lock */ = {
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
C6C9A172B337EB12851E53A3 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -480,7 +452,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -499,7 +471,6 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NutPlayerFlutter;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -519,7 +490,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 6385803DFF5615AB5A426D76 /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = 6B6C4FD9C0BBEBB441A65F3A /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -537,7 +508,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AE4F8A509E834A1E6019B4A1 /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 41C5B4FECBC1AB22501D4D1F /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -553,7 +524,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = ED0594A0BE28CA6FB21AC6AE /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = FE9E9C027F59BE61D9D85175 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -614,7 +585,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -663,7 +634,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -684,7 +655,6 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NutPlayerFlutter;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -717,7 +687,6 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NutPlayerFlutter;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyCZseoWZwl06QT-vrPWPKc46FRkFvegLDo</string>
<key>GCM_SENDER_ID</key>
<string>803206890572</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>tech.nut.nutplayer.flutter</string>
<key>PROJECT_ID</key>
<string>nutplayer-flutter</string>
<key>STORAGE_BUCKET</key>
<string>nutplayer-flutter.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:803206890572:ios:32e52f7f99b39c1602e0e9</string>
</dict>
</plist>
+2 -16
View File
@@ -4,8 +4,6 @@
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -26,30 +24,18 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<string>LaunchScreen.storyboard</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
@@ -1,7 +0,0 @@
{
"file_generated_by": "FlutterFire CLI",
"purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory",
"GOOGLE_APP_ID": "1:803206890572:ios:32e52f7f99b39c1602e0e9",
"FIREBASE_PROJECT_ID": "nutplayer-flutter",
"GCM_SENDER_ID": "803206890572"
}
@@ -1,68 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyB7HEhQGvbebWG-B75L9DcSlq_n0EG_zTc',
appId: '1:803206890572:android:ad09971112d775d202e0e9',
messagingSenderId: '803206890572',
projectId: 'nutplayer-flutter',
storageBucket: 'nutplayer-flutter.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyCZseoWZwl06QT-vrPWPKc46FRkFvegLDo',
appId: '1:803206890572:ios:32e52f7f99b39c1602e0e9',
messagingSenderId: '803206890572',
projectId: 'nutplayer-flutter',
storageBucket: 'nutplayer-flutter.appspot.com',
iosBundleId: 'tech.nut.nutplayer.flutter',
);
}
+6 -35
View File
@@ -1,46 +1,17 @@
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:flutter/material.dart';
import 'src/features/main_screen/presentation/home.dart';
import 'package:screen_brightness/screen_brightness.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final double systemBrightness = await ScreenBrightness().system;
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
// Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
runApp(MyApp(brightness: systemBrightness));
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
final double brightness;
const MyApp({required this.brightness, super.key});
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return RepositoryProvider(
create: (context) => SettingsRepository.createDefaults(brightness),
child: const CupertinoApp( //Для теста убрать const
theme: CupertinoThemeData(
brightness: Brightness.light,
primaryColor: CupertinoColors.link
),
home: Home()//Bместо Home() подставить PlayerView()
)
return const MaterialApp(
home: Home(),
);
}
}
@@ -1,6 +1,17 @@
import '../models/option_data.dart';
extension OptionsListExtension on List<OptionData> {
void unselectAll() {
forEach((element) {
element.isSelected = false;
});
}
bool hasSelection() {
var selectedElements = where((element) => element.isSelected);
return selectedElements.isNotEmpty;
}
OptionData? selected() {
return where((element) => element.isSelected).first;
}
@@ -1,47 +1,17 @@
import 'package:flutter/cupertino.dart';
class OptionData {
final Key key;
final String title;
final String? value;
final bool isLive;
final bool isSelected;
late Key key;
late String title;
late bool isSelected;
const OptionData({required this.key, required this.title, this.value, this.isLive = false, this.isSelected = false});
const OptionData.withValueEqualTitle(Key key, String title): this(key: key, title: title, value: title);
}
class BoolOptionData {
final Key key;
final String title;
final bool isSelected;
final Function(bool)? onChange;
BoolOptionData({required this.key, required this.title, required this.isSelected, this.onChange});
}
class OptionDataContainer {
final Key? key;
final String title;
final List<OptionData> options;
final int? selectedIndex;
final Function(OptionData)? onSelectedOption;
OptionDataContainer({
this.key,
required this.title,
required this.options,
this.selectedIndex,
this.onSelectedOption
});
OptionData({required this.key, required this.title, this.isSelected = false});
}
class NumericOptionData {
final Key key;
final String title;
final int value;
final Function(int)? onChange;
late Key key;
late String title;
late int value;
const NumericOptionData({required this.key, required this.title, required this.value, this.onChange});
NumericOptionData({required this.key, required this.title, required this.value});
}
@@ -1,113 +0,0 @@
enum RepositoryFullscreenSettings { landscape, flexible, off }
enum RepositorySkinColor { white }
enum RepositoryVideoQuality { auto, p2160, p1440, p1080, p720, p480, p360, p240, p144 }
enum RepositoryVideoQualityNaming { common, rus, eng, resolution }
enum RepositoryLogType { off, info, debug }
extension RepositoryLogTitle on RepositoryLogType {
String get key {
switch (this) {
case RepositoryLogType.info:
return "info";
case RepositoryLogType.debug:
return "debug";
case RepositoryLogType.off:
return "off";
}
}
}
extension RepositoryVideoQualityIdentifier on RepositoryVideoQuality {
String get identifier {
switch (this) {
case RepositoryVideoQuality.auto:
return "auto";
case RepositoryVideoQuality.p144:
return "p144";
case RepositoryVideoQuality.p240:
return "p240";
case RepositoryVideoQuality.p360:
return "p360";
case RepositoryVideoQuality.p480:
return "p480";
case RepositoryVideoQuality.p720:
return "p720";
case RepositoryVideoQuality.p1080:
return "p1080";
case RepositoryVideoQuality.p1440:
return "p1440";
case RepositoryVideoQuality.p2160:
return "p2160";
}
}
}
class SettingsRepository {
late bool isSkinByDefault;
late RepositoryFullscreenSettings fullscreenSettings;
late bool isPipAvailable;
late bool isSettingsAvailable;
late RepositorySkinColor skinColor;
late bool isAutostart;
late double volume;
late double brightness;
late double speed;
late RepositoryVideoQualityNaming qualityNaming;
late RepositoryVideoQuality quality;
late bool isSubtitlesAvailable;
late int start;
late bool isLoop;
late int playlist;
late int track;
late int chunk;
late RepositoryLogType log;
SettingsRepository({
required this.isSkinByDefault,
required this.fullscreenSettings,
required this.isPipAvailable,
required this.isSettingsAvailable,
required this.skinColor,
required this.isAutostart,
required this.brightness,
required this.volume,
required this.speed,
required this.qualityNaming,
required this.quality,
required this.isSubtitlesAvailable,
required this.start,
required this.isLoop,
required this.playlist,
required this.track,
required this.chunk,
required this.log
});
factory SettingsRepository.createDefaults(double brightness) {
return SettingsRepository(
isSkinByDefault: true,
fullscreenSettings: RepositoryFullscreenSettings.landscape,
isPipAvailable: true,
isSettingsAvailable: true,
skinColor: RepositorySkinColor.white,
isAutostart: false,
brightness: brightness,
volume: 0.5,
speed: 1,
qualityNaming: RepositoryVideoQualityNaming.common,
quality: RepositoryVideoQuality.auto,
isSubtitlesAvailable: true,
start: 0,
isLoop: false,
playlist: 5000,
track: 3000,
chunk: 3000,
log: RepositoryLogType.info
);
}
}
@@ -1,19 +1,19 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
class CheckmarkListOption extends StatelessWidget {
final String title;
final bool isSelected;
final OptionData data;
final Function? onTap;
const CheckmarkListOption(this.title, this.isSelected, this.onTap, {super.key});
const CheckmarkListOption(this.data, this.onTap, {super.key});
@override
Widget build(BuildContext context) {
return CupertinoListTile(
key: key,
key: data.key,
onTap: () { if (onTap != null) { onTap!(); } },
trailing: isSelected ? const Icon(CupertinoIcons.check_mark, color: CupertinoColors.link) : null,
title: Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
trailing: data.isSelected ? const Icon(CupertinoIcons.check_mark, color: CupertinoColors.link) : null,
title: Text(data.title, style: const TextStyle(fontSize: 17))
);
}
}
@@ -1,106 +1,31 @@
import 'package:flutter/cupertino.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
class InputView extends StatefulWidget {
final String title;
final int value;
final Function(int)? newSelection;
class InputView extends StatelessWidget {
const InputView(this.title, this.value, this.newSelection, {super.key});
@override
State<InputView> createState() => _InputViewState();
}
class _InputViewState extends State<InputView> {
final NumericOptionData data;
final TextEditingController _controller = TextEditingController();
final FocusNode _nodeText = FocusNode();
late String _textBefore;
InputView(this.data, {super.key});
@override
Widget build(BuildContext context) {
_controller.text = '${widget.value}';
_textBefore = '${widget.value}';
_controller.text = '${data.value}';
return CupertinoListTile(
key: widget.key,
key: data.key,
trailing: SizedBox(
width: 155,
height: 30,
child: KeyboardActions(
tapOutsideBehavior: TapOutsideBehavior.translucentDismiss,
config: _buildConfig(context),
child: Container(
padding: const EdgeInsets.only(left: 12),
child: CupertinoTextField(
minLines: 1,
maxLines: 1,
maxLength: 10,
autocorrect: false,
focusNode: _nodeText,
decoration: null,
keyboardType: TextInputType.number,
controller: _controller,
onSubmitted: _onSubmit,
onTapOutside: (_) {
_controller.text = _textBefore;
},
),
),
)
width: 155,
child: CupertinoTextField(
minLines: 1,
maxLines: 1,
maxLength: 10,
autocorrect: false,
keyboardType: TextInputType.number,
controller: _controller,
decoration: null,
),
),
title: Text(widget.title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17)));
}
void _onSubmit(String newText) {
_textBefore = newText;
var newTime = int.tryParse(newText);
if (newTime != null) {
widget.newSelection?.call(newTime);
}
}
KeyboardActionsConfig _buildConfig(BuildContext context) {
return KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: CupertinoColors.white,
keyboardSeparatorColor: CupertinoColors.systemGrey4,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _nodeText,
toolbarButtons: [
(node) {
return TextFieldTapRegion(
child: Container(
width: MediaQuery.of(context).size.width - 14,
margin: const EdgeInsets.symmetric(horizontal: 7),
child: Row(
children: [
CupertinoButton(
padding: const EdgeInsets.all(5),
onPressed: () {
_controller.text = _textBefore;
node.unfocus();
},
child: const Text("Отмена", style: TextStyle(fontWeight: FontWeight.w500))
),
const Spacer(),
CupertinoButton(
padding: const EdgeInsets.all(5),
onPressed: () {
_onSubmit(_controller.text);
node.unfocus();
},
child: const Text("Готово", style: TextStyle(fontWeight: FontWeight.w500))
)
],
),
),
);
}
],
),
],
title: Text(data.title, style: const TextStyle(fontSize: 17))
);
}
}
}
@@ -1,70 +1,54 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/common/extensions/options_list_extension.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
class OptionsView extends StatelessWidget {
final String title;
final List<OptionData> options;
final int? selectedIndex;
final Function(int)? newSelection;
const OptionsView(this.title, this.options, this.selectedIndex, this.newSelection, {super.key});
const OptionsView(this.title, this.options, {super.key});
@override
Widget build(BuildContext context) {
return CupertinoListTile(
key: key,
onTap: () { _showAlertDialog(context);},
title: Row(
children: [
Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17)),
const SizedBox(width: 30),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Text(options[selectedIndex ?? 0].title,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 17,
color: CupertinoColors.systemGrey,
fontWeight: FontWeight.w400,
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
),
)
),
const Icon(CupertinoIcons.chevron_forward, color: CupertinoColors.systemGrey2)
]
)
);
key: key,
onTap: () { _showAlertDialog(context);},
trailing: Row(
children: [
if (options.selected() != null)
Text(options.selected()!.title,
style: const TextStyle(
fontSize: 17,
color: CupertinoColors.systemGrey,
fontWeight: FontWeight.w400)),
const Icon(CupertinoIcons.chevron_forward,
color: CupertinoColors.systemGrey2)
],
),
title: Text(title, style: const TextStyle(fontSize: 17)));
}
void _showAlertDialog(BuildContext context) {
showCupertinoModalPopup<void>(
context: context,
barrierColor: CupertinoColors.black.withOpacity(0.5),
builder: (BuildContext context) => CupertinoActionSheet(
title: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: CupertinoColors.darkBackgroundGray)),
actions: <Widget>[...options.indexed.map((valueIndex) {
return Container(
color: CupertinoColors.white.withOpacity(0.8),
child: CupertinoActionSheetAction(
onPressed: () {
newSelection?.call(valueIndex.$1);
Navigator.pop(context);
},
child: Text(valueIndex.$2.title)
),
);
}).toList()],
cancelButton: CupertinoActionSheetAction(
title: Text(title),
actions: <CupertinoActionSheetAction>[...options.map((value) {
return CupertinoActionSheetAction(
child: Text(value.title),
onPressed: () {
Navigator.pop(context);
}
);
}).toList() + [
CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () {
Navigator.pop(context);
},
child: const Text('Отмена'),
),
)]
],
),
);
}
@@ -1,94 +0,0 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_content.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_error.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_subtitles.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/models/firebase_statistics.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/parsing/response.dart';
class FirebaseProvider extends Provider {
final String fileName;
static const root = "PlayableContent/";
FirebaseProvider(this.fileName);
static Future<List<String>> getConfigNames() async {
var storage = FirebaseStorage.instance.ref();
var values = await storage.child(root).listAll();
return values.items.map((e) => e.name).toList();
}
@override
Future<PlayerContent> retrieveContent() async {
var storage = FirebaseStorage.instance.ref();
final file = storage.child("$root$fileName");
try {
var results = await file.getData(5 * 1024 * 1024);
return _handleLoadedData(results);
} catch (error) {
var fbError = FirebaseGetDataError(error);
throw Future.error(fbError);
}
}
FirebaseContent _handleLoadedData(Uint8List? data) {
if (data == null) { throw FirebaseEmptyDataError(); }
var encoded = base64Encode(data);
var decoded = utf8.fuse(base64).decode(encoded);
var json = jsonDecode(decoded);
var response = Response.fromJson(json);
if (response.playbacks.isEmpty) { throw FirebaseNoPlaybacksError(); }
var uri = Uri.tryParse(response.playbacks.first.streamUrl);
if (uri != null) {
final statistics = response.statistics.map((stat) =>
FirebaseStatistics(
stat.name,
stat.urlTemplate,
stat.start,
stat.delay,
stat.count,
stat.method == "get" ? HTTPMethod.get : HTTPMethod.post,
stat.body
)
);
final subtitles = response.subtitles.map((sub) =>
FirebaseSubtitles(
sub.title,
sub.type == "srt" ? SubtitleType.srt : SubtitleType.unknown,
sub.url,
sub.language
)
);
final ContentType? contentType;
final urlPath = uri.toString();
if (urlPath.endsWith('.mp4')) {
contentType = Mp4ContentType(urlPath: urlPath);
} else if (urlPath.endsWith('.m3u8')) {
final isLive = response.playbacks.first.isLive;
contentType = HlsContentType(urlPath: urlPath, isLive: isLive);
} else {
contentType = null;
}
if (contentType != null) {
return FirebaseContent(
contentType,
statistics.toList(),
subtitles.toList()
);
} else {
throw FirebaseUnknownFormatError();
}
} else {
throw FirebaseIncorrectUrlError();
}
}
}
@@ -1,14 +0,0 @@
import 'package:nut_player/nut_player.dart';
class FirebaseContent extends PlayerContent {
@override
ContentType content;
@override
List<PlayerStatisticRecord> statistics;
@override
List<PlayerSubtitleRecord> subtitles;
FirebaseContent(this.content, this.statistics, this.subtitles);
}
@@ -1,9 +0,0 @@
class FirebaseGetDataError extends Error {
final Object innerError;
FirebaseGetDataError(this.innerError);
}
class FirebaseEmptyDataError extends Error {}
class FirebaseNoPlaybacksError extends Error {}
class FirebaseIncorrectUrlError extends Error {}
class FirebaseUnknownFormatError extends Error {}
@@ -1,26 +0,0 @@
import 'package:nut_player/nut_player.dart';
class FirebaseStatistics extends PlayerStatisticRecord {
@override
String name;
@override
String urlTemplate;
@override
double start;
@override
double delay;
@override
int count;
@override
HTTPMethod method;
@override
String? body;
FirebaseStatistics(this.name, this.urlTemplate, this.start, this.delay, this.count, this.method, this.body);
}
@@ -1,17 +0,0 @@
import 'package:nut_player/nut_player.dart';
class FirebaseSubtitles extends PlayerSubtitleRecord {
@override
String title;
@override
SubtitleType type;
@override
String url;
@override
String language;
FirebaseSubtitles(this.title, this.type, this.url, this.language);
}
@@ -1,19 +0,0 @@
class PlaybackRecord {
final String streamType;
final String streamUrl;
final bool isLive;
final bool isVideo;
final bool isAudio;
PlaybackRecord({required this.streamType, required this.streamUrl, required this.isLive, required this.isVideo, required this.isAudio});
factory PlaybackRecord.fromJson(dynamic data) {
return PlaybackRecord(
streamType: data['stream_type'],
streamUrl: data['stream_url'],
isLive: data['is_live'],
isVideo: data['is_video'],
isAudio: data['is_audio']
);
}
}
@@ -1,23 +0,0 @@
import 'statistic_record.dart';
import 'subtitle_record.dart';
import 'playback_record.dart';
class Response {
final List<PlaybackRecord> playbacks;
final List<StatisticRecord> statistics;
final List<PlayerSubtitleRecord> subtitles;
Response({required this.playbacks, required this.statistics, required this.subtitles});
factory Response.fromJson(dynamic data) {
var playbacks = data['playback'].map((item) => PlaybackRecord.fromJson(item)).toList();
var statistics = data['stat'].map((item) => StatisticRecord.fromJson(item)).toList();
var subtitles = data['subtitle'].map((item) => PlayerSubtitleRecord.fromJson(item)).toList();
return Response(
playbacks: List<PlaybackRecord>.from(playbacks),
statistics: List<StatisticRecord>.from(statistics),
subtitles: List<PlayerSubtitleRecord>.from(subtitles)
);
}
}
@@ -1,23 +0,0 @@
class StatisticRecord {
final String name;
final String urlTemplate;
final double start;
final double delay;
final int count;
final String method;
final String? body;
StatisticRecord({required this.name, required this.urlTemplate, required this.start, required this.delay, required this.count, required this.method, this.body});
factory StatisticRecord.fromJson(dynamic data) {
return StatisticRecord(
name: data['name'],
urlTemplate: data['url_template'],
start: data['start'] == null ? 0.0 : data['start'].toDouble(),
delay: data['delay'] == null ? 0.0 : data['delay'].toDouble(),
count: data['count'],
method: data['method'],
body: data['body'],
);
}
}
@@ -1,17 +0,0 @@
class PlayerSubtitleRecord {
final String title;
final String type;
final String url;
final String language;
PlayerSubtitleRecord({required this.title, required this.type, required this.url, required this.language});
factory PlayerSubtitleRecord.fromJson(dynamic data) {
return PlayerSubtitleRecord(
title: data['title'],
type: data['type'],
url: data['url'],
language: data['language']
);
}
}
@@ -1,74 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/firebase_provider.dart';
part 'json_event.dart';
part 'json_state.dart';
class JsonBloc extends Bloc<JsonEvent, JsonState> {
JsonBloc() : super(JsonState.create()) {
on<ChooseWayToGetConfigEvent>(_onChooseWayToGetConfigEvent);
on<ChooseWayToGetLinkEvent>(_onChooseWayToGetLinkEvent);
on<ChooseFirebaseConfigEvent>(_onChooseFirebaseConfigEvent);
on<ChooseExampleLinkEvent>(_onChooseExampleLinkEvent);
on<TypeOwnLinkEvent>(_onTypeOwnLinkEvent);
on<JsonResetEvent>(_onResetEvent);
}
_onResetEvent(JsonResetEvent event, Emitter<JsonState> emit) {
emit(JsonState.create());
}
_onChooseWayToGetConfigEvent(ChooseWayToGetConfigEvent event, Emitter<JsonState> emit) async {
JsonState? parentState = state;
while (parentState?.parent != null) { parentState = parentState?.parent; }
if (event.selectedIndex == 0) {
emit(ConfigLinkChosenOptionState.create(parentState));
} else {
emit(FirebaseChosenOptionState.create(parentState, const [], null));
var fileNames = await FirebaseProvider.getConfigNames();
emit(FirebaseChosenOptionState.create(parentState, _createFBOptions(fileNames), null));
}
}
List<OptionData> _createFBOptions(List<String> names) {
return names.indexed.map((e) =>
OptionData.withValueEqualTitle(
Key("ListElementConfig${e.$1}ID"),
e.$2)
).toList();
}
_onChooseWayToGetLinkEvent(ChooseWayToGetLinkEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseLinkExampleState.create(parent: parentState, selectedIndex: null)
: UseJsonOwnLinkState.create(parent: parentState, urlPath: null, isLive: null));
}
_onChooseFirebaseConfigEvent(ChooseFirebaseConfigEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(FirebaseChosenOptionState.create(parentState, state.options, event.selectedIndex));
}
_onChooseExampleLinkEvent(ChooseExampleLinkEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseLinkExampleState.create(parent: parentState, selectedIndex: event.selectedIndex));
}
_onTypeOwnLinkEvent(TypeOwnLinkEvent event, Emitter<JsonState> emit) {
JsonState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseJsonOwnLinkState.create(parent: parentState, urlPath: event.urlPath, isLive: event.isLive));
}
}
@@ -1,32 +0,0 @@
part of 'json_bloc.dart';
@immutable
sealed class JsonEvent {}
class JsonResetEvent extends JsonEvent {}
class ChooseWayToGetConfigEvent extends JsonEvent {
final int selectedIndex;
ChooseWayToGetConfigEvent(this.selectedIndex);
}
class ChooseWayToGetLinkEvent extends JsonEvent {
final int selectedIndex;
ChooseWayToGetLinkEvent(this.selectedIndex);
}
class ChooseFirebaseConfigEvent extends JsonEvent {
final int selectedIndex;
ChooseFirebaseConfigEvent(this.selectedIndex);
}
class ChooseExampleLinkEvent extends JsonEvent {
final int selectedIndex;
ChooseExampleLinkEvent(this.selectedIndex);
}
class TypeOwnLinkEvent extends JsonEvent {
final String urlPath;
final bool? isLive;
TypeOwnLinkEvent(this.urlPath, this.isLive);
}
@@ -1,234 +0,0 @@
part of 'json_bloc.dart';
typedef JsonStateIndexedEventCreator = JsonEvent Function(int index);
typedef JsonStateStringEventCreator = JsonEvent Function(String value);
enum UIType { list, input }
/// Состояние по умолчанию
@immutable
class JsonState {
final String title;
final Key key;
final List<OptionData> options;
final String? urlPath;
final bool? isLive;
final int? initialIndex;
final int? currentIndex;
final JsonState? parent;
final bool isFinal;
final UIType uiType;
final JsonStateIndexedEventCreator? createIndexedEvent;
final JsonStateStringEventCreator? createStringEvent;
const JsonState({
required this.title,
required this.key,
this.options = const [],
this.urlPath,
this.isLive,
this.initialIndex,
this.currentIndex,
this.parent,
this.isFinal = false,
required this.uiType,
this.createIndexedEvent,
this.createStringEvent
});
factory JsonState.create() {
return JsonState(
title: '1. Выберите способ получения конфига',
key: const Key('ChooseWayToGetConfigListID'),
options: const [
OptionData(key: Key('ListElementConfigLinkID'), title: 'Ссылка на конфиг'),
OptionData(key: Key('ListElementFirebaseID'), title: 'Firebase')
],
uiType: UIType.list,
createIndexedEvent: (index) => ChooseWayToGetConfigEvent(index),
);
}
}
/// Состояние, когда выбран пункт "Ссылка на конфиг"
class ConfigLinkChosenOptionState extends JsonState {
const ConfigLinkChosenOptionState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
JsonState? parent,
UIType uiType,
JsonStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
parent: parent,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory ConfigLinkChosenOptionState.create(JsonState? parent) {
return ConfigLinkChosenOptionState(
'2. Выберите способ получения ссылки',
const Key('ChooseWayToGetUrlListID'),
const [
OptionData(key: Key('ListElementUseUrlExampleID'), title: 'Использовать пример ссылки'),
OptionData(key: Key('ListElementUseOwnUrlID'), title: 'Указать свою')
],
0,
parent,
UIType.list,
(index) => ChooseWayToGetLinkEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Firebase"
class FirebaseChosenOptionState extends JsonState {
const FirebaseChosenOptionState(
String title,
Key key,
List<OptionData> options,
String? urlPath,
bool? isLive,
int? initialIndex,
int? currentIndex,
JsonState? parent,
bool isFinal,
UIType uiType,
JsonStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
urlPath: urlPath,
isLive: isLive,
options: options,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory FirebaseChosenOptionState.create(JsonState? parent, List<OptionData> options, int? selectedIndex) {
final String? currentUrlPath;
if (selectedIndex != null) {
currentUrlPath = options[selectedIndex].value;
} else {
currentUrlPath = null;
}
return FirebaseChosenOptionState(
'2. Выберите файл конфига',
const Key('ChooseFirebasebConfigListID'),
options,
currentUrlPath,
false,
1,
selectedIndex,
parent,
selectedIndex != null,
UIType.list,
(index) => ChooseFirebaseConfigEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Использовать пример ссылки"
class UseLinkExampleState extends JsonState {
const UseLinkExampleState(
String title,
Key key,
String? urlPath,
List<OptionData> options,
int? initialIndex,
int? currentIndex,
JsonState? parent,
bool isFinal,
UIType uiType,
JsonStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
urlPath: urlPath,
options: options,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory UseLinkExampleState.create({JsonState? parent, int? selectedIndex}) {
var options = const [
OptionData(key: Key('ListElementUrl1ID'), title: 'Статистика (GET)', value: 'http://chest-101.gc.nut.team:8000/play/opt/5edd1215b2e34a3eb70654a117ea2935'),
OptionData(key: Key('ListElementUrl2ID'), title: 'Статистика (POST)', value: 'http://chest-101.gc.nut.team:8000/play/opt/5edd1215b2e34a3eb70654a117ea2937'),
OptionData(key: Key('ListElementUrl3ID'), title: 'Субтитры (SRT)', value: 'http://chest-101.gc.nut.team:8000/play/opt/e1dc888381e04b4290924dd9ec7b33ed'),
];
final String? currentUrlPath;
if (selectedIndex != null) {
currentUrlPath = options[selectedIndex].value;
} else {
currentUrlPath = null;
}
return UseLinkExampleState(
'3. Выберите пример ссылки',
const Key('ChooseUrlExampleListID'),
currentUrlPath,
options,
0,
selectedIndex,
parent,
selectedIndex != null,
UIType.list,
(index) => ChooseExampleLinkEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Указать свою"
class UseJsonOwnLinkState extends JsonState {
const UseJsonOwnLinkState(
String title,
Key key,
String? urlPath,
bool? isLive,
int? initialIndex,
JsonState? parent,
bool isFinal,
UIType uiType,
JsonStateStringEventCreator? createStringEvent
): super(
title: title,
key: key,
urlPath: urlPath,
isLive: isLive,
initialIndex: initialIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createStringEvent: createStringEvent
);
factory UseJsonOwnLinkState.create({JsonState? parent, String? urlPath, bool? isLive}) {
var finalUrlPath = urlPath ?? '';
return UseJsonOwnLinkState(
'3. Укажите ссылку',
const Key('TypeOwnUrlViewID'),
finalUrlPath,
isLive,
1,
parent,
finalUrlPath.isNotEmpty,
UIType.input,
(value) => TypeOwnLinkEvent(value, isLive)
);
}
}
@@ -1,59 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/features/main_screen/domain/firebase/firebase_provider.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
part 'main_event.dart';
part 'main_state.dart';
enum InputType { url, json }
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(const ChosenJsonState()) {
on<ChooseJsonEvent>(_onChooseJson);
on<ChooseUrlEvent>(_onChooseUrl);
on<OpenSettingsEvent>(_onOpenSettings);
on<OpenPlayerEvent>(_onOpenPlayer);
}
_onChooseJson(ChooseJsonEvent event, Emitter<MainState> emit) {
emit(const ChosenJsonState());
}
_onChooseUrl(ChooseUrlEvent event, Emitter<MainState> emit) {
emit(const ChosenUrlState());
}
_onOpenSettings(OpenSettingsEvent event, Emitter<MainState> emit) {
emit(SettingState(state.inputType));
}
_onOpenPlayer(OpenPlayerEvent event, Emitter<MainState> emit) {
Provider? provider;
final jsonUrlPath = event.jsonState.urlPath;
final urlUrlPath = event.urlState.urlPath;
if (event.jsonState.isFinal && jsonUrlPath != null) {
if (event.jsonState is FirebaseChosenOptionState) {
provider = FirebaseProvider(jsonUrlPath);
} else if (event.jsonState is UseJsonOwnLinkState || event.jsonState is UseLinkExampleState) {
provider = JsonProvider(jsonUrlPath);
}
// TODO: Поддежка Json-конфигов
} else if (event.urlState.isFinal && urlUrlPath != null) {
provider = CommonProvider.url(
urlUrlPath,
isLive: event.urlState.isLive ?? false,
isLoop: event.isLoop
);
}
if (provider != null) {
emit(PlayerState(state.inputType, provider));
}
}
}
@@ -1,15 +0,0 @@
part of 'main_bloc.dart';
@immutable
sealed class MainEvent {}
class ChooseJsonEvent extends MainEvent {}
class ChooseUrlEvent extends MainEvent {}
class OpenSettingsEvent extends MainEvent {}
class OpenPlayerEvent extends MainEvent {
final JsonState jsonState;
final UrlState urlState;
final bool isLoop;
OpenPlayerEvent(this.jsonState, this.urlState, this.isLoop);
}
@@ -1,24 +0,0 @@
part of 'main_bloc.dart';
@immutable
sealed class MainState {
final InputType inputType;
const MainState(this.inputType);
}
class ChosenJsonState extends MainState {
const ChosenJsonState() : super(InputType.json);
}
class ChosenUrlState extends MainState {
const ChosenUrlState() : super(InputType.url);
}
class SettingState extends MainState {
const SettingState(super.inputType);
}
class PlayerState extends MainState {
final Provider provider;
const PlayerState(super.inputType, this.provider);
}
@@ -1,86 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
part 'url_event.dart';
part 'url_state.dart';
class UrlBloc extends Bloc<UrlEvent, UrlState> {
UrlBloc() : super(UrlState.create()) {
on<ChooseWayToLoadVideoEvent>(_onChooseWayToGetConfigEvent);
on<ChooseExampleVideoEvent>(_onChooseExampleLinkEvent);
on<ChooseWayToGetLinkEvent>(_onChooseWayToGetLinkEvent);
on<TypeOwnLinkEvent>(_onTypeOwnLinkEvent);
on<ChooseExtraOptionsEvent>(_onChooseExtraOptionsEvent);
on<ChooseConfigFileEvent>(_onChooseConfigFileEvent);
on<ChooseVideoFormatEvent>(_onChoseVideoFormatEvent);
on<TypeOwnLinkWithoutFormatEvent>(_onTypeOwnLinkWithoutFormatEvent);
on<UrlResetEvent>(_onResetEvent);
}
_onResetEvent(UrlResetEvent event, Emitter<UrlState> emit) {
emit(UrlState.create());
}
_onChooseWayToGetConfigEvent(ChooseWayToLoadVideoEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseExampleChosenOptionState.create(parentState, null)
: LoadWithLinkChosenOptionState.create(parentState));
}
_onChooseExampleLinkEvent(ChooseExampleVideoEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseExampleChosenOptionState.create(parentState, event.selectedIndex));
}
_onChooseWayToGetLinkEvent(ChooseWayToGetLinkEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseOwnLinkState.create(parentState, null)
: ChooseExtraOptionsState.create(parentState));
}
_onTypeOwnLinkEvent(TypeOwnLinkEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseOwnLinkState.create(parentState, event.urlPath));
}
_onChooseExtraOptionsEvent(ChooseExtraOptionsEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(event.selectedIndex == 0
? UseOwnLinkWithoutFormat.create(parentState, null, null)
: ChooseJsonFileState.create(parentState, null));
}
_onChooseConfigFileEvent(ChooseConfigFileEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(ChooseJsonFileState.create(parentState, event.selectedIndex));
}
_onChoseVideoFormatEvent(ChooseVideoFormatEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseOwnLinkWithoutFormat.create(parentState, state.urlPath, event.selectedIndex));
}
_onTypeOwnLinkWithoutFormatEvent(TypeOwnLinkWithoutFormatEvent event, Emitter<UrlState> emit) {
UrlState? parentState = state;
while (parentState?.parent?.parent?.parent?.parent != null) { parentState = parentState?.parent; }
emit(UseOwnLinkWithoutFormat.create(parentState, event.urlPath, state.currentIndex));
}
}
@@ -1,52 +0,0 @@
part of 'url_bloc.dart';
@immutable
sealed class UrlEvent {}
class UrlResetEvent extends UrlEvent {}
class ChooseWayToLoadVideoEvent extends UrlEvent {
final int selectedIndex;
ChooseWayToLoadVideoEvent(this.selectedIndex);
}
class ChooseExampleVideoEvent extends UrlEvent {
final int selectedIndex;
ChooseExampleVideoEvent(this.selectedIndex);
}
class ChooseWayToGetLinkEvent extends UrlEvent {
final int selectedIndex;
ChooseWayToGetLinkEvent(this.selectedIndex);
}
class TypeOwnLinkEvent extends UrlEvent {
final String urlPath;
TypeOwnLinkEvent(this.urlPath);
}
class ChooseExtraOptionsEvent extends UrlEvent {
final int selectedIndex;
ChooseExtraOptionsEvent(this.selectedIndex);
}
class ChooseVideoFormatEvent extends UrlEvent {
final int selectedIndex;
ChooseVideoFormatEvent(this.selectedIndex);
}
class TypeOwnLinkWithoutFormatEvent extends UrlEvent {
final String urlPath;
TypeOwnLinkWithoutFormatEvent(this.urlPath);
}
class ChooseConfigFileEvent extends UrlEvent {
final int selectedIndex;
ChooseConfigFileEvent(this.selectedIndex);
}
class UpdateOwnLinkWithoutFormatEvent extends UrlEvent {
final String? urlPath;
final int? index;
UpdateOwnLinkWithoutFormatEvent(this.urlPath, this.index);
}
@@ -1,345 +0,0 @@
part of 'url_bloc.dart';
typedef UrlStateIndexedEventCreator = UrlEvent Function(int index);
typedef UrlStateStringEventCreator = UrlEvent Function(String value);
enum UIType { list, input, listWithInput }
/// Состояние по умолчанию
@immutable
class UrlState {
final String title;
final Key key;
final List<OptionData> options;
final String? visibleUrlPath;
final String? urlPath;
final bool? isLive;
final int? initialIndex;
final int? currentIndex;
final UrlState? parent;
final bool isFinal;
final UIType uiType;
final UrlStateIndexedEventCreator? createIndexedEvent;
final UrlStateStringEventCreator? createStringEvent;
const UrlState({
required this.title,
required this.key,
this.options = const [],
this.visibleUrlPath,
this.urlPath,
this.isLive,
this.initialIndex,
this.currentIndex,
this.parent,
this.isFinal = false,
required this.uiType,
this.createIndexedEvent,
this.createStringEvent
});
factory UrlState.create() {
return UrlState(
title: '1. Выберите вариант загрузки видео',
key: const Key('ChooseWayToLoadVideoListID'),
options: const [
OptionData(key: Key('ListElementExampleID'), title: 'Использовать пример'),
OptionData(key: Key('ListElementUrlID'), title: 'Загрузить по ссылке')
],
uiType: UIType.list,
createIndexedEvent: (index) => ChooseWayToLoadVideoEvent(index),
);
}
}
/// Состояние, когда выбран пункт "Использовать пример"
class UseExampleChosenOptionState extends UrlState {
const UseExampleChosenOptionState(
String title,
Key key,
List<OptionData> options,
String? urlPath,
bool? isLive,
int? initialIndex,
int? currentIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
urlPath: urlPath,
isLive: isLive,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory UseExampleChosenOptionState.create(UrlState? parent, int? selectedIndex) {
var options = const [
OptionData(key: Key('ListElementUrl1ID'), title: 'HLS VOD', value: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'),
OptionData(key: Key('ListElementUrl2ID'), title: 'HLS LIVE', value: 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', isLive: true),
OptionData(key: Key('ListElementUrl3ID'), title: 'MP4 (4K)', value: 'https://filesamples.com/samples/video/mp4/sample_3840x2160.mp4'),
OptionData(key: Key('ListElementUrl4ID'), title: 'MP4 (1440p)', value: 'https://filesamples.com/samples/video/mp4/sample_2560x1440.mp4'),
OptionData(key: Key('ListElementUrl5ID'), title: 'MP4 (1080p)', value: 'https://filesamples.com/samples/video/mp4/sample_1920x1080.mp4'),
OptionData(key: Key('ListElementUrl6ID'), title: 'MP4 (720р)', value: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'),
OptionData(key: Key('ListElementUrl7ID'), title: 'MP4 (360p)', value: 'https://filesamples.com/samples/video/mp4/sample_640x360.mp4'),
OptionData(key: Key('ListElementUrl8ID'), title: 'MP4 (144р)', value: 'https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4'),
OptionData(key: Key('ListElementUrl9ID'), title: 'MP4 (более 10 часов)', value: 'https://cloud.nut.tech/index.php/s/TgFkebRSs3A9i3r/download/Гендальф_10_часовая_версия_Gandalf_10_hour_version.mp4')
];
final String? currentUrlPath;
final bool? isLive;
if (selectedIndex != null) {
currentUrlPath = options[selectedIndex].value;
isLive = options[selectedIndex].isLive;
} else {
currentUrlPath = null;
isLive = null;
}
return UseExampleChosenOptionState(
'2. Выберите пример видео',
const Key('ChooseWayToGetUrlListID'),
options,
currentUrlPath,
isLive,
0,
selectedIndex,
parent,
(selectedIndex != null),
UIType.list,
(index) => ChooseExampleVideoEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Загрузить по ссылке"
class LoadWithLinkChosenOptionState extends UrlState {
const LoadWithLinkChosenOptionState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
UrlState? parent,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
parent: parent,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory LoadWithLinkChosenOptionState.create(UrlState? parent) {
return LoadWithLinkChosenOptionState(
'2. Выберите способ получения ссылки',
const Key('ChooseWayToGetLinkListID'),
const [
OptionData(key: Key('ListElementUseUrlID'), title: 'Указать ссылку'),
OptionData(key: Key('ListElementExtraOptionsID'), title: 'Использовать доп. опции')
],
1,
parent,
UIType.list,
(index) => ChooseWayToGetLinkEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Указать ссылку"
class UseOwnLinkState extends UrlState {
const UseOwnLinkState(
String title,
Key key,
String? urlPath,
bool? isLive,
int? initialIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateStringEventCreator? createStringEvent
): super(
title: title,
key: key,
urlPath: urlPath,
isLive: isLive,
initialIndex: initialIndex,
parent: parent,
isFinal: isFinal,
visibleUrlPath: urlPath,
uiType: uiType,
createStringEvent: createStringEvent
);
factory UseOwnLinkState.create(UrlState? parent, String? urlPath, {bool isLive = false}) {
var finalUrlPath = urlPath ?? 'https://cloud.nut.tech/index.php/s/iY3KtaL7bekWnwM/download/IMG_3059.MP4';
return UseOwnLinkState(
'3. Вставьте ссылку',
const Key('TypeOwnUrlViewID'),
finalUrlPath,
isLive,
0,
parent,
finalUrlPath.isNotEmpty,
UIType.input,
(value) => TypeOwnLinkEvent(value)
);
}
}
/// Состояние, когда выбран пункт "Использовать доп. опции"
class ChooseExtraOptionsState extends UrlState {
const ChooseExtraOptionsState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
UrlState? parent,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
parent: parent,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory ChooseExtraOptionsState.create(UrlState? parent) {
return ChooseExtraOptionsState(
'3. Выберите дополнительные опции',
const Key('ChooseExtraOptionsListID'),
const [
OptionData(key: Key('ListElementNoFormatOptionID'), title: 'Ссылка без расширения'),
OptionData(key: Key('ListElementJSONFileID'), title: 'JSON-файл для MP4')
],
1,
parent,
UIType.list,
(index) => ChooseExtraOptionsEvent(index)
);
}
}
/// Состояние, когда выбран пункт "Ссылка без расширения"
class UseOwnLinkWithoutFormat extends UrlState {
const UseOwnLinkWithoutFormat(
String title,
Key key,
List<OptionData> options,
String? urlPath,
String? visibleUrlPath,
int? initialIndex,
int? currentIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent,
UrlStateStringEventCreator? createStringEvent
): super(
title: title,
key: key,
options: options,
urlPath: urlPath,
visibleUrlPath: visibleUrlPath,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent,
createStringEvent: createStringEvent
);
factory UseOwnLinkWithoutFormat.create(UrlState? parent, String? urlPath, int? selectedIndex) {
var options = const [
OptionData(key: Key('ListElementFormat1ID'), title: 'MP4', value: '.mp4'),
OptionData(key: Key('ListElementFormat3ID'), title: 'M3U8', value: '.m3u8')
];
var visibleUrlPath = _removeFormatIfNeeded(urlPath) ?? 'https://cloud.nut.tech/index.php/s/iY3KtaL7bekWnwM/download/IMG_3059';
var currentSelectedIndex = selectedIndex ?? 0;
var finalUrlPath = "$visibleUrlPath${options[currentSelectedIndex].value}";
return UseOwnLinkWithoutFormat(
'4. Вставьте ссылку и выберите формат видео',
const Key('UseOwnLinkWithoutFormatListID'),
options,
finalUrlPath,
visibleUrlPath,
0,
currentSelectedIndex,
parent,
finalUrlPath.isNotEmpty,
UIType.listWithInput,
(index) => ChooseVideoFormatEvent(index),
(value) => TypeOwnLinkWithoutFormatEvent(value)
);
}
static String? _removeFormatIfNeeded(String? urlPath) {
if (urlPath != null && urlPath.endsWith('.mp4')) {
return urlPath.replaceAll(RegExp('.mp4'), '');
} else if (urlPath != null && urlPath.endsWith('.m3u8')) {
return urlPath.replaceAll(RegExp('.m3u8'), '');
} else {
return urlPath;
}
}
}
/// Состояние, когда выбран пункт "JSON-файл для MP4"
class ChooseJsonFileState extends UrlState {
const ChooseJsonFileState(
String title,
Key key,
List<OptionData> options,
int? initialIndex,
int? currentIndex,
UrlState? parent,
bool isFinal,
UIType uiType,
UrlStateIndexedEventCreator? createIndexedEvent
): super(
title: title,
key: key,
options: options,
initialIndex: initialIndex,
currentIndex: currentIndex,
parent: parent,
isFinal: isFinal,
uiType: uiType,
createIndexedEvent: createIndexedEvent
);
factory ChooseJsonFileState.create(UrlState? parent, int? selectedIndex) {
return ChooseJsonFileState(
'4. Выберите файл конфигурации',
const Key('ChooseConfigFileListID'),
const [
OptionData(key: Key('ListElementConfig1ID'), title: 'Config 1'),
OptionData(key: Key('ListElementConfig2ID'), title: 'Config 2'),
OptionData(key: Key('ListElementConfig3ID'), title: 'Config 3'),
OptionData(key: Key('ListElementConfig4ID'), title: 'Config 4')
],
1,
selectedIndex,
parent,
// TODO: Пока не реализовано
false,//selectedIndex != null,
UIType.list,
(index) => ChooseConfigFileEvent(index)
);
}
}
@@ -1,13 +1,11 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
import '../../../common/repository/settings_repository.dart';
import '../domain/main_bloc/main_bloc.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_view.dart';
import 'package:nut_player_example/src/features/settings_screen/presentation/settings_view.dart';
class Buttons extends StatelessWidget {
final bool _isReady;
const Buttons({super.key});
const Buttons(this._isReady, {super.key});
@override
Widget build(BuildContext context) {
@@ -18,45 +16,43 @@ class Buttons extends StatelessWidget {
// КНОПКА ПОСМОТРЕТЬ ДЕМО
Container(
width: double.infinity,
margin: const EdgeInsets.only(top: 25, left: 16, right: 16, bottom: 5),
child: Builder(
builder: (context) {
var urlState = context.watch<UrlBloc>().state;
var jsonState = context.watch<JsonBloc>().state;
return CupertinoButton(
key: const Key('WatchDemoButtonID'),
color: (urlState.isFinal || jsonState.isFinal) ? CupertinoColors.link : CupertinoColors.systemGrey2,
borderRadius: BorderRadius.circular(12),
onPressed: () {
context.read<MainBloc>().add(OpenPlayerEvent(jsonState, urlState, context.read<SettingsRepository>().isLoop));
},
child: const Text(
'Посмотреть демо',
textAlign: TextAlign.center,
style: TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.white,
fontSize: 17,
fontWeight: FontWeight.w600),
));
}
)),
margin: const EdgeInsets.only(
top: 25, left: 16, right: 16, bottom: 5),
child: CupertinoButton(
key: const Key('WatchDemoButtonID'),
color: _isReady ? CupertinoColors.link : CupertinoColors.systemGrey2,
borderRadius: BorderRadius.circular(12),
onPressed: () {
Navigator.push(context,
CupertinoPageRoute(builder: (BuildContext context) { return PlayerView();})
);
},
child: const Text(
'Посмотреть демо',
textAlign: TextAlign.center,
style: TextStyle(
color: CupertinoColors.white,
fontSize: 17,
fontWeight: FontWeight.w600),
))),
//КНОПКА НАСТРОЙКИ ПЛЕЕРА
Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
margin: const EdgeInsets.symmetric(
vertical: 5, horizontal: 16),
child: CupertinoButton(
key: const Key('PlayerSettingButtonID'),
onPressed: () {
context.read<MainBloc>().add(OpenSettingsEvent());
Navigator.push(context,
CupertinoPageRoute(builder: (BuildContext context) { return const SettingsView();})
);
},
child: const Text(
'Настройки плеера',
textAlign: TextAlign.center,
style: TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.link,
fontSize: 17,
fontWeight: FontWeight.w600),
@@ -1,26 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/main_screen/domain/main_bloc/main_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/buttons.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/json_view.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/url_view.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_view.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
import '../../player_screen/domain/bloc/playerview_bloc.dart';
import '../../settings_screen/presentation/settings_view.dart';
class Home extends StatelessWidget {
enum InputType { url, json }
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
InputType _inputType = InputType.json;
bool _isReady = false;
@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp
]);
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
navigationBar: CupertinoNavigationBar(
@@ -29,130 +26,78 @@ class Home extends StatelessWidget {
border: null,
backgroundColor: CupertinoColors.white,
),
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => MainBloc()),
BlocProvider(create: (_) => JsonBloc()),
BlocProvider(create: (_) => UrlBloc())
],
child: BlocListener<MainBloc, MainState>(
listener: (context, state) { _navigate(state, context); },
child: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: BlocBuilder<MainBloc, MainState>(
builder: (context, state) {
return Column(
children: [
Container(
key: const Key('MainSegmentedButtonID'),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
color: CupertinoColors.white,
child: _buildSegmentedButtonWidget(context, state.inputType, (newInputType) {
if (newInputType != null) {
context.read<MainBloc>().add(newInputType == InputType.json
? ChooseJsonEvent()
: ChooseUrlEvent());
newInputType == InputType.url
? context.read<JsonBloc>().add(JsonResetEvent())
: context.read<UrlBloc>().add(UrlResetEvent());
}
})
),
if (state.inputType == InputType.url)
const URLView()
else if (state.inputType == InputType.json)
const JSONView()
else
const Text('unknown')
]
);
},
)
),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Builder(
builder: (context) {
return const Buttons();
child: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Column(
children: [
Container(
key: const Key('MainSegmentedButtonID'),
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 16),
color: CupertinoColors.white,
child: CupertinoSlidingSegmentedControl<InputType>(
groupValue: _inputType,
onValueChanged: (InputType? value) {
if (value != null) {
setState(() {
_inputType = value;
_isReady = false;
});
}
),
)
),
],
},
children: const <InputType, Widget>{
InputType.json: Padding(
key: Key('JSONSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('JSON-конфиг',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
),
InputType.url: Padding(
key: Key('URLSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Видео URL',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
)
},
),
),
if (_inputType == InputType.json)
JSONView((isReady) {
setState(() {
_isReady = isReady;
});
})
else if (_inputType == InputType.url)
URLView((isReady) {
setState(() {
_isReady = isReady;
});
})
]
)
),
),
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Buttons(_isReady)
)
),
],
),
),
);
}
_navigate(MainState state, BuildContext context) {
final repository = context.read<SettingsRepository>();
if (state is PlayerState) {
Navigator.push(context, CupertinoPageRoute(builder: (_) {
return BlocProvider(
create: (BuildContext context) => PlayerViewBloc(provider: state.provider, repository: repository),
child: BlocListener<PlayerViewBloc, PlayerViewState>(
listener: (context, state) {
if (state is PlayerViewDismiss) {
Navigator.of(context).pop();
}
},
child: const PlayerView(),
),
);
}));
} else if (state is SettingState) {
Navigator.push(context, CupertinoPageRoute(builder: (_) {
return BlocProvider(
create: (context) => SettingsBloc(repository),
child: BlocListener<SettingsBloc, SettingsState>(
listener: (context, state) {
if (state is SettingsDismiss) {
Navigator.of(context).pop();
}
},
child: const SettingsView(),
)
);
}));
}
}
_buildSegmentedButtonWidget(BuildContext context, InputType currentInputType, Function(InputType?) onValueChanged) {
return CupertinoSlidingSegmentedControl<InputType>(
groupValue: currentInputType,
onValueChanged: onValueChanged,
children: const <InputType, Widget>{
InputType.json: Padding(
key: Key('JSONSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('JSON-конфиг',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
),
InputType.url: Padding(
key: Key('URLSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Видео URL',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)),
)
},
);
}
}
}
@@ -1,81 +1,117 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import '../../../common/models/option_data.dart';
import '../../../common/extensions/options_list_extension.dart';
import 'options_list.dart';
import 'url_input.dart';
class JSONView extends StatelessWidget {
const JSONView({super.key});
class JSONView extends StatefulWidget {
final Function(bool)? isReady;
const JSONView(this.isReady, {super.key});
@override
State<JSONView> createState() => _JSONViewState();
}
class _JSONViewState extends State<JSONView> {
final List<OptionData> _waysToGetConfig = [
OptionData(key: const Key('ListElementConfigLinkID'), title: 'Ссылка на конфиг'),
OptionData(key: const Key('ListElementFirebaseID'), title: 'Firebase'),
];
final List<OptionData> _waysToGetUrlpath = [
OptionData(key: const Key('ListElementUseUrlExampleID'), title: 'Использовать пример ссылки'),
OptionData(key: const Key('ListElementUseOwnUrlID'), title: 'Указать свою')
];
final List<OptionData> _firebaseConfigs = [
OptionData(key: const Key('ListElementConfig1ID'), title: 'Config 1'),
OptionData(key: const Key('ListElementConfig2ID'), title: 'Config 2'),
OptionData(key: const Key('ListElementConfig3ID'), title: 'Config 3'),
OptionData(key: const Key('ListElementConfig4ID'), title: 'Config 4')
];
final List<OptionData> _linksExamples = [
OptionData(key: const Key('ListElementUrl1ID'), title: 'Субтитры_STR'),
OptionData(key: const Key('ListElementUrl2ID'), title: 'Example_2'),
OptionData(key: const Key('ListElementUrl3ID'), title: 'Example_3'),
OptionData(key: const Key('ListElementUrl4ID'), title: 'Example_4'),
OptionData(key: const Key('ListElementUrl5ID'), title: 'Example_5'),
OptionData(key: const Key('ListElementUrl6ID'), title: 'Example_6'),
OptionData(key: const Key('ListElementUrl7ID'), title: 'Example_7'),
OptionData(key: const Key('ListElementUrl8ID'), title: 'Example_8'),
OptionData(key: const Key('ListElementUrl9ID'), title: 'Example_9'),
OptionData(key: const Key('ListElementUrl10ID'), title: 'Example_10')
];
String _currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
@override
Widget build(BuildContext context) {
return BlocBuilder<JsonBloc, JsonState>(
builder: (context, state) {
return Column(
children: _buildAllWidgets(context, state, null)
);
},
);
}
List<Widget> _buildAllWidgets(BuildContext context, JsonState state, int? index) {
List<Widget> widgets = [];
final parent = state.parent;
if (parent != null) {
widgets.addAll(_buildAllWidgets(context, parent, state.initialIndex));
}
var widget = _buildWidget(state, context, index);
if (widget != null) {
widgets.removeWhere((element) => element.key == widget.key);
widgets.add(widget);
}
return widgets;
}
Widget? _buildWidget(JsonState state, BuildContext context, int? index) {
if (state.uiType == UIType.input) {
return URLInput(
state.title,
state.urlPath!,
(value) {
final createString = state.createStringEvent;
if (createString != null) {
context.read<JsonBloc>().add(createString(value));
}
},
key: state.key,
);
} else if (state.uiType == UIType.list) {
if (state.options.isEmpty) { return _buildLoader(); }
var isListCheckmarked = state.options.length > 2;
return OptionsList(
state.options,
isListCheckmarked ? state.currentIndex : index,
state.title,
isListCheckmarked ? ListType.checkmark : ListType.radio,
return Column(children: [
OptionsList(_waysToGetConfig, '1. Выберите способ получения конфига',
ListType.radio, (selectedIndex) {
setState(() {
_waysToGetConfig.unselectAll();
_waysToGetConfig[selectedIndex].isSelected = true;
_resetSecondStep();
});
}, key: const Key('ListChooseWayToGetConfigID')),
if (_waysToGetConfig[0].isSelected)
OptionsList(_waysToGetUrlpath, '2. Выберите способ получения ссылки',
ListType.radio, (selectedIndex) {
setState(() {
_waysToGetUrlpath.unselectAll();
_waysToGetUrlpath[selectedIndex].isSelected = true;
_resetThirdStep();
});
}, key: const Key('ListChooseWayToGetUrlID'))
else if (_waysToGetConfig[1].isSelected)
OptionsList(
_firebaseConfigs, '2. Выберите файл конфига', ListType.checkmark,
(selectedIndex) {
final createIndexed = state.createIndexedEvent;
if (createIndexed != null) {
context.read<JsonBloc>().add(createIndexed(selectedIndex));
}
},
key: state.key
);
} else {
return null;
}
setState(() {
_firebaseConfigs.unselectAll();
_firebaseConfigs[selectedIndex].isSelected = true;
_resetThirdStep();
widget.isReady!(true);
});
}, key: const Key('ListChooseFbConfigID')),
if (_waysToGetUrlpath[0].isSelected)
OptionsList(
_linksExamples, '3. Выберите пример ссылки', ListType.checkmark,
(selectedIndex) {
setState(() {
_linksExamples.unselectAll();
_linksExamples[selectedIndex].isSelected = true;
widget.isReady!(true);
});
}, key: const Key('ListChooseUrlExampleID'))
else if (_waysToGetUrlpath[1].isSelected)
URLInput('3. Укажите ссылку',
_currentUrlPath, (isURLEmpty) {
setState(() {
widget.isReady!(!isURLEmpty);
});
}),
]);
}
Widget _buildLoader() {
return Container(
margin: const EdgeInsets.all(10),
width: double.infinity,
height: 80,
child: const CupertinoActivityIndicator(radius: 20),
);
void _resetSecondStep() {
setState(() {
_firebaseConfigs.unselectAll();
_waysToGetUrlpath.unselectAll();
_resetThirdStep();
});
}
void _resetThirdStep() {
setState(() {
_linksExamples.unselectAll();
_currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
if (widget.isReady != null) {
widget.isReady!(false);
}
});
}
}
@@ -1,7 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/views/checkmark_list_option.dart';
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/radio_list_option.dart';
import '../../../common/models/option_data.dart';
@@ -10,12 +8,11 @@ enum ListType { radio, checkmark }
class OptionsList extends StatelessWidget {
final List<OptionData> options;
final int? selectedOptionIndex;
final String? title;
final ListType type;
final Function(int)? onSelection;
const OptionsList(this.options, this.selectedOptionIndex, this.title, this.type, this.onSelection, {super.key});
const OptionsList(this.options, this.title, this.type, this.onSelection, {super.key});
@override
Widget build(BuildContext context) {
@@ -30,7 +27,6 @@ class OptionsList extends StatelessWidget {
Text(title!,
textAlign: TextAlign.right,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
fontWeight: FontWeight.w500,
color: CupertinoColors.black)),
@@ -51,32 +47,24 @@ class OptionsList extends StatelessWidget {
color: CupertinoColors.separator);
},
itemBuilder: (_, int newIndex) {
return BlocBuilder<JsonBloc, JsonState>(
builder: (context, state) {
return _buildListOptionWidget(type, newIndex, selectedOptionIndex, context.read<JsonBloc>());
},
);
if (type == ListType.radio) {
return RadioListOption(options[newIndex], () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else if (type == ListType.checkmark) {
return CheckmarkListOption(options[newIndex], () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else {
return null;
}
}))
],
),
);
}
Widget _buildListOptionWidget(ListType type, int newIndex, int? selectedIndex, JsonBloc bloc) {
if (type == ListType.radio) {
return RadioListOption(options[newIndex], newIndex == selectedIndex, () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else if (type == ListType.checkmark) {
return CheckmarkListOption(options[newIndex].title, newIndex == selectedIndex, () {
if (onSelection != null) {
onSelection!(newIndex);
}
});
} else {
return const Text('unknown');
}
}
}
@@ -3,10 +3,9 @@ import 'package:nut_player_example/src/common/models/option_data.dart';
class RadioListOption extends StatelessWidget {
final OptionData data;
final bool isSelected;
final Function? onTap;
const RadioListOption(this.data, this.isSelected, this.onTap, {super.key});
const RadioListOption(this.data, this.onTap, {super.key});
@override
Widget build(BuildContext context) {
@@ -19,10 +18,10 @@ class RadioListOption extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.all(color: CupertinoColors.link, width: 2.5),
color: isSelected ? CupertinoColors.link : CupertinoColors.white
color: data.isSelected ? CupertinoColors.link : CupertinoColors.white
),
),
title: Text(data.title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
title: Text(data.title, style: const TextStyle(fontSize: 17))
);
}
}
@@ -1,27 +1,29 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
class URLInput extends StatelessWidget {
class URLInput extends StatefulWidget {
final String title;
final String initialText;
final Function(String)? newText;
final String placeholder;
final Function(bool)? isTextFieldEmpty;
URLInput(this.title, this.initialText, this.newText, {super.key});
const URLInput(this.title, this.placeholder, this.isTextFieldEmpty, {super.key});
@override
State<URLInput> createState() => _URLInputState();
}
class _URLInputState extends State<URLInput> {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
_controller.text = initialText;
return Container(
margin: const EdgeInsets.only(top: 20, left: 16, right: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
Text(widget.title,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
fontWeight: FontWeight.w500,
color: CupertinoColors.black)),
@@ -48,7 +50,6 @@ class URLInput extends StatelessWidget {
onPressed: _getClipboardText,
child: const Text('Вставка',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 17,
fontWeight: FontWeight.w500,
color: CupertinoColors.link))),
@@ -56,7 +57,6 @@ class URLInput extends StatelessWidget {
onPressed: _cleanTextField,
child: const Text('Очистить',
style: TextStyle(
decoration: TextDecoration.none,
fontSize: 17,
fontWeight: FontWeight.w500,
color: CupertinoColors.link)))
@@ -69,12 +69,20 @@ class URLInput extends StatelessWidget {
void _getClipboardText() async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
_controller.text = clipboardData?.text ?? '';
newText?.call(_controller.text);
setState(() {
_controller.text = clipboardData?.text ?? '';
if (widget.isTextFieldEmpty != null) {
widget.isTextFieldEmpty!(_controller.text.isEmpty);
}
});
}
void _cleanTextField() async {
_controller.text = '';
newText?.call('');
setState(() {
_controller.text = '';
if (widget.isTextFieldEmpty != null) {
widget.isTextFieldEmpty!(true);
}
});
}
}
@@ -1,108 +1,169 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/domain/url_bloc/url_bloc.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/options_list.dart';
import 'package:nut_player_example/src/features/main_screen/presentation/url_input.dart';
import '../../../common/extensions/options_list_extension.dart';
import '../../../common/models/option_data.dart';
class URLView extends StatelessWidget {
class URLView extends StatefulWidget {
final Function(bool)? isReady;
const URLView({super.key});
const URLView(this.isReady, {super.key});
@override
State<URLView> createState() => _URLViewState();
}
class _URLViewState extends State<URLView> {
final List<OptionData> _waysToDownloadVideo = [
OptionData(key: const Key('ListElementExampleID'), title: 'Использовать пример'),
OptionData(key: const Key('ListElementUrlID'), title: 'Загрузить по ссылке'),
];
final List<OptionData> _videoExamples = [
OptionData(key: const Key('ListElementUrl1ID'), title: 'MP4'),
OptionData(key: const Key('ListElementUrl2ID'), title: 'Example_2'),
OptionData(key: const Key('ListElementUrl3ID'), title: 'Example_3'),
OptionData(key: const Key('ListElementUrl4ID'), title: 'MOV'),
OptionData(key: const Key('ListElementUrl5ID'), title: 'Example_5'),
OptionData(key: const Key('ListElementUrl6ID'), title: 'AVI'),
OptionData(key: const Key('ListElementUrl7ID'), title: 'MKV')
];
final List<OptionData> _waysToGetUrl = [
OptionData(key: const Key('ListElementUseUrlID'), title: 'Указать ссылку'),
OptionData(key: const Key('ListElementExtraOptionsID'), title: 'Использовать доп. опции')
];
final List<OptionData> _extraOptions = [
OptionData(key: const Key('ListElementNoFormatOptionID'), title: 'Ссылка без расширения'),
OptionData(key: const Key('ListElementJSONFileID'), title: 'JSON-файл для MP4'),
];
final List<OptionData> _formats = [
OptionData(key: const Key('ListElementFormat1ID'), title: 'MP4'),
OptionData(key: const Key('ListElementFormat2ID'), title: 'Example_3'),
OptionData(key: const Key('ListElementFormat3ID'), title: 'MOV'),
OptionData(key: const Key('ListElementFormat4ID'), title: 'MKV')
];
final List<OptionData> _configs = [
OptionData(key: const Key('ListElementConfig1ID'), title: 'Config 1'),
OptionData(key: const Key('ListElementConfig2ID'), title: 'Config 2'),
OptionData(key: const Key('ListElementConfig3ID'), title: 'Config 3'),
OptionData(key: const Key('ListElementConfig4ID'), title: 'Config 4')
];
String _currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
bool _isURLCorrect = false;
bool _isFormatSelected = false;
@override
Widget build(BuildContext context) {
return BlocBuilder<UrlBloc, UrlState>(
builder: (context, state) {
return Column(
children: _buildAllWidgets(context, state, null)
);
},
);
}
List<Widget> _buildAllWidgets(BuildContext context, UrlState state, int? index) {
List<Widget> widgets = [];
final parent = state.parent;
if (parent != null) {
widgets.addAll(_buildAllWidgets(context, parent, state.currentIndex ?? state.initialIndex));
}
var newWidget = _buildWidget(state, context, index);
if (newWidget != null) {
widgets.removeWhere((element) => element.key == newWidget.key);
widgets.add(newWidget);
}
return widgets;
}
Widget? _buildWidget(UrlState state, BuildContext context, int? index) {
if (state.uiType == UIType.listWithInput) {
return _buildComplexWidget(state, context);
} else if (state.uiType == UIType.input) {
return URLInput(
state.title,
state.visibleUrlPath!,
(value) {
final createString = state.createStringEvent;
if (createString != null) {
context.read<UrlBloc>().add(createString(value));
}
},
key: state.key,
);
} else if (state.uiType == UIType.list) {
var isListCheckmarked = state.options.length > 2;
return OptionsList(
state.options,
isListCheckmarked ? state.currentIndex : index,
state.title,
isListCheckmarked ? ListType.checkmark : ListType.radio,
(selectedIndex) {
final createIndexed = state.createIndexedEvent;
if (createIndexed != null) {
context.read<UrlBloc>().add(createIndexed(selectedIndex));
}
},
key: state.key
);
} else {
return null;
}
}
Widget? _buildComplexWidget(UrlState state, BuildContext context) {
if (state.visibleUrlPath == null) { return null; }
return Column(
children: [
URLInput(
state.title,
state.visibleUrlPath!,
(value) {
final createString = state.createStringEvent;
if (createString != null) {
context.read<UrlBloc>().add(createString(value));
OptionsList(_waysToDownloadVideo, '1. Выберите вариант загрузки видео',
ListType.radio, (selectedIndex) {
_waysToDownloadVideo.unselectAll();
_waysToDownloadVideo[selectedIndex].isSelected = true;
_resetSecondStep();
}, key: const Key('ListChooseDownloadVideoOption')),
if (_waysToDownloadVideo[0].isSelected)
OptionsList(_videoExamples, '2. Выберите пример видео',
ListType.checkmark, (selectedIndex) {
setState(() {
_videoExamples.unselectAll();
_videoExamples[selectedIndex].isSelected = true;
_resetThirdStep();
if (widget.isReady != null) {
widget.isReady!(true);
}
});
}, key: const Key('ListChooseWayToGetUrlID'))
else if (_waysToDownloadVideo[1].isSelected)
OptionsList(
_waysToGetUrl, '2. Выберите способ получения ссылки',
ListType.radio, (selectedIndex) {
setState(() {
_waysToGetUrl.unselectAll();
_waysToGetUrl[selectedIndex].isSelected = true;
_resetThirdStep();
});
}, key: const Key('ListChooseFbConfigID')),
if (_waysToGetUrl[0].isSelected)
URLInput('3. Вставьте ссылку',
_currentUrlPath, (isURLEmpty) {
_resetFourthStep();
if (widget.isReady != null) {
widget.isReady!(!isURLEmpty);
}
},
key: state.key,
),
OptionsList(
state.options,
state.currentIndex,
null,
ListType.checkmark,
(selectedIndex) {
final createIndexed = state.createIndexedEvent;
if (createIndexed != null) {
context.read<UrlBloc>().add(createIndexed(selectedIndex));
}
},
),
],
})
else if (_waysToGetUrl[1].isSelected)
OptionsList(_extraOptions, '3. Выберите дополнительные опции',
ListType.radio, (selectedIndex) {
setState(() {
_extraOptions.unselectAll();
_extraOptions[selectedIndex].isSelected = true;
_resetFourthStep();
});
}),
if (_extraOptions[0].isSelected)
Column(
children: [
URLInput('4. Вставьте ссылку и выберите формат видео', _currentUrlPath, (isURLEmpty) {
_isURLCorrect = !isURLEmpty;
if (widget.isReady != null) {
widget.isReady!(_isURLCorrect && _isFormatSelected);
}
}),
OptionsList(_formats, null, ListType.checkmark, (selectedIndex) {
setState(() {
_formats.unselectAll();
_formats[selectedIndex].isSelected = true;
_isFormatSelected = true;
if (widget.isReady != null) {
widget.isReady!(_isURLCorrect && _isFormatSelected);
}
});
})
]
)
else if (_extraOptions[1].isSelected)
OptionsList(_configs, '4. Выберите файл конфигурации', ListType.checkmark, (selectedIndex) {
setState(() {
_configs.unselectAll();
_configs[selectedIndex].isSelected = true;
if (widget.isReady != null) {
widget.isReady!(true);
}
});
})
]
);
}
void _resetSecondStep() {
setState(() {
_videoExamples.unselectAll();
_waysToGetUrl.unselectAll();
_resetThirdStep();
});
}
void _resetThirdStep() {
setState(() {
_extraOptions.unselectAll();
_resetFourthStep();
});
}
void _resetFourthStep() {
setState(() {
_configs.unselectAll();
_formats.unselectAll();
_currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
if (widget.isReady != null) {
widget.isReady!(false);
}
});
}
}
@@ -1,264 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/player_screen/domain/model/subtitle.dart';
import 'package:nut_player_example/src/features/player_screen/domain/model/video_quality.dart';
import 'package:nut_player_example/src/features/player_screen/mapper/settings_mapper.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
part 'playerview_event.dart';
part 'playerview_state.dart';
class PlayerViewBloc extends Bloc<PlayerViewEvent, PlayerViewState> {
final VideoPlayerController controller;
late SettingsRepository startSettings;
late VoidCallback? _subtitlesListener;
late VoidCallback? _qualitiesListener;
late VoidCallback? _isPlayingListener;
PlayerViewBloc({required Provider provider, required SettingsRepository repository}):
controller = VideoPlayerController.provider(provider)
..initialize(params: {'enablePip': repository.isPipAvailable,
'enableAutostart': repository.isAutostart,
'enableSettings': repository.isSettingsAvailable,
'enableFullscreen': repository.fullscreenSettings != RepositoryFullscreenSettings.off,
'isSkinByDefault': repository.isSkinByDefault,
'startPosition': repository.start,
'enableLoop': repository.isLoop,
'quality': SettingsMapper.qualityDictionary(repository.quality),
'qualityNaming': repository.qualityNaming.name,
'timeouts': {
'playlist': repository.playlist,
'track': repository.track,
'chunk': repository.chunk
}}),
super(PlayerViewController.createFromRepository(repository)) {
startSettings = repository;
_onInitialize(repository);
on<DismissEvent>(_onDismissEvent);
on<PlayEvent>(_onPlayEvent);
on<PauseEvent>(_onPauseEvent);
on<EndEvent>(_onEndEvent);
on<SeekEvent>(_onSeekEvent);
on<SpeedChangedEvent>(_onSpeedChangedEvent);
on<VolumeChangedEvent>(_onVolumeChangedEvent);
on<QualityChangedEvent>(_onQualityChangedEvent);
on<SubsChangedEvent>(_onSubsChangedEvent);
on<StartPositionChangedEvent>(_onStartPositionChangedEvent);
on<SubsReceivedEvent>(_onSubsReceivedEvent);
on<QualitiesReceivedEvent>(_onQualitiesReceivedEvent);
}
_onInitialize(SettingsRepository repository) {
controller.setLog(repository.log.key);
controller.setVolume(repository.volume);
controller.setBrightness(repository.brightness);
_isPlayingListener = () {
final isPlaying = controller.value.isPlaying;
if (isPlaying) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
controller.setPlaybackSpeed(playerState.speed);
_listenToChanges(_isPlayingListener, false);
}
};
_listenToChanges(_isPlayingListener, true);
_qualitiesListener = () {
final qualities = controller.value.qualities;
if (qualities != null && qualities.isNotEmpty) {
add(QualitiesReceivedEvent(qualities));
_listenToChanges(_qualitiesListener, false);
_qualitiesListener = null;
}
};
_listenToChanges(_qualitiesListener, true);
if (!repository.isSubtitlesAvailable) { return; }
_subtitlesListener = () {
final subtitles = controller.value.subtitles;
if (subtitles != null) {
if (subtitles.length > 1) { add(SubsReceivedEvent(subtitles)); }
_listenToChanges(_subtitlesListener, false);
_subtitlesListener = null;
}
};
_listenToChanges(_subtitlesListener, true);
}
_listenToChanges(VoidCallback? listener, bool listen) {
if (listener != null) {
listen ? controller.addListener(listener) : controller.removeListener(listener);
}
}
_onPlayEvent(PlayEvent event, Emitter<PlayerViewState> emit) {
controller.play();
}
_onPauseEvent(PauseEvent event, Emitter<PlayerViewState> emit) {
controller.pause();
}
_onEndEvent(EndEvent event, Emitter<PlayerViewState> emit) {
controller.end();
}
_onSeekEvent(SeekEvent event, Emitter<PlayerViewState> emit) async {
final seekTime = event.time;
controller.seek(Duration(seconds: seekTime.toInt()));
}
_onDismissEvent(DismissEvent event, Emitter<PlayerViewState> emit) {
emit(PlayerViewDismiss());
}
_onQualitiesReceivedEvent(QualitiesReceivedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final List<VideoQualityOption> mappedQualities = event.qualities.map((e) =>
VideoQualityOption(
id: e["id"],
bandwidth: e["bandwidth"],
width: e["width"],
height: e["height"],
title: e["title"]
)
).toList();
final settingsQuality = startSettings.quality;
final presetQuality = mappedQualities.firstWhere((element) =>
element.quality.identifier == settingsQuality.identifier,
orElse: () => mappedQualities.first);
var sortedQualities = mappedQualities..sort((e1, e2) => e1.height.compareTo(e2.height));
emit(playerState.copy(qualities: sortedQualities, quality: presetQuality));
}
_onSubsReceivedEvent(SubsReceivedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
var subtitles = event.subs.entries.map((element) => Subtitle(id: element.key, title: element.value)).toList();
final offSubtitle = subtitles.firstWhere((element) => element.title == "Settings.Subtitles.off");
final newOffSubtitle = Subtitle(id: offSubtitle.id, title: "Выкл.");
subtitles.removeWhere((element) => element.title == "Settings.Subtitles.off");
var sortedSubtitles = subtitles..sort((e1, e2) => e1.title.compareTo(e2.title));
sortedSubtitles.insert(0, newOffSubtitle);
emit(playerState.copy(subtitles: sortedSubtitles, currentSubtitle: sortedSubtitles.first));
}
_onSubsChangedEvent(SubsChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final currentSub = playerState.subtitles?.firstWhere((element) => event.option.value == element.id);
if (currentSub == null) { return; }
controller.setSubtitles(currentSub.id);
emit(playerState.copy(currentSubtitle: currentSub));
}
_onSpeedChangedEvent(SpeedChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final speedWithoutX = event.option.title.replaceAll(RegExp('x'), '');
final speed = double.tryParse(speedWithoutX) ?? 1;
controller.setPlaybackSpeed(speed);
emit(playerState.copy(speed: speed));
}
_onVolumeChangedEvent(VolumeChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final volume = double.parse(event.option.title);
controller.setVolume(volume);
emit(playerState.copy(volume: volume));
}
_onStartPositionChangedEvent(StartPositionChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final duration = Duration(seconds: event.value);
controller.seek(duration);
emit(playerState.copy(start: event.value));
}
_onQualityChangedEvent(QualityChangedEvent event, Emitter<PlayerViewState> emit) {
final playerState = state;
if (playerState is! PlayerViewController) { return; }
final currentQuality = playerState.qualities?.firstWhere((element) => event.option.value == element.id);
if (currentQuality == null) { return; }
controller.setQuality(currentQuality.id);
emit(playerState.copy(quality: currentQuality));
}
int currentNumericValue(NumericOptionData setting) {
final playerState = state;
if (playerState is! PlayerViewController) { return 0; }
if (setting.key == const Key('PlaybackOptionStartPositionID')) {
return playerState.start ?? 0;
} else {
return 0;
}
}
int selectedIndex(OptionDataContainer setting) {
if (setting.key == const Key('PlaybackVolumeSettingID')) {
return _selectedIndexForVolume(setting.options);
} else if (setting.key == const Key('PlaybackSpeedSettingID')) {
return _selectedIndexForSpeed(setting.options);
} else if (setting.key == const Key('PlaybackSubsSettingID')) {
return _selectedIndexForSubtitle(setting.options);
} else if (setting.key == const Key('PlaybackQualitySettingID')) {
return _selectedIndexForQuality(setting.options);
} else {
return 0;
}
}
int _selectedIndexForSpeed(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 3; }
return settings.indexWhere((element) => element.value == playerState.speed.toString());
}
int _selectedIndexForVolume(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 3; }
return settings.indexWhere((element) => element.value == playerState.volume.toString());
}
int _selectedIndexForSubtitle(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 0; }
return settings.indexWhere((element) => element.value == playerState.currentSubtitle?.id);
}
int _selectedIndexForQuality(List<OptionData> settings) {
final playerState = state;
if (playerState is! PlayerViewController) { return 0; }
return settings.indexWhere((element) => element.value == playerState.quality?.id);
}
}
@@ -1,67 +0,0 @@
part of 'playerview_bloc.dart';
@immutable
sealed class PlayerViewEvent {
const PlayerViewEvent();
}
class DismissEvent extends PlayerViewEvent {
const DismissEvent();
}
class PlayEvent extends PlayerViewEvent {
const PlayEvent();
}
class PauseEvent extends PlayerViewEvent {
const PauseEvent();
}
class EndEvent extends PlayerViewEvent {
const EndEvent();
}
class SeekEvent extends PlayerViewEvent {
final double time;
const SeekEvent(this.time);
}
class SpeedChangedEvent extends PlayerViewEvent {
final OptionData option;
const SpeedChangedEvent(this.option);
}
class VolumeChangedEvent extends PlayerViewEvent {
final OptionData option;
const VolumeChangedEvent(this.option);
}
class QualityChangedEvent extends PlayerViewEvent {
final OptionData option;
const QualityChangedEvent(this.option);
}
class SubsReceivedEvent extends PlayerViewEvent {
final Map<String, String> subs;
const SubsReceivedEvent(this.subs);
}
class QualitiesReceivedEvent extends PlayerViewEvent {
final List<Map<String, dynamic>> qualities;
const QualitiesReceivedEvent(this.qualities);
}
class SubsChangedEvent extends PlayerViewEvent {
final OptionData option;
const SubsChangedEvent(this.option);
}
class FullscreenChangedEvent extends PlayerViewEvent {
final OptionData option;
const FullscreenChangedEvent(this.option);
}
class StartPositionChangedEvent extends PlayerViewEvent {
final int value;
const StartPositionChangedEvent(this.value);
}
@@ -1,142 +0,0 @@
part of 'playerview_bloc.dart';
sealed class PlayerViewState {}
class PlayerViewDismiss extends PlayerViewState {}
@immutable
class PlayerViewController extends PlayerViewState {
final double volume;
final double speed;
final int? start;
final VideoQualityOption? quality;
final List<Subtitle>? subtitles;
final List<VideoQualityOption>? qualities;
final Subtitle? currentSubtitle;
PlayerViewController(
{required this.volume,
required this.speed,
this.start,
this.quality,
this.subtitles,
this.qualities,
this.currentSubtitle});
PlayerViewController copy(
{double? volume,
double? speed,
int? start,
VideoQualityOption? quality,
List<Subtitle>? subtitles,
List<VideoQualityOption>? qualities,
Subtitle? currentSubtitle}) {
return PlayerViewController(
volume: volume ?? this.volume,
speed: speed ?? this.speed,
start: start ?? this.start,
quality: quality ?? this.quality,
subtitles: subtitles ?? this.subtitles,
qualities: qualities ?? this.qualities,
currentSubtitle: currentSubtitle ?? this.currentSubtitle
);
}
factory PlayerViewController.createFromRepository(
SettingsRepository repository) {
return PlayerViewController(
volume: repository.volume,
speed: repository.speed,
start: repository.start);
}
static final playbackSettings = [
OptionDataContainer(
key: const Key('PlaybackVolumeSettingID'),
title: 'Громкость',
options: const [
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume00ID'), '0.0'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume01ID'), '0.1'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume02ID'), '0.2'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume03ID'), '0.3'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume04ID'), '0.4'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume05ID'), '0.5'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume06ID'), '0.6'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume07ID'), '0.7'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume08ID'), '0.8'),
OptionData.withValueEqualTitle(
Key('PlaybackOptionVolume09ID'), '0.9'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume10ID'), '1.0')
],
selectedIndex: 4,
onSelectedOption: (option) => VolumeChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackSpeedSettingID'),
title: 'Скорость',
options: const [
OptionData(key: Key('PlaybackOptionSpeed025xID'), title: '0.25x', value: '0.25'),
OptionData(key: Key('PlaybackOptionSpeed05xID'), title: '0.5x', value: '0.5'),
OptionData(key: Key('PlaybackOptionSpeed075xID'), title: '0.75x', value: '0.75'),
OptionData(key: Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', value: '1.0'),
OptionData(key: Key('PlaybackOptionSpeed125xID'), title: '1.25x', value: '1.25'),
OptionData(key: Key('PlaybackOptionSpeed15xID'), title: '1.5x', value: '1.5'),
OptionData(key: Key('PlaybackOptionSpeed175xID'), title: '1.75x', value: '1.75'),
OptionData(key: Key('PlaybackOptionSpeed2xID'), title: '2x', value: '2.0')
],
selectedIndex: 3,
onSelectedOption: (option) => SpeedChangedEvent(option)
)
];
static final startPosition = NumericOptionData(
key: const Key('PlaybackOptionStartPositionID'),
title: 'Стартовая позиция',
value: 0,
onChange: (newValue) => StartPositionChangedEvent(newValue)
);
static OptionDataContainer? createSubs(List<Subtitle>? subs) {
if (subs == null) { return null; }
final options = subs.map((option) => OptionData(
key: Key(option.id),
title: option.title,
value: option.id
)).toList();
return OptionDataContainer(
key: const Key('PlaybackSubsSettingID'),
title: 'Субтитры',
options: options,
selectedIndex: 0,
onSelectedOption: (option) => SubsChangedEvent(option)
);
}
static OptionDataContainer? createQualities(List<VideoQualityOption>? qualities) {
if (qualities == null) { return null; }
final options = qualities.map((option) => OptionData(
key: Key(option.id),
title: option.title,
value: option.id
)).toList();
return OptionDataContainer(
key: const Key('PlaybackQualitySettingID'),
title: 'Качество',
options: options,
selectedIndex: 0,
onSelectedOption: (option) => QualityChangedEvent(option)
);
}
}
@@ -1,83 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/features/player_screen/domain/bloc/playerview_bloc.dart';
part 'custom_skin_event.dart';
part 'custom_skin_state.dart';
class CustomSkinBloc extends Bloc<CustomSkinEvent, CustomSkinState> {
final PlayerViewBloc _bloc;
CustomSkinBloc(this._bloc): super(CustomSkin.create()) {
_onInitialize();
on<ValueChanged>(_onValueChanged);
on<SkinVisibilityChanged>(_onSkinVisibilityChanged);
on<CurrentPositionChanged>(_onCurrentPositionChanged);
on<PlaybackChanged>(_onPlaybackChanged);
on<PlayerSeekEvent>(_onPlayerSeekEvent);
on<PlayerSeekBackEvent>(_onPlayerSeekBackEvent);
on<PlayerSeekForwardEvent>(_onPlayerSeekForwardEvent);
}
_onInitialize() {
_bloc.controller.addListener(() {
add(ValueChanged(
_bloc.controller.value.isPlaying,
_bloc.controller.value.duration.inSeconds.toDouble(),
_bloc.controller.value.position.inSeconds.toDouble()
));
});
}
_onValueChanged(ValueChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
emit(skinState.copy(
isPlaying: event.isPlaying,
duration: event.duration,
currentPosition: event.time
));
}
_onSkinVisibilityChanged(SkinVisibilityChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
emit(skinState.copy(isVisible: !skinState.isVisible));
}
_onCurrentPositionChanged(CurrentPositionChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
emit(skinState.copy(currentPosition: event.time));
}
_onPlaybackChanged(PlaybackChanged event, Emitter<CustomSkinState> emit) {
final skinState = state;
if (skinState is! CustomSkin) { return; }
_bloc.add(skinState.isPlaying ? const PauseEvent() : const PlayEvent());
}
_onPlayerSeekBackEvent(PlayerSeekBackEvent event, Emitter<CustomSkinState> emit) async {
final currentTime = await _bloc.controller.position;
if (currentTime != null) {
final seekTime = currentTime.inSeconds - 15;
_bloc.add(SeekEvent(seekTime.toDouble()));
}
}
_onPlayerSeekForwardEvent(PlayerSeekForwardEvent event, Emitter<CustomSkinState> emit) async {
final currentTime = await _bloc.controller.position;
if (currentTime != null) {
final seekTime = currentTime.inSeconds + 15;
_bloc.add(SeekEvent(seekTime.toDouble()));
}
}
_onPlayerSeekEvent(PlayerSeekEvent event, Emitter<CustomSkinState> emit) {
_bloc.add(SeekEvent(event.time));
}
}
@@ -1,38 +0,0 @@
part of 'custom_skin_bloc.dart';
@immutable
sealed class CustomSkinEvent {}
class SkinVisibilityChanged extends CustomSkinEvent {
SkinVisibilityChanged();
}
class ValueChanged extends CustomSkinEvent {
final bool isPlaying;
final double duration;
final double time;
ValueChanged(this.isPlaying, this.duration, this.time);
}
class PlaybackChanged extends CustomSkinEvent {
PlaybackChanged();
}
class CurrentPositionChanged extends CustomSkinEvent {
final double time;
CurrentPositionChanged(this.time);
}
class PlayerSeekBackEvent extends CustomSkinEvent {
PlayerSeekBackEvent();
}
class PlayerSeekForwardEvent extends CustomSkinEvent {
PlayerSeekForwardEvent();
}
class PlayerSeekEvent extends CustomSkinEvent {
final double time;
PlayerSeekEvent(this.time);
}
@@ -1,31 +0,0 @@
part of 'custom_skin_bloc.dart';
@immutable
sealed class CustomSkinState {}
class CustomSkin extends CustomSkinState {
final bool isVisible;
final bool isPlaying;
final double duration;
final double currentPosition;
CustomSkin({required this.isVisible, required this.isPlaying, required this.duration, required this.currentPosition});
CustomSkin copy({bool? isVisible, bool? isPlaying, double? duration, double? currentPosition}) {
return CustomSkin(
isVisible: isVisible ?? this.isVisible,
isPlaying: isPlaying ?? this.isPlaying,
duration: duration ?? this.duration,
currentPosition: currentPosition ?? this.currentPosition
);
}
factory CustomSkin.create() {
return CustomSkin(
isVisible: true,
isPlaying: false,
duration: 0,
currentPosition: 0
);
}
}
@@ -1,6 +0,0 @@
class Subtitle {
final String id;
final String title;
Subtitle({required this.id, required this.title});
}
@@ -1,73 +0,0 @@
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
class VideoQualityOption {
String id;
int bandwidth;
int width;
int height;
String title;
late VideoQuality quality;
VideoQualityOption({
required this.id,
required this.bandwidth,
required this.width,
required this.height,
required this.title
}) {
quality = _videoQuality(width,height,bandwidth);
}
static VideoQuality _qualityFromResolution(int pixels) {
if (pixels >= 1000 && pixels < 90500) {
return VideoQuality.p144;
} else if (pixels >= 90500 && pixels < 170500) {
return VideoQuality.p240;
} else if (pixels >= 170500 && pixels < 280500) {
return VideoQuality.p360;
} else if (pixels >= 280500 && pixels < 640500) {
return VideoQuality.p480;
} else if (pixels >= 640500 && pixels < 1500500) {
return VideoQuality.p720;
} else if (pixels >= 1500500 && pixels < 2400500) {
return VideoQuality.p1080;
} else if (pixels >= 2400500 && pixels < 6000500) {
return VideoQuality.p1440;
} else if (pixels >= 6000500) {
return VideoQuality.p2160;
} else {
return VideoQuality.auto;
}
}
static VideoQuality _qualityFromBandwidth(int bandwidth) {
if (bandwidth >= 1 && bandwidth < 400001) {
return VideoQuality.p144;
} else if (bandwidth >= 400001 && bandwidth < 800001) {
return VideoQuality.p240;
} else if (bandwidth >= 800001 && bandwidth < 1200001) {
return VideoQuality.p360;
} else if (bandwidth >= 1200001 && bandwidth < 1800001) {
return VideoQuality.p480;
} else if (bandwidth >= 1800001 && bandwidth < 3500001) {
return VideoQuality.p720;
} else if (bandwidth >= 3500001 && bandwidth < 8000001) {
return VideoQuality.p1080;
} else if (bandwidth >= 8000001 && bandwidth < 12000001) {
return VideoQuality.p1440;
} else if (bandwidth >= 12000001) {
return VideoQuality.p2160;
} else {
return VideoQuality.auto;
}
}
static VideoQuality _videoQuality(int width, int height, int bandwidth) {
if (width != 0 && height != 0) {
final pixels = width * height;
return _qualityFromResolution(pixels);
} else {
return _qualityFromBandwidth(bandwidth);
}
}
}
@@ -1,157 +0,0 @@
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
class SettingsMapper {
static RepositoryFullscreenSettings repoFullscreenSettings(FullscreenSettings setting) {
switch (setting) {
case FullscreenSettings.landscape:
return RepositoryFullscreenSettings.landscape;
case FullscreenSettings.flexible:
return RepositoryFullscreenSettings.flexible;
case FullscreenSettings.off:
return RepositoryFullscreenSettings.off;
}
}
static RepositoryVideoQualityNaming repoQualityNaming(String naming) {
switch (naming) {
case "common":
return RepositoryVideoQualityNaming.common;
case "rus":
return RepositoryVideoQualityNaming.rus;
case "eng":
return RepositoryVideoQualityNaming.eng;
case "resolution":
return RepositoryVideoQualityNaming.resolution;
default:
return RepositoryVideoQualityNaming.common;
}
}
static VideoQualityNaming qualityNaming(RepositoryVideoQualityNaming naming) {
switch (naming) {
case RepositoryVideoQualityNaming.common:
return VideoQualityNaming.common;
case RepositoryVideoQualityNaming.rus:
return VideoQualityNaming.rus;
case RepositoryVideoQualityNaming.eng:
return VideoQualityNaming.eng;
case RepositoryVideoQualityNaming.resolution:
return VideoQualityNaming.resolution;
default:
return VideoQualityNaming.common;
}
}
static RepositoryVideoQuality repoVideoQuality(String setting) {
switch (setting) {
case "auto":
return RepositoryVideoQuality.auto;
case "p144":
return RepositoryVideoQuality.p144;
case "p240":
return RepositoryVideoQuality.p240;
case "p360":
return RepositoryVideoQuality.p360;
case "p480":
return RepositoryVideoQuality.p480;
case "p720":
return RepositoryVideoQuality.p720;
case "p1080":
return RepositoryVideoQuality.p1080;
case "p1440":
return RepositoryVideoQuality.p1440;
case "p2160":
return RepositoryVideoQuality.p2160;
default:
return RepositoryVideoQuality.auto;
}
}
static VideoQuality quality(RepositoryVideoQuality videoQuality) {
switch (videoQuality) {
case RepositoryVideoQuality.auto:
return VideoQuality.auto;
case RepositoryVideoQuality.p2160:
return VideoQuality.p2160;
case RepositoryVideoQuality.p1440:
return VideoQuality.p1440;
case RepositoryVideoQuality.p1080:
return VideoQuality.p1080;
case RepositoryVideoQuality.p720:
return VideoQuality.p720;
case RepositoryVideoQuality.p480:
return VideoQuality.p480;
case RepositoryVideoQuality.p360:
return VideoQuality.p360;
case RepositoryVideoQuality.p240:
return VideoQuality.p240;
case RepositoryVideoQuality.p144:
return VideoQuality.p144;
}
}
static FullscreenSettings fullscreenSetting(RepositoryFullscreenSettings settings) {
switch (settings) {
case RepositoryFullscreenSettings.landscape:
return FullscreenSettings.landscape;
case RepositoryFullscreenSettings.flexible:
return FullscreenSettings.flexible;
case RepositoryFullscreenSettings.off:
return FullscreenSettings.off;
}
}
static SkinColor skinColor(RepositorySkinColor skinColor) {
switch (skinColor) {
case RepositorySkinColor.white:
return SkinColor.white;
}
}
static LogType logType(RepositoryLogType logType) {
switch (logType) {
case RepositoryLogType.off:
return LogType.off;
case RepositoryLogType.info:
return LogType.info;
case RepositoryLogType.debug:
return LogType.debug;
}
}
static Map<String, dynamic> qualityDictionary(RepositoryVideoQuality quality) {
switch (quality) {
case RepositoryVideoQuality.auto:
return _createQualityMap(bandwidth: 0, width: 0, height: 0);
case RepositoryVideoQuality.p2160:
return _createQualityMap(bandwidth: 32000000, width: 3840, height: 2160);
case RepositoryVideoQuality.p1440:
return _createQualityMap(bandwidth: 12000000, width: 2560, height: 1440);
case RepositoryVideoQuality.p1080:
return _createQualityMap(bandwidth: 8000000, width: 1920, height: 1080);
case RepositoryVideoQuality.p720:
return _createQualityMap(bandwidth: 3500000, width: 1280, height: 720);
case RepositoryVideoQuality.p480:
return _createQualityMap(bandwidth: 1800000, width: 854, height: 480);
case RepositoryVideoQuality.p360:
return _createQualityMap(bandwidth: 1200000, width: 640, height: 360);
case RepositoryVideoQuality.p240:
return _createQualityMap(bandwidth: 800000, width: 426, height: 240);
case RepositoryVideoQuality.p144:
return _createQualityMap(bandwidth: 400000, width: 256, height: 144);
default:
return _createQualityMap(bandwidth: 0, width: 0, height: 0);
}
}
static Map<String, int> _createQualityMap({required int bandwidth,
required int width,
required int height}) {
return {
'bandwidth': bandwidth,
'height': height,
'width': width,
};
}
}
@@ -8,24 +8,25 @@ class PlayerButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(
child: CupertinoButton(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 10),
borderRadius: const BorderRadius.all(Radius.circular(10)),
return SizedBox(
width: 103,
child: CupertinoButton(
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 20),
borderRadius: const BorderRadius.all(Radius.circular(10)),
color: CupertinoColors.link,
child: Text(title,
textAlign: TextAlign.center,
style: const TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.white,
fontWeight: FontWeight.w500)
textAlign: TextAlign.center,
style: const TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.w500
)
),
onPressed: () {
if (onPressed != null) {
onPressed!();
}
}
)
),
);
}
}
}
@@ -1,139 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/features/player_screen/domain/bloc/playerview_bloc.dart';
import 'package:nut_player_example/src/features/player_screen/domain/custom_skin_bloc/custom_skin_bloc.dart';
class PlayerCustomSkin extends StatelessWidget {
const PlayerCustomSkin({super.key});
@override
Widget build(BuildContext context) {
final bloc = CustomSkinBloc(context.read<PlayerViewBloc>());
return BlocProvider(
create: (context) => bloc,
child: GestureDetector(
onTap: () { bloc.add(SkinVisibilityChanged()); },
child: BlocBuilder<CustomSkinBloc, CustomSkinState>(
builder: (context, state) {
final skinState = state;
if (skinState is! CustomSkin) { return const Text('Incorrect widget'); }
return AbsorbPointer(
absorbing: !skinState.isVisible,
child: Visibility(
maintainSize: true,
maintainAnimation: true,
maintainState: true,
visible: skinState.isVisible,
child: Container(
decoration: BoxDecoration(
gradient: RadialGradient(
radius: 2,
colors: [
CupertinoColors.white.withOpacity(0.1),
CupertinoColors.systemMint.withOpacity(1)
]
)
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoButton(
onPressed: () {
bloc.add(PlayerSeekBackEvent());
},
child: Image.asset('assets/skinIcons/rewind_back.png', width: 50)
),
_buildPlaybackButton(bloc),
CupertinoButton(
onPressed: () {
bloc.add(PlayerSeekForwardEvent());
},
child: Image.asset('assets/skinIcons/rewind_forward.png', width: 50)
),
]
),
if (skinState.duration >= skinState.currentPosition && skinState.duration > 0)
_buildTimeArea(bloc)
],
)),
),
);
},
),
),
);
}
Widget _buildTimeArea(CustomSkinBloc skinBloc) {
final skinState = skinBloc.state;
if (skinState is! CustomSkin) { return const Text('Unsupported view'); }
return Column(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 10),
child: CupertinoSlider(
value: skinState.currentPosition,
activeColor: CupertinoColors.systemMint,
thumbColor: CupertinoColors.white,
min: 0,
max: skinState.duration,
onChangeEnd: (value) {
skinBloc.add(PlayerSeekEvent(value));
},
onChanged: (value) {
skinBloc.add(CurrentPositionChanged(value.roundToDouble()));
}),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text(_timeFormatter(skinState.currentPosition),
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 12,
decoration: TextDecoration.none)),
const Spacer(),
Text(_timeFormatter(skinState.duration),
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 12,
decoration: TextDecoration.none)),
],
),
)
],
);
}
Widget _buildPlaybackButton(CustomSkinBloc skinBloc) {
final skinState = skinBloc.state;
if (skinState is! CustomSkin) { return const Text('Incorrect widget'); }
return CupertinoButton(
onPressed: () {
skinBloc.add(PlaybackChanged());
},
child: Image.asset(skinState.isPlaying ? 'assets/skinIcons/pause.png' : 'assets/skinIcons/play.png', width: 70)
);
}
String _timeFormatter(double time) {
Duration duration = Duration(seconds: time.round());
return [duration.inHours, duration.inMinutes, duration.inSeconds]
.map((seg) => seg.remainder(60).toString().padLeft(2, '0'))
.join(':');
}
}
@@ -1,134 +0,0 @@
import 'package:flutter/cupertino.dart';
import '../../../common/models/option_data.dart';
class PlayerSettingsView extends StatelessWidget {
final List<OptionDataContainer> options;
final Function(OptionData, Key)? onChange;
const PlayerSettingsView({required this.options, this.onChange, super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(5))
),
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.only(top: 5, bottom: 30),
itemBuilder: (context, index) {
final item = options[index];
return Column(children: <Widget>[
_buildNavTile(item, context),
_buildSeparator()
]);
},
itemCount: options.length,
),
);
}
Widget _buildNavTile(OptionDataContainer option, BuildContext context) {
return CupertinoListTile(
title: Row(
children: [
Text(option.title, style: TextStyle(color: CupertinoColors.black.withOpacity(0.45), fontWeight: FontWeight.w400)),
const Spacer(),
Text(option.options[option.selectedIndex ?? 0].title, style: const TextStyle(color: CupertinoColors.black)),
const SizedBox(width: 7),
const Icon(CupertinoIcons.chevron_forward, color: CupertinoColors.systemGrey)
],
),
onTap: () async {
await Navigator.of(context).push<bool?>(
PageRouteBuilder(
opaque: false,
barrierDismissible: true,
pageBuilder: (_, __, ___) => _buildOptionsView(option, context)),
);
},
);
}
Widget _buildOptionsView(OptionDataContainer option, BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(5))
),
child: TapRegion(
onTapOutside: (_) {
Navigator.of(context)..pop()..pop();
},
child: ListView.builder(
padding: const EdgeInsets.only(top: 5, bottom: 30),
shrinkWrap: true,
itemBuilder: (context, index) {
if (index == 0) {
return Column(children: [
_buildBackButton(option, context),
_buildSeparator()
]);
} else {
return Column(children: [
_buildOptionTile(option, index - 1, context),
_buildSeparator()
]);
}
},
itemCount: option.options.length + 1,
),
),
),
);
}
Widget _buildBackButton(OptionDataContainer option, BuildContext context) {
return CupertinoListTile(
title: Row(
children: [
const Icon(CupertinoIcons.chevron_back, color: CupertinoColors.systemGrey),
const SizedBox(width: 5),
Text(option.title, style: TextStyle(color: CupertinoColors.black.withOpacity(0.45), fontWeight: FontWeight.w400)),
],
),
onTap: () {
Navigator.of(context).pop();
},
);
}
Widget _buildSeparator() {
return Container(
margin: const EdgeInsets.only(left: 20),
height: 0.8,
color: CupertinoColors.systemGrey4
);
}
Widget _buildOptionTile(OptionDataContainer option, int index, BuildContext context) {
final currentOption = option.options[index];
final isSelected = option.selectedIndex == index;
return CupertinoListTile(
title: Row(
children: [
if (isSelected) const Icon(CupertinoIcons.checkmark, color: CupertinoColors.black),
SizedBox(width: (isSelected) ? 6 : 30),
Text(currentOption.title, style: const TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w400)),
const Spacer()
],
),
onTap: () {
final key = option.key;
if (key != null) {
onChange?.call(currentOption, key);
Navigator.of(context)..pop()..pop();
}
},
);
}
}
@@ -1,393 +1,184 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/common/views/input_view.dart';
import 'package:nut_player_example/src/common/views/options_view.dart';
import 'package:nut_player_example/src/features/player_screen/domain/bloc/playerview_bloc.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_button.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_custom_skin.dart';
import 'package:nut_player_example/src/features/player_screen/presentation/player_settings_view.dart';
class PlayerView extends StatefulWidget {
const PlayerView({super.key});
class PlayerView extends StatelessWidget {
PlayerView({super.key});
@override
State<PlayerView> createState() => _PlayerViewState();
}
final controller = createPlayerController();
final List<OptionData> _playerAPIOptions = [
OptionData(key: const Key('PlaybackOptionVolumeID'), title: 'Громкость'),
OptionData(key: const Key('PlaybackOptionQualityID'), title: 'Качество'),
OptionData(key: const Key('PlaybackOptionSubsID'), title: 'Субтитры'),
OptionData(key: const Key('PlaybackOptionSpeedID'), title: 'Скорость')
];
final List<OptionData> _volumeOptions = [
OptionData(key: const Key('PlaybackOptionVolume00ID'), title: '0.0'),
OptionData(key: const Key('PlaybackOptionVolume01ID'), title: '0.1'),
OptionData(key: const Key('PlaybackOptionVolume02ID'), title: '0.2'),
OptionData(key: const Key('PlaybackOptionVolume03ID'), title: '0.3'),
OptionData(key: const Key('PlaybackOptionVolume04ID'), title: '0.4'),
OptionData(key: const Key('PlaybackOptionVolume05ID'), title: '0.5', isSelected: true),
OptionData(key: const Key('PlaybackOptionVolume06ID'), title: '0.6'),
OptionData(key: const Key('PlaybackOptionVolume07ID'), title: '0.7'),
OptionData(key: const Key('PlaybackOptionVolume08ID'), title: '0.8'),
OptionData(key: const Key('PlaybackOptionVolume09ID'), title: '0.9'),
OptionData(key: const Key('PlaybackOptionVolume10ID'), title: '1.0')
];
final List<OptionData> _speedOptions = [
OptionData(key: const Key('PlaybackOptionSpeed025xID'), title: '0.25x'),
OptionData(key: const Key('PlaybackOptionSpeed05xID'), title: '0.5x'),
OptionData(key: const Key('PlaybackOptionSpeed075xID'), title: '0.75x'),
OptionData(key: const Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', isSelected: true),
OptionData(key: const Key('PlaybackOptionSpeed125xID'), title: '1.25x'),
OptionData(key: const Key('PlaybackOptionSpeed15xID'), title: '1.5x'),
OptionData(key: const Key('PlaybackOptionSpeed175xID'), title: '1.75x'),
OptionData(key: const Key('PlaybackOptionSpeed2xID'), title: '2x'),
];
final List<OptionData> _qualityOptions = [
OptionData(key: const Key('PlaybackOptionQualityAutoID'), title: 'Авто', isSelected: true),
OptionData(key: const Key('PlaybackOptionQuality4kID'), title: '4K'),
OptionData(key: const Key('PlaybackOptionQuality1440pID'), title: '1440p Ultra HD'),
OptionData(key: const Key('PlaybackOptionQuality1080pID'), title: '1080p FHD'),
OptionData(key: const Key('PlaybackOptionQuality720pID'), title: '720p HD'),
OptionData(key: const Key('PlaybackOptionQuality480pID'), title: '480p'),
OptionData(key: const Key('PlaybackOptionQuality360pID'), title: '360p'),
OptionData(key: const Key('PlaybackOptionQuality240pID'), title: '240p'),
OptionData(key: const Key('PlaybackOptionQuality144pID'), title: '144p')
];
final List<OptionData> _subsOptions = [
OptionData(key: const Key('PlaybackOptionSubsOffID'), title: 'Выкл.', isSelected: true),
OptionData(key: const Key('PlaybackOptionSubsAutoID'), title: 'Созданы автоматически'),
OptionData(key: const Key('PlaybackOptionSubsEngID'), title: 'Английские'),
OptionData(key: const Key('PlaybackOptionSubsRusID'), title: 'Русские')
];
final NumericOptionData _startPositionOption = NumericOptionData(key: const Key('PlaybackOptionStartPositionID'), title: 'Стартовая позиция', value: 0);
class _PlayerViewState extends State<PlayerView> {
final TextEditingController _textController = TextEditingController();
StreamSubscription<String>? _logSubscription;
StreamSubscription<Object>? _skinActionsSubscription;
late PlayerViewBloc _bloc;
var _turns = 0;
bool _isFullScreen = false;
@override
void initState() {
super.initState();
_bloc = context.read<PlayerViewBloc>();
_logSubscription = _bloc.controller.logStream.listen((event) {
final text = _textController.text;
_textController.text = '$event\r\n$text';
});
_skinActionsSubscription = _bloc.controller.skinActionStream.listen((event) {
final call = event as MethodCall;
switch (call.method) {
case 'pluginSkinDefaultAction':
final fullscreen = call.arguments["onEnter"];
final repository = context.read<SettingsRepository>();
if (_isFullScreen != fullscreen &&
repository.fullscreenSettings != RepositoryFullscreenSettings.off) {
setState(() {
_isFullScreen = fullscreen;
if (fullscreen) {
// В случае ландшафтного режима нам нужно сделать 3 поворота,
// чтобы сделать ориентацию интерфейса как в ютубе.
// Можно повернуть на 1, но тогда интерфейс не будет по тому же
// краю, как в иосном демо-приложении
_turns = repository.fullscreenSettings == RepositoryFullscreenSettings.landscape ? 3 : 0;
} else {
_turns = 0;
}
});
}
case 'pluginSkinSettingsAction':
showCupertinoModalPopup(
context: context,
builder: (_) {
final speedSettings = PlayerViewController.playbackSettings.firstWhere((element) => element.key == const Key('PlaybackSpeedSettingID'));
final updatedSpeedSetting = _updatedSetting(speedSettings);
var options = [updatedSpeedSetting];
final qualitiesSettings = _createQualities(_bloc);
if (qualitiesSettings != null) {
final updatedQualitiesSettings = _updatedSetting(qualitiesSettings);
options.add(updatedQualitiesSettings);
}
final subsSettings = _createSubs(_bloc);
if (subsSettings != null) {
final updatedSubsSettings = _updatedSetting(subsSettings);
options.add(updatedSubsSettings);
}
return PlayerSettingsView(
options: options,
onChange: (option, key) {
switch (key) {
case const Key('PlaybackSpeedSettingID'):
_bloc.add(SpeedChangedEvent(option));
break;
case const Key('PlaybackSubsSettingID'):
_bloc.add(SubsChangedEvent(option));
break;
case const Key('PlaybackQualitySettingID'):
_bloc.add(QualityChangedEvent(option));
break;
default:
break;
}
});
}
);
}
});
}
@override
Widget build(BuildContext context) {
final videoPlayer = VideoPlayer(_bloc.controller);
return _isFullScreen ? _makeFullscreenWidget(videoPlayer)
: _makePortraitWidget(videoPlayer);
}
@override
void deactivate() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp
]);
_logSubscription?.cancel();
_logSubscription = null;
_skinActionsSubscription?.cancel();
_skinActionsSubscription = null;
final bloc = context.read<PlayerViewBloc>();
bloc.controller.dispose();
super.deactivate();
}
Widget _makePortraitWidget(VideoPlayer videoPlayer) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp
]);
_textController.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
navigationBar: CupertinoNavigationBar(
key: const Key('PlayerAppBarID'),
middle: const Text('Демо NutPlayer',
style: TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.black,
fontWeight: FontWeight.w600)),
leading: CupertinoNavigationBarBackButton(
previousPageTitle: 'Назад',
onPressed: () {
_bloc.add(const DismissEvent());
}),
padding: const EdgeInsetsDirectional.all(0),
navigationBar: const CupertinoNavigationBar(
key: Key('PlayerAppBarID'),
middle: Text('Демо NutPlayer',
style: TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w600)
),
leading: CupertinoNavigationBarBackButton(previousPageTitle: 'Назад'),
padding: EdgeInsetsDirectional.all(0),
backgroundColor: CupertinoColors.white,
),
child: SafeArea(
child: BlocBuilder<PlayerViewBloc, PlayerViewState>(
builder: (context, state) {
return Column(children: [
AspectRatio(
aspectRatio: 16 / 10,
child: _buildPlayer(videoPlayer, context),
),
const SizedBox(height: 10),
Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PlayerButton('Пуск', () {
_bloc.add(const PlayEvent());
}),
const SizedBox(width: 10),
PlayerButton('Пауза', () {
_bloc.add(const PauseEvent());
}),
const SizedBox(width: 10),
PlayerButton('Завершить', () {
_bloc.add(const EndEvent());
})
],
),
),
child: SingleChildScrollView(
child: Column(
children: [
AspectRatio(aspectRatio: 16/10,
// TODO: Встроить плеер
child: Container(
color: CupertinoColors.link,
child: VideoPlayer(controller),
)
),
Expanded(
child: ListView(
shrinkWrap: true,
children: [
CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('API Проигрывателя'.toUpperCase(),
style: const TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.systemGrey,
fontSize: 13,
fontWeight: FontWeight.w400)),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PlayerButton('Пуск', () {}),
PlayerButton('Пауза', () {}),
PlayerButton('Стоп', () {})
],
),
CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('API Проигрывателя'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
children: _buildDynamicWidgets(PlayerViewController.playbackSettings, _bloc)
),
CupertinoListSection.insetGrouped(
margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
hasLeading: false,
children: _buildWidgets([PlayerViewController.startPosition], _bloc)
),
),
children: <Widget>[..._playerAPIOptions.map((value) {
if (value.key == const Key('PlaybackOptionVolumeID')) {
return OptionsView(value.title, _volumeOptions);
} else if (value.key == const Key('PlaybackOptionSpeedID')) {
return OptionsView(value.title, _speedOptions);
} else if (value.key == const Key('PlaybackOptionQualityID')) {
return OptionsView(value.title, _qualityOptions);
} else {
return OptionsView(value.title, _subsOptions);
}
}).toList()]
),
if (context.read<SettingsRepository>().log != RepositoryLogType.off)
_buildLog()
]
)
)
]);
},
),
));
}
CupertinoListSection.insetGrouped(
margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
hasLeading: false,
children: <Widget>[InputView(_startPositionOption)]
),
Widget _buildLog() {
return CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text('Лог плеера'.toUpperCase(),
style: const TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.systemGrey,
fontSize: 13,
fontWeight: FontWeight.w400)),
const Spacer(),
SizedBox(
height: 25,
child: CupertinoButton(
onPressed: () {
_textController.text = '';
},
padding: const EdgeInsets.all(0),
child: const Text('Очистить')),
)
],
),
),
children: <Widget>[
CupertinoTextField(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 18),
decoration: const BoxDecoration(
border: null,
borderRadius: BorderRadius.all(Radius.circular(12)),
color: CupertinoColors.white,
),
minLines: 1,
maxLines: 10,
readOnly: true,
maxLength: null,
autocorrect: false,
keyboardType: TextInputType.multiline,
controller: _textController,
),
]
);
}
List<Widget> _buildDynamicWidgets(List<Object> objects, PlayerViewBloc bloc) {
var widgets = _buildWidgets(objects, bloc);
final qualities = _createQualities(bloc);
if (qualities != null) {
final qualityWidget = _buildOptionsView(qualities, bloc);
widgets.insert(1, qualityWidget);
}
final subtitles = _createSubs(bloc);
if (subtitles != null) {
final subtitleWidget = _buildOptionsView(subtitles, bloc);
widgets.insert(2, subtitleWidget);
}
return widgets;
}
OptionDataContainer? _createQualities(PlayerViewBloc bloc) {
final state = bloc.state;
if (state is! PlayerViewController) { return null; }
return PlayerViewController.createQualities(state.qualities);
}
OptionDataContainer? _createSubs(PlayerViewBloc bloc) {
final state = bloc.state;
if (state is! PlayerViewController) { return null; }
return PlayerViewController.createSubs(state.subtitles);
}
OptionDataContainer _updatedSetting(OptionDataContainer oldValue) {
return OptionDataContainer(
key: oldValue.key,
title: oldValue.title,
options: oldValue.options,
selectedIndex: _bloc.selectedIndex(oldValue),
onSelectedOption: oldValue.onSelectedOption
);
}
Widget _buildOptionsView(OptionDataContainer setting, PlayerViewBloc bloc) {
final selectedIndex = bloc.selectedIndex(setting);
return OptionsView(
setting.title,
setting.options,
selectedIndex,
(selectedIndex) {
final selectedOption = setting.options[selectedIndex];
final event = setting.onSelectedOption?.call(selectedOption);
if (event != null) {
bloc.add(event);
}
},
key: setting.key
);
}
List<Widget> _buildWidgets(List<Object> objects, PlayerViewBloc bloc) {
return <Widget>[...objects.map((setting) {
if (setting is OptionDataContainer) {
return _buildOptionsView(setting, bloc);
} else if (setting is NumericOptionData) {
final currentValue = bloc.currentNumericValue(setting);
return InputView(
setting.title,
currentValue, (newTime) {
final event = setting.onChange?.call(newTime);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
} else {
return const Text('not implemented');
}
}).toList()];
}
Widget _buildPlayer(VideoPlayer videoPlayer, BuildContext context) {
List<Widget> widgets = [videoPlayer];
final repository = context.read<SettingsRepository>();
final isSkinByDefault = repository.isSkinByDefault;
if (!isSkinByDefault) {
widgets.add(_buildCustomSkin());
}
return isSkinByDefault ? videoPlayer : Stack(children: widgets);
}
Widget _buildCustomSkin() {
return const PlayerCustomSkin();
}
Widget _makeFullscreenWidget(VideoPlayer videoPlayer) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
final size = MediaQuery.of(context).size;
return SafeArea(
child:
OrientationBuilder(
builder: (context, orientation) {
final repository = context.read<SettingsRepository>();
switch (orientation) {
case Orientation.portrait:
// в случае режима .landscape нужно поворачивать на 90 градусов 2 раза,
// т.к. 1 раз поворачивается сам экран + надо поворачивать
// представление, чтобы проскочить свободное положение экрана.
// Для .flexible не требуются дополнительные повороты, видео вращается вместе с экраном.
_turns += repository.fullscreenSettings == RepositoryFullscreenSettings.flexible ? 0 : 2;
case Orientation.landscape:
// в случае ландшатного режима нам нужно компенсировать поворот
// самого экрана, поэтому отнимаем один поворот на 90 градусов
_turns += repository.fullscreenSettings == RepositoryFullscreenSettings.flexible ? 0 : -1;
}
return RotatedBox(
quarterTurns: _turns,
child: Container(
width: size.width,
height: size.height,
color: CupertinoColors.black,
child: videoPlayer,
)
);
}
CupertinoListSection.insetGrouped(
hasLeading: false,
header: Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text('Лог плеера'.toUpperCase(),
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
const Spacer(),
SizedBox(
height: 25,
child: CupertinoButton(
onPressed: () { _textController.text = ''; },
padding: const EdgeInsets.all(0),
child: const Text('Очистить')),
)
],
),
),
children: <Widget>[
CupertinoTextField(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 18),
decoration: const BoxDecoration(
border: null,
borderRadius: BorderRadius.all(Radius.circular(12)),
color: CupertinoColors.white,
),
minLines: 1,
maxLines: 10,
readOnly: true,
maxLength: null,
autocorrect: false,
keyboardType: TextInputType.multiline,
controller: _textController,
),
]
),
]
),
)
)
);
}
static VideoPlayerController createPlayerController() {
final controller = VideoPlayerController.network("https://cloud.nut.tech/index.php/s/CEb4Xd48wd98kj4/download/star-wars.mp4")
..initialize();
return controller;
}
}
@@ -1,330 +0,0 @@
import 'dart:async';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player/nut_player.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/repository/settings_repository.dart';
import 'package:nut_player_example/src/features/player_screen/mapper/settings_mapper.dart';
part 'settings_event.dart';
part 'settings_state.dart';
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
final SettingsRepository _repository;
StreamSubscription<String>? _versionSubscription;
SettingsBloc(this._repository) : super(SettingsInitialState.createFromRepository(_repository)) {
_onInitialize();
on<DismissEvent>(_onDismissEvent);
on<CrashEvent>(_onCrashEvent);
on<SkinChangedEvent>(_onSkinChangedEvent);
on<SpeedChangedEvent>(_onSpeedChangedEvent);
on<VolumeChangedEvent>(_onVolumeChangedEvent);
on<BrightnessChangedEvent>(_onBrightnessChangedEvent);
on<AutostartChangedEvent>(_onAutostartChangedEvent);
on<FullscreenChangedEvent>(_onFullscreenChangedEvent);
on<PipChangedEvent>(_onPipChangedEvent);
on<SettingsChangedEvent>(_onSettingsChangedEvent);
on<ColorChangedEvent>(_onColorChangedEvent);
on<QualityChangedEvent>(_onQualityChangedEvent);
on<QualityNamingChangedEvent>(_onQualityNamingChangedEvent);
on<SubsChangedEvent>(_onSubsChangedEvent);
on<StartPositionChangedEvent>(_onStartPositionChangedEvent);
on<LoopChangedEvent>(_onLoopChangedEvent);
on<PlaylistTimeoutsChangedEvent>(_onPlaylistTimeoutsChangedEvent);
on<TrackTimeoutsChangedEvent>(_onTrackTimeoutsChangedEvent);
on<ChunkTimeoutsChangedEvent>(_onChunkTimeoutsChangedEvent);
on<LogTypeChangedEvent>(_onLogTypeChangedEvent);
on<VersionReceivedEvent>(_onVersionReceived);
}
_onInitialize() {
_versionSubscription = PlayerVersionObserver().versionStream.listen((version) {
add(VersionReceivedEvent(version));
});
}
_onVersionReceived(VersionReceivedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
emit(settingsState.copy(playerVersion: event.version));
}
_onDismissEvent(DismissEvent event, Emitter<SettingsState> emit) {
_versionSubscription?.cancel();
_versionSubscription = null;
emit(SettingsDismiss());
}
_onCrashEvent(CrashEvent event, Emitter<SettingsState> emit) {
FirebaseCrashlytics.instance.crash();
}
_onSkinChangedEvent(SkinChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isSkinByDefault = event.isSkinByDefault;
emit(settingsState.copy(isSkinByDefault: event.isSkinByDefault));
}
_onSpeedChangedEvent(SpeedChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final speedWithoutX = event.option.title.replaceAll(RegExp('x'), '');
final speed = double.tryParse(speedWithoutX) ?? 1;
_repository.speed = speed;
emit(settingsState.copy(speed: speed));
}
_onFullscreenChangedEvent(FullscreenChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final fullscreenMode = FullscreenSettings.values.firstWhere((element) => element.title == event.option.value);
_repository.fullscreenSettings = SettingsMapper.repoFullscreenSettings(fullscreenMode);
emit(settingsState.copy(fullscreenSettings: fullscreenMode));
}
_onVolumeChangedEvent(VolumeChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final volume = double.parse(event.option.title);
_repository.volume = volume;
emit(settingsState.copy(volume: volume));
}
_onBrightnessChangedEvent(BrightnessChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final brightness = double.parse(event.option.title);
_repository.brightness = brightness;
emit(settingsState.copy(brightness: brightness));
}
_onStartPositionChangedEvent(StartPositionChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.start = event.value;
emit(settingsState.copy(start: event.value));
}
_onPlaylistTimeoutsChangedEvent(PlaylistTimeoutsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.playlist = event.playlist;
emit(settingsState.copy(playlist: event.playlist));
}
_onTrackTimeoutsChangedEvent(TrackTimeoutsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.track = event.track;
emit(settingsState.copy(track: event.track));
}
_onChunkTimeoutsChangedEvent(ChunkTimeoutsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.chunk = event.chunk;
emit(settingsState.copy(chunk: event.chunk));
}
_onAutostartChangedEvent(AutostartChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isAutostart = event.isOn;
emit(settingsState.copy(isAutostart: event.isOn));
}
_onLoopChangedEvent(LoopChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isLoop = event.isOn;
emit(settingsState.copy(isLoop: event.isOn));
}
_onPipChangedEvent(PipChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isPipAvailable = event.isOn;
emit(settingsState.copy(isPipAvailable: event.isOn));
}
_onSettingsChangedEvent(SettingsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isSettingsAvailable = event.isOn;
emit(settingsState.copy(isSettingsAvailable: event.isOn));
}
_onSubsChangedEvent(SubsChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
_repository.isSubtitlesAvailable = event.isOn;
emit(settingsState.copy(isSubtitlesAvailable: event.isOn));
}
_onLogTypeChangedEvent(LogTypeChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
switch (event.log) {
case LogType.info:
_repository.log = RepositoryLogType.info;
case LogType.debug:
_repository.log = RepositoryLogType.debug;
case LogType.off:
_repository.log = RepositoryLogType.off;
}
emit(settingsState.copy(log: event.log));
}
_onQualityChangedEvent(QualityChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final quality = event.option.value;
if (quality == null) { return; }
final repoQuality = SettingsMapper.repoVideoQuality(quality);
_repository.quality = repoQuality;
final mappedQuality = SettingsMapper.quality(repoQuality);
emit(settingsState.copy(quality: mappedQuality));
}
_onQualityNamingChangedEvent(QualityNamingChangedEvent event, Emitter<SettingsState> emit) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return; }
final qualityNamingString = event.option.value;
if (qualityNamingString == null) { return; }
final repoQualityNaming = SettingsMapper.repoQualityNaming(qualityNamingString);
_repository.qualityNaming = repoQualityNaming;
emit(settingsState.copy(
qualityNaming: SettingsMapper.qualityNaming(repoQualityNaming))
);
}
_onColorChangedEvent(ColorChangedEvent event, Emitter<SettingsState> emit) {}
int currentNumericValue(NumericOptionData setting) {
if (setting.key == const Key('PlaybackOptionStartPositionID')) {
return _repository.start;
} else if (setting.key == const Key('TimeoutsOptionManifestID')) {
return _repository.playlist;
} else if (setting.key == const Key('TimeoutsOptionTrackID')) {
return _repository.track;
} else if (setting.key == const Key('TimeoutsOptionChunkID')) {
return _repository.chunk;
} else {
return 0;
}
}
int selectedIndex(OptionDataContainer setting) {
if (setting.key == const Key('PlaybackOptionSpeedID')) {
return _selectedIndexForSpeed(setting.options);
} else if (setting.key == const Key('PlaybackOptionVolumeID')) {
return _selectedIndexForVolume(setting.options);
} else if (setting.key == const Key('PlaybackOptionBrightnessID')) {
return _selectedIndexForBrightness(setting.options);
} else if (setting.key == const Key('FullscreenID')) {
return _selectedIndexForFullscreen(setting.options);
} else if (setting.key == const Key('PlaybackOptionQualityID')) {
return _selectedIndexForQuality(setting.options);
} else if (setting.key == const Key('PlaybackOptionQualityNamingID')) {
return _selectedIndexForQualityNaming(setting.options);
}
else {
return 0;
}
}
bool isSelected(BoolOptionData setting) {
if (setting.key == const Key('PlaybackOptionAutostartID')) {
return _repository.isAutostart;
} else if (setting.key == const Key('ExtraOptionLoopID')) {
return _repository.isLoop;
} else if (setting.key == const Key('SkinStandardPipID')) {
return _repository.isPipAvailable;
} else if (setting.key == const Key('SkinStandardSettingsID')) {
return _repository.isSettingsAvailable;
} else if (setting.key == const Key('SkinOptionDefaultID')) {
return _repository.isSkinByDefault;
} else if (setting.key == const Key('SkinOptionByUserID')) {
return !_repository.isSkinByDefault;
} else if (setting.key == const Key('PlaybackOptionSubtitlesID')) {
return _repository.isSubtitlesAvailable;
} else {
return false;
}
}
int _selectedIndexForSpeed(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 3; }
return settings.indexWhere((element) => element.value == settingsState.speed.toString());
}
int _selectedIndexForVolume(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 5; }
return settings.indexWhere((element) => element.value == settingsState.volume.toString());
}
int _selectedIndexForBrightness(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 5; }
return settings.indexWhere((element) {
double rounded = (settingsState.brightness*10).roundToDouble();
double bright = rounded/10.0;
return bright.toString() == element.value;
});
}
int _selectedIndexForFullscreen(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 0; }
return settings.indexWhere((element) => element.value == settingsState.fullscreenSettings.title);
}
int _selectedIndexForQuality(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 0; }
return settings.indexWhere((element) => element.value == settingsState.quality.identifier);
}
int _selectedIndexForQualityNaming(List<OptionData> settings) {
final settingsState = state;
if (settingsState is! SettingsInitialState) { return 0; }
return settings.indexWhere((element) => element.value == settingsState.qualityNaming.name);
}
}
@@ -1,107 +0,0 @@
part of 'settings_bloc.dart';
@immutable
abstract class SettingsEvent {}
class DismissEvent extends SettingsEvent {}
class CrashEvent extends SettingsEvent {}
class VersionReceivedEvent extends SettingsEvent {
final String version;
VersionReceivedEvent(this.version);
}
class SpeedChangedEvent extends SettingsEvent {
final OptionData option;
SpeedChangedEvent(this.option);
}
class VolumeChangedEvent extends SettingsEvent {
final OptionData option;
VolumeChangedEvent(this.option);
}
class SkinChangedEvent extends SettingsEvent {
final bool isSkinByDefault;
SkinChangedEvent(this.isSkinByDefault);
}
class BrightnessChangedEvent extends SettingsEvent {
final OptionData option;
BrightnessChangedEvent(this.option);
}
class QualityChangedEvent extends SettingsEvent {
final OptionData option;
QualityChangedEvent(this.option);
}
class QualityNamingChangedEvent extends SettingsEvent {
final OptionData option;
QualityNamingChangedEvent(this.option);
}
class FullscreenChangedEvent extends SettingsEvent {
final OptionData option;
FullscreenChangedEvent(this.option);
}
class ColorChangedEvent extends SettingsEvent {
final OptionData option;
ColorChangedEvent(this.option);
}
class SubsChangedEvent extends SettingsEvent {
final bool isOn;
SubsChangedEvent(this.isOn);
}
class PipChangedEvent extends SettingsEvent {
final bool isOn;
PipChangedEvent(this.isOn);
}
class SettingsChangedEvent extends SettingsEvent {
final bool isOn;
SettingsChangedEvent(this.isOn);
}
class AutostartChangedEvent extends SettingsEvent {
final bool isOn;
AutostartChangedEvent(this.isOn);
}
class LoopChangedEvent extends SettingsEvent {
final bool isOn;
LoopChangedEvent(this.isOn);
}
class StartPositionChangedEvent extends SettingsEvent {
final int value;
StartPositionChangedEvent(this.value);
}
class PlaylistTimeoutsChangedEvent extends SettingsEvent {
final int playlist;
PlaylistTimeoutsChangedEvent(this.playlist);
}
class TrackTimeoutsChangedEvent extends SettingsEvent {
final int track;
TrackTimeoutsChangedEvent(this.track);
}
class ChunkTimeoutsChangedEvent extends SettingsEvent {
final int chunk;
ChunkTimeoutsChangedEvent(this.chunk);
}
class LogTypeChangedEvent extends SettingsEvent {
final LogType log;
LogTypeChangedEvent(this.log);
}
@@ -1,360 +0,0 @@
part of 'settings_bloc.dart';
enum FullscreenSettings { landscape, flexible, off }
extension FullscreenSettingsExtension on FullscreenSettings {
String get title {
switch (this) {
case FullscreenSettings.landscape:
return 'Альбомный';
case FullscreenSettings.flexible:
return 'Свободный';
case FullscreenSettings.off:
return 'Выкл.';
default:
return 'Неизвестно';
}
}
}
enum SkinColor { white }
enum VideoQualityNaming { common, rus, eng, resolution }
enum LogType { off, info, debug }
enum VideoQuality { auto, p2160, p1440, p1080, p720, p480, p360, p240, p144 }
extension VideoQualityIdentifier on VideoQuality {
String get identifier {
switch (this) {
case VideoQuality.auto:
return "auto";
case VideoQuality.p144:
return "p144";
case VideoQuality.p240:
return "p240";
case VideoQuality.p360:
return "p360";
case VideoQuality.p480:
return "p480";
case VideoQuality.p720:
return "p720";
case VideoQuality.p1080:
return "p1080";
case VideoQuality.p1440:
return "p1440";
case VideoQuality.p2160:
return "p2160";
}
}
}
sealed class SettingsState {}
class SettingsDismiss extends SettingsState {}
@immutable
class SettingsInitialState extends SettingsState {
final String playerVersion;
final bool isSkinByDefault;
final FullscreenSettings fullscreenSettings;
final bool isPipAvailable;
final bool isSettingsAvailable;
final SkinColor skinColor;
final bool isAutostart;
final double volume;
final double brightness;
final double speed;
final VideoQualityNaming qualityNaming;
final VideoQuality quality;
final bool isSubtitlesAvailable;
final int start;
final bool isLoop;
final int playlist;
final int track;
final int chunk;
final LogType log;
SettingsInitialState({
required this.playerVersion,
required this.isSkinByDefault,
required this.fullscreenSettings,
required this.isPipAvailable,
required this.isSettingsAvailable,
required this.skinColor,
required this.isAutostart,
required this.brightness,
required this.volume,
required this.speed,
required this.qualityNaming,
required this.quality,
required this.isSubtitlesAvailable,
required this.start,
required this.isLoop,
required this.playlist,
required this.track,
required this.chunk,
required this.log
});
SettingsInitialState copy({
String? playerVersion,
bool? isSkinByDefault,
FullscreenSettings? fullscreenSettings,
bool? isPipAvailable,
bool? isSettingsAvailable,
SkinColor? skinColor,
bool? isAutostart,
double? volume,
double? brightness,
double? speed,
VideoQualityNaming? qualityNaming,
VideoQuality? quality,
bool? isSubtitlesAvailable,
int? start,
bool? isLoop,
int? playlist,
int? track,
int? chunk,
LogType? log
}) {
return SettingsInitialState(
playerVersion: playerVersion ?? this.playerVersion,
isSkinByDefault: isSkinByDefault ?? this.isSkinByDefault,
fullscreenSettings: fullscreenSettings ?? this.fullscreenSettings,
isPipAvailable: isPipAvailable ?? this.isPipAvailable,
isSettingsAvailable: isSettingsAvailable ?? this.isSettingsAvailable,
skinColor: skinColor ?? this.skinColor,
isAutostart: isAutostart ?? this.isAutostart,
volume: volume ?? this.volume,
brightness: brightness ?? this.brightness,
speed: speed ?? this.speed,
qualityNaming: qualityNaming ?? this.qualityNaming,
quality: quality ?? this.quality,
isSubtitlesAvailable: isSubtitlesAvailable ?? this.isSubtitlesAvailable,
start: start ?? this.start,
isLoop: isLoop ?? this.isLoop,
playlist: playlist ?? this.playlist,
track: track ?? this.track,
chunk: chunk ?? this.chunk,
log: log ?? this.log
);
}
factory SettingsInitialState.createFromRepository(SettingsRepository repository) {
return SettingsInitialState(
playerVersion: "",
isSkinByDefault: repository.isSkinByDefault,
fullscreenSettings: SettingsMapper.fullscreenSetting(repository.fullscreenSettings),
isPipAvailable: repository.isPipAvailable,
isSettingsAvailable: repository.isSettingsAvailable,
skinColor: SettingsMapper.skinColor(repository.skinColor),
isAutostart: repository.isAutostart,
volume: repository.volume,
brightness: repository.brightness,
speed: repository.speed,
qualityNaming: SettingsMapper.qualityNaming(repository.qualityNaming),
quality: SettingsMapper.quality(repository.quality),
isSubtitlesAvailable: repository.isSubtitlesAvailable,
start: repository.start,
isLoop: repository.isLoop,
playlist: repository.playlist,
track: repository.track,
chunk: repository.chunk,
log: SettingsMapper.logType(repository.log)
);
}
static final skinSettings = [
BoolOptionData(
key: const Key('SkinOptionDefaultID'),
title: 'По умолчанию',
isSelected: true,
onChange: (_) => SkinChangedEvent(true)
),
BoolOptionData(
key: const Key('SkinOptionByUserID'),
title: 'Свой',
isSelected: false,
onChange: (_) => SkinChangedEvent(false)
)
];
static final standardSkinSettings = [
OptionDataContainer(
key: const Key('FullscreenID'),
title: 'Полноэкранный режим',
options: const [
OptionData.withValueEqualTitle(Key('SkinStandardFullscreenAlbumID'), 'Альбомный'),
OptionData.withValueEqualTitle(Key('SkinStandardFullscreenFlexID'),'Свободный'),
OptionData.withValueEqualTitle(Key('SkinStandardFullscreenOffID'), "Выкл.")
],
onSelectedOption: (option) => FullscreenChangedEvent(option)
),
BoolOptionData(
key: const Key('SkinStandardPipID'),
title: 'Картинка в картинке',
isSelected: false,
onChange: (isOn) => PipChangedEvent(isOn)
),
BoolOptionData(
key: const Key('SkinStandardSettingsID'),
title: 'Настройки',
isSelected: true,
onChange: (isOn) => SettingsChangedEvent(isOn)
),
OptionDataContainer(
key: const Key('ColorID'),
title: 'Цвет',
options: const [
OptionData(key: Key('SkinStandardColor1ID'), title: '#0D70D1'),
OptionData(key: Key('SkinStandardColor2ID'), title: '#000000'),
OptionData(key: Key('SkinStandardColor3ID'), title: '#FFFFFF')
]
)
];
static final playbackOptions = [
BoolOptionData(
key: const Key('PlaybackOptionAutostartID'),
title: 'Автостарт',
isSelected: false,
onChange: (isOn) => AutostartChangedEvent(isOn)
),
OptionDataContainer(
key: const Key('PlaybackOptionVolumeID'),
title: 'Громкость',
options: const [
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume00ID'), '0.0'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume01ID'), '0.1'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume02ID'), '0.2'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume03ID'), '0.3'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume04ID'), '0.4'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume05ID'), '0.5'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume06ID'), '0.6'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume07ID'), '0.7'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume08ID'), '0.8'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume09ID'), '0.9'),
OptionData.withValueEqualTitle(Key('PlaybackOptionVolume10ID'), '1.0')
],
onSelectedOption: (option) => VolumeChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionBrightnessID'),
title: 'Яркость',
options: const [
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness00ID'), '0.0'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness01ID'), '0.1'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness02ID'), '0.2'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness03ID'), '0.3'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness04ID'), '0.4'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness05ID'), '0.5'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness06ID'), '0.6'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness07ID'), '0.7'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness08ID'), '0.8'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness09ID'), '0.9'),
OptionData.withValueEqualTitle(Key('PlaybackOptionBrightness10ID'), '1.0')
],
onSelectedOption: (option) => BrightnessChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionSpeedID'),
title: 'Скорость',
options: const [
OptionData(key: Key('PlaybackOptionSpeed025xID'), title: '0.25x', value: '0.25'),
OptionData(key: Key('PlaybackOptionSpeed05xID'), title: '0.5x', value: '0.5'),
OptionData(key: Key('PlaybackOptionSpeed075xID'), title: '0.75x', value: '0.75'),
OptionData(key: Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', value: '1.0'),
OptionData(key: Key('PlaybackOptionSpeed125xID'), title: '1.25x', value: '1.25'),
OptionData(key: Key('PlaybackOptionSpeed15xID'), title: '1.5x', value: '1.5'),
OptionData(key: Key('PlaybackOptionSpeed175xID'), title: '1.75x', value: '1.75'),
OptionData(key: Key('PlaybackOptionSpeed2xID'), title: '2x', value: '2.0')
],
onSelectedOption: (option) => SpeedChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionQualityID'),
title: 'Качество',
options: const [
OptionData(key: Key('PlaybackOptionQualityAutoID'), title: 'Авто', value: "auto"),
OptionData(key: Key('PlaybackOptionQuality4kID'), title: '4K', value: "p2160"),
OptionData(key: Key('PlaybackOptionQuality1440pID'), title: '1440p Ultra HD', value: "p1440"),
OptionData(key: Key('PlaybackOptionQuality1080pID'), title: '1080p FHD', value: "p1080"),
OptionData(key: Key('PlaybackOptionQuality720pID'), title: '720p HD', value: "p720"),
OptionData(key: Key('PlaybackOptionQuality480pID'), title: '480p', value: "p480"),
OptionData(key: Key('PlaybackOptionQuality360pID'), title: '360p', value: "p360"),
OptionData(key: Key('PlaybackOptionQuality240pID'), title: '240p', value: "p240"),
OptionData(key: Key('PlaybackOptionQuality144pID'), title: '144p', value: "p144"),
],
onSelectedOption: (option) => QualityChangedEvent(option)
),
OptionDataContainer(
key: const Key('PlaybackOptionQualityNamingID'),
title: 'Название качества',
options: const [
OptionData(key: Key('PlaybackOptionQualityNameCommonID'), title: 'Common', value: "common"),
OptionData(key: Key('PlaybackOptionQualityNameRusID'), title: 'Russian', value: "rus"),
OptionData(key: Key('PlaybackOptionQualityNameEngID'), title: 'English', value: "eng"),
OptionData(key: Key('PlaybackOptionQualityNameResolutionID'), title: 'Resolution', value: "resolution"),
],
onSelectedOption: (option) => QualityNamingChangedEvent(option)
),
BoolOptionData(
key: const Key('PlaybackOptionSubtitlesID'),
title: 'Субтитры',
isSelected: true,
onChange: (isOn) => SubsChangedEvent(isOn)
),
NumericOptionData(
key: const Key('PlaybackOptionStartPositionID'),
title: 'Стартовая позиция',
value: 0,
onChange: (value) => StartPositionChangedEvent(value)
)
];
static final extraOption = BoolOptionData(
key: const Key('ExtraOptionLoopID'),
title: 'Зацикленность',
isSelected: false,
onChange: (isOn) => LoopChangedEvent(isOn)
);
static List<NumericOptionData> timeoutsOptions = [
NumericOptionData(
key: const Key('TimeoutsOptionManifestID'),
title: 'Манифест (мс)',
value: 5000,
onChange: (value) => PlaylistTimeoutsChangedEvent(value)
),
NumericOptionData(
key: const Key('TimeoutsOptionTrackID'),
title: 'Трек (мс)',
value: 3000,
onChange: (value) => TrackTimeoutsChangedEvent(value)
),
NumericOptionData(
key: const Key('TimeoutsOptionChunkID'),
title: 'Сегмент (мс)',
value: 3000,
onChange: (value) => ChunkTimeoutsChangedEvent(value)
)
];
static const logOptions = {
"LogType.off": OptionData(key: Key('LogOptionOffId'), title: 'Выкл.'),
"LogType.info": OptionData(key: Key('LogOptionInfoID'), title: 'Инфо.'),
"LogType.debug": OptionData(key: Key('LogOptionDebugID'), title: 'Отладка')
};
}
@@ -1,40 +1,138 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
import 'package:nut_player_example/src/common/views/checkmark_list_option.dart';
import 'package:nut_player_example/src/common/views/input_view.dart';
import 'package:nut_player_example/src/features/settings_screen/domain/settings_bloc.dart';
import 'package:nut_player_example/src/common/views/options_view.dart';
import 'package:nut_player_example/src/features/settings_screen/presentation/toggle_view.dart';
import 'package:package_info_plus/package_info_plus.dart';
class SettingsView extends StatelessWidget {
enum LogType { off, info, debug }
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@override
State<SettingsView> createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
final List<OptionData> _skinOptions = [
OptionData(key: const Key('SkinOptionDefaultID'), title: 'По умолчанию', isSelected: true),
OptionData(key: const Key('SkinOptionByUserID'), title: 'Свой')
];
final List<OptionData> _standardSkinOptions = [
OptionData(key: const Key('SkinStandardOptionFullscreenID'), title: 'Полноэкранный режим'),
OptionData(key: const Key('SkinStandardOptionPipID'), title: 'Картинка в картинке', isSelected: true),
OptionData(key: const Key('SkinStandardOptionSettingsID'), title: 'Настройки', isSelected: true),
OptionData(key: const Key('SkinStandardOptionColorID'), title: 'Цвет')
];
final List<OptionData> _fullscreen = [
OptionData(key: const Key('SkinStandardFullscreenAlbumID'), title: 'Альбомный', isSelected: true),
OptionData(key: const Key('SkinStandardFullscreenFlexID'), title: 'Гибкий'),
OptionData(key: const Key('SkinStandardFullscreenOffID'), title: 'Выкл.')
];
final List<OptionData> _colors = [
OptionData(key: const Key('SkinStandardColor1ID'), title: '#0D70D1', isSelected: true),
OptionData(key: const Key('SkinStandardColor2ID'), title: '#000000'),
OptionData(key: const Key('SkinStandardColor3ID'), title: '#FFFFFF')
];
final List<OptionData> _playbackOptions = [
OptionData(key: const Key('PlaybackOptionAutostartID'), title: 'Автостарт'),
OptionData(key: const Key('PlaybackOptionVolumeID'), title: 'Громкость'),
OptionData(key: const Key('PlaybackOptionBrightnessID'), title: 'Яркость'),
OptionData(key: const Key('PlaybackOptionSpeedID'), title: 'Скорость'),
OptionData(key: const Key('PlaybackOptionQualityNamingID'), title: 'Название качества'),
OptionData(key: const Key('PlaybackOptionQualityID'), title: 'Качество'),
OptionData(key: const Key('PlaybackOptionSubtitlesID'), title: 'Субтитры', isSelected: true)
];
final NumericOptionData _startPositionOption = NumericOptionData(key: const Key('PlaybackOptionStartPositionID'), title: 'Стартовая позиция', value: 0);
final OptionData _extraOption = OptionData(key: const Key('ExtraOptionLoopID'), title: 'Зацикленность');
LogType _currentLogType = LogType.info;
final List<OptionData> _volumeOptions = [
OptionData(key: const Key('PlaybackOptionVolume00ID'), title: '0.0'),
OptionData(key: const Key('PlaybackOptionVolume01ID'), title: '0.1'),
OptionData(key: const Key('PlaybackOptionVolume02ID'), title: '0.2'),
OptionData(key: const Key('PlaybackOptionVolume03ID'), title: '0.3'),
OptionData(key: const Key('PlaybackOptionVolume04ID'), title: '0.4'),
OptionData(key: const Key('PlaybackOptionVolume05ID'), title: '0.5', isSelected: true),
OptionData(key: const Key('PlaybackOptionVolume06ID'), title: '0.6'),
OptionData(key: const Key('PlaybackOptionVolume07ID'), title: '0.7'),
OptionData(key: const Key('PlaybackOptionVolume08ID'), title: '0.8'),
OptionData(key: const Key('PlaybackOptionVolume09ID'), title: '0.9'),
OptionData(key: const Key('PlaybackOptionVolume10ID'), title: '1.0')
];
final List<OptionData> _brightnessOptions = [
OptionData(key: const Key('PlaybackOptionBrightness00ID'), title: '0.0'),
OptionData(key: const Key('PlaybackOptionBrightness01ID'), title: '0.1'),
OptionData(key: const Key('PlaybackOptionBrightness02ID'), title: '0.2'),
OptionData(key: const Key('PlaybackOptionBrightness03ID'), title: '0.3'),
OptionData(key: const Key('PlaybackOptionBrightness04ID'), title: '0.4'),
OptionData(key: const Key('PlaybackOptionBrightness05ID'), title: '0.5', isSelected: true),
OptionData(key: const Key('PlaybackOptionBrightness06ID'), title: '0.6'),
OptionData(key: const Key('PlaybackOptionBrightness07ID'), title: '0.7'),
OptionData(key: const Key('PlaybackOptionBrightness08ID'), title: '0.8'),
OptionData(key: const Key('PlaybackOptionBrightness09ID'), title: '0.9'),
OptionData(key: const Key('PlaybackOptionBrightness10ID'), title: '1.0')
];
final List<OptionData> _speedOptions = [
OptionData(key: const Key('PlaybackOptionSpeed025xID'), title: '0.25x'),
OptionData(key: const Key('PlaybackOptionSpeed05xID'), title: '0.5x'),
OptionData(key: const Key('PlaybackOptionSpeed075xID'), title: '0.75x'),
OptionData(key: const Key('PlaybackOptionSpeedNormalID'), title: 'Обычная', isSelected: true),
OptionData(key: const Key('PlaybackOptionSpeed125xID'), title: '1.25x'),
OptionData(key: const Key('PlaybackOptionSpeed15xID'), title: '1.5x'),
OptionData(key: const Key('PlaybackOptionSpeed175xID'), title: '1.75x'),
OptionData(key: const Key('PlaybackOptionSpeed2xID'), title: '2x'),
];
final List<OptionData> _qualityOptions = [
OptionData(key: const Key('PlaybackOptionQualityAutoID'), title: 'Авто', isSelected: true),
OptionData(key: const Key('PlaybackOptionQuality4kID'), title: '4K'),
OptionData(key: const Key('PlaybackOptionQuality1440pID'), title: '1440p Ultra HD'),
OptionData(key: const Key('PlaybackOptionQuality1080pID'), title: '1080p FHD'),
OptionData(key: const Key('PlaybackOptionQuality720pID'), title: '720p HD'),
OptionData(key: const Key('PlaybackOptionQuality480pID'), title: '480p'),
OptionData(key: const Key('PlaybackOptionQuality360pID'), title: '360p'),
OptionData(key: const Key('PlaybackOptionQuality240pID'), title: '240p'),
OptionData(key: const Key('PlaybackOptionQuality144pID'), title: '144p')
];
final List<OptionData> _qualityNamingOptions = [
OptionData(key: const Key('PlaybackOptionQualityNameCommonID'), title: 'Common', isSelected: true),
OptionData(key: const Key('PlaybackOptionQualityNameRusID'), title: 'Russian'),
OptionData(key: const Key('PlaybackOptionQualityNameEngID'), title: 'English'),
OptionData(key: const Key('PlaybackOptionQualityNameResolutionID'), title: 'Resolution')
];
final List<NumericOptionData> _timeoutsOptions = [
NumericOptionData(key: const Key('TimeoutsOptionManifestID'), title: 'Манифест (мс)', value: 5000),
NumericOptionData(key: const Key('TimeoutsOptionTrackID'), title: 'Трек (мс)', value: 3000),
NumericOptionData(key: const Key('TimeoutsOptionChunkID'), title: 'Сегмент (мс)', value: 3000)
];
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
navigationBar: CupertinoNavigationBar(
key: const Key('SettingsAppBarID'),
middle: const Text('Настройки', style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.black, fontWeight: FontWeight.w600)),
leading: CupertinoNavigationBarBackButton(
previousPageTitle: 'Назад',
onPressed: () {
final bloc = context.read<SettingsBloc>();
bloc.add(DismissEvent());
}
),
padding: const EdgeInsetsDirectional.all(0),
navigationBar: const CupertinoNavigationBar(
key: Key('SettingsAppBarID'),
middle: Text('Настройки', style: TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w600)),
leading: CupertinoNavigationBarBackButton(previousPageTitle: 'Назад'),
padding: EdgeInsetsDirectional.all(0),
backgroundColor: CupertinoColors.white,
),
child: SafeArea(
child: SingleChildScrollView(
child: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
final bloc = context.read<SettingsBloc>();
return Column(
child: Column(
children: [
// НАСТРОЙКИ СКИНА
CupertinoListSection.insetGrouped(
@@ -42,10 +140,14 @@ class SettingsView extends StatelessWidget {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Выбор скина'.toUpperCase(),
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: _buildSkinSettings(bloc)
children: <Widget>[..._skinOptions.map((value) {
return CheckmarkListOption(
value, () {},
);
}).toList()]
),
// НАСТРОЙКИ СТАНДАРТНОГО СКИНА
@@ -54,10 +156,18 @@ class SettingsView extends StatelessWidget {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Настройки стандартного скина'.toUpperCase(),
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: _buildSettingsWidgets(SettingsInitialState.standardSkinSettings, bloc)
children: <Widget>[..._standardSkinOptions.map((value) {
if (value.key == const Key('SkinStandardOptionFullscreenID')) {
return OptionsView(value.title, _fullscreen);
} else if (value.key == const Key('SkinStandardOptionColorID')) {
return OptionsView(value.title, _colors);
} else {
return ToggleView(value);
}
}).toList()]
),
// ВОСПРОИЗВЕДЕНИЕ
@@ -66,10 +176,28 @@ class SettingsView extends StatelessWidget {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Воспроизведение'.toUpperCase(),
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: _buildSettingsWidgets(SettingsInitialState.playbackOptions, bloc)
children: <Widget>[..._playbackOptions.map((value) {
if (value.key == const Key('PlaybackOptionVolumeID')) {
return OptionsView(value.title, _volumeOptions);
} else if (value.key == const Key('PlaybackOptionBrightnessID')) {
return OptionsView(value.title, _brightnessOptions);
} else if (value.key == const Key('PlaybackOptionSpeedID')) {
return OptionsView(value.title, _speedOptions);
} else if (value.key == const Key('PlaybackOptionQualityID')) {
return OptionsView(value.title, _qualityOptions);
} else if (value.key == const Key('PlaybackOptionQualityNamingID')) {
return OptionsView(value.title, _qualityNamingOptions);
} else if (value.key == const Key('PlaybackOptionStartPositionID')) {
return ToggleView(value);
} else {
return ToggleView(value);
}
}).toList()
+ [InputView(_startPositionOption)]
]
),
// ДОПОЛНИТЕЛЬНО
@@ -78,16 +206,16 @@ class SettingsView extends StatelessWidget {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Дополнительно'.toUpperCase(),
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
footer: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: const Text('Доступно только для видео в формате mp4, длительностью не более 5 мин.',
style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 12, fontWeight: FontWeight.w400)
style: TextStyle(color: CupertinoColors.systemGrey, fontSize: 12, fontWeight: FontWeight.w400)
),
),
children: _buildSettingsWidgets([SettingsInitialState.extraOption], bloc)
children: [ToggleView(_extraOption)]
),
// ЗАДЕРЖКИ
@@ -96,10 +224,12 @@ class SettingsView extends StatelessWidget {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Настройка задержек (только для HLS)'.toUpperCase(),
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: _buildSettingsWidgets(SettingsInitialState.timeoutsOptions, bloc)
children: <Widget>[..._timeoutsOptions.map((value) {
return InputView(value);
}).toList()]
),
// Логирование
@@ -108,7 +238,7 @@ class SettingsView extends StatelessWidget {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Логирование'.toUpperCase(),
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: [
@@ -118,7 +248,49 @@ class SettingsView extends StatelessWidget {
color: CupertinoColors.systemGrey3,
border: Border.all(color: CupertinoColors.systemGrey3)
),
child: _buildLog(context)
child: CupertinoSlidingSegmentedControl<LogType>(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
groupValue: _currentLogType,
onValueChanged: (LogType? value) {
if (value != null) {
setState(() {
_currentLogType = value;
});
}
},
children: const <LogType, Widget>{
LogType.off: Padding(
key: Key('LogOffSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Выкл.',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
),
LogType.info: Padding(
key: Key('LogInfoSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Инфо',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
),
LogType.debug: Padding(
key: Key('LogDebugSegmentElementID'),
padding: EdgeInsets.symmetric(vertical: 6),
child: Text('Отладка',
style: TextStyle(
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
)
},
)
),
]
),
@@ -129,7 +301,7 @@ class SettingsView extends StatelessWidget {
header: Container(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
child: Text('Firebase'.toUpperCase(),
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
),
),
children: [
@@ -139,8 +311,9 @@ class SettingsView extends StatelessWidget {
alignment: Alignment.centerLeft,
borderRadius: const BorderRadius.all(Radius.circular(10)),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: const Text('Проверка падения приложения', style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.black, fontSize: 17, fontWeight: FontWeight.w400)),
onPressed: () { bloc.add(CrashEvent()); }
child: const Text('Проверка падения приложения',
style: TextStyle(color: CupertinoColors.black, fontSize: 17, fontWeight: FontWeight.w400)),
onPressed: (){}
)
)
]
@@ -149,145 +322,13 @@ class SettingsView extends StatelessWidget {
// ВЕРСИЯ
Container(
margin: const EdgeInsets.only(top: 20),
child: FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
return _buildVersion(snapshot, bloc.state);
})
child: const Text('VERSION 1.2.0 (1063)',
style: TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400))
)
]
);
},
)
)
)
)
);
}
Widget _buildVersion(AsyncSnapshot<PackageInfo> packageInfo, SettingsState state) {
const textStyle = TextStyle(
decoration: TextDecoration.none,
color: CupertinoColors.systemGrey,
fontSize: 13,
fontWeight: FontWeight.w400);
if (packageInfo.hasData) {
var versions = [Text('ВЕРСИЯ ПРИЛОЖЕНИЯ ${packageInfo.requireData.version}', style: textStyle)];
final settingsState = state;
if (settingsState is! SettingsInitialState) { return Column(children: versions); }
versions.add(Text('ВЕРСИЯ NUT.PLAYER ${settingsState.playerVersion}', style: textStyle));
return Column(children: versions);
} else if (packageInfo.hasError) {
return Text(packageInfo.error.toString(), style: textStyle);
} else {
return _buildLoader();
}
}
Widget _buildLoader() {
return Container(
margin: const EdgeInsets.all(10),
width: double.infinity,
height: 80,
child: const CupertinoActivityIndicator(radius: 20),
);
}
List<Widget> _buildSkinSettings(SettingsBloc bloc) {
return <Widget>[...SettingsInitialState.skinSettings.map((setting) {
final isSelected = bloc.isSelected(setting);
return CheckmarkListOption(
setting.title,
isSelected, () {
final event = setting.onChange?.call(!isSelected);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
}).toList()];
}
List<Widget> _buildSettingsWidgets(List<Object> objects, SettingsBloc bloc) {
return <Widget>[...objects.map((setting) {
if (setting is OptionDataContainer) {
final selectedIndex = bloc.selectedIndex(setting);
return OptionsView(
setting.title,
setting.options,
selectedIndex,
(selectedIndex) {
final selectedOption = setting.options[selectedIndex];
final event = setting.onSelectedOption?.call(selectedOption);
if (event != null) {
bloc.add(event);
}
},
key: setting.key,
);
} else if (setting is BoolOptionData) {
final isSelected = bloc.isSelected(setting);
return ToggleView(
title: setting.title,
isSelected: isSelected,
onChange: (isOn) {
final event = setting.onChange?.call(isOn);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
} else if (setting is NumericOptionData) {
final currentValue = bloc.currentNumericValue(setting);
return InputView(
setting.title,
currentValue,
(newTime) {
final event = setting.onChange?.call(newTime);
if (event != null) {
bloc.add(event);
}
}, key: setting.key,
);
} else {
return const Text('not implemented');
}
}).toList()];
}
Widget? _buildLog(BuildContext context) {
final bloc = context.read<SettingsBloc>();
final settingsState = bloc.state;
if (settingsState is! SettingsInitialState) { return null; }
return CupertinoSlidingSegmentedControl<LogType>(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
groupValue: settingsState.log,
onValueChanged: (LogType? value) {
if (value != null) {
bloc.add(LogTypeChangedEvent(value));
}
},
children: <LogType, Widget>{
LogType.off: _buildLogSegment(SettingsInitialState.logOptions[LogType.off.toString()]!),
LogType.info: _buildLogSegment(SettingsInitialState.logOptions[LogType.info.toString()]!),
LogType.debug: _buildLogSegment(SettingsInitialState.logOptions[LogType.debug.toString()]!),
},
);
}
Padding _buildLogSegment(OptionData data) {
return Padding(
key: data.key,
padding: const EdgeInsets.symmetric(vertical: 6),
child: Text(data.title,
style: const TextStyle(
decoration: TextDecoration.none,
fontSize: 16,
color: CupertinoColors.black,
fontWeight: FontWeight.normal)
),
);
}
}
@@ -1,24 +1,31 @@
import 'package:flutter/cupertino.dart';
import 'package:nut_player_example/src/common/models/option_data.dart';
class ToggleView extends StatelessWidget {
final String title;
final bool isSelected;
final Function(bool)? onChange;
class ToggleView extends StatefulWidget {
final OptionData data;
const ToggleView({super.key, required this.title, required this.isSelected, this.onChange});
const ToggleView(this.data, {super.key});
@override
State<ToggleView> createState() => _ToggleViewState();
}
class _ToggleViewState extends State<ToggleView> {
@override
Widget build(BuildContext context) {
return CupertinoListTile(
key: key,
key: widget.data.key,
trailing: CupertinoSwitch(
value: isSelected,
value: widget.data.isSelected,
activeColor: CupertinoColors.activeGreen,
onChanged: (bool value) {
onChange?.call(value);
setState(() {
widget.data.isSelected = value;
});
},
),
title: Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
title: Text(widget.data.title, style: const TextStyle(fontSize: 17))
);
}
}
+17 -245
View File
@@ -1,14 +1,6 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: d84d98f1992976775f83083523a34c5d22fea191eec3abb2bd09537fb623c2e0
url: "https://pub.dev"
source: hosted
version: "1.3.7"
async:
dependency: transitive
description:
@@ -17,14 +9,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
url: "https://pub.dev"
source: hosted
version: "8.1.2"
boolean_selector:
dependency: transitive
description:
@@ -53,10 +37,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.2"
cupertino_icons:
dependency: "direct main"
description:
@@ -81,83 +65,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.4"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "95580fa07c8ca3072a2bb1fecd792616a33f8683477d25b7d29d3a6a399e6ece"
url: "https://pub.dev"
source: hosted
version: "2.17.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2
url: "https://pub.dev"
source: hosted
version: "4.8.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: e8c408923cd3a25bd342c576a114f2126769cd1a57106a4edeaa67ea4a84e962
url: "https://pub.dev"
source: hosted
version: "2.8.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "833cf891d10e5e819a2034048ff7e8882bcc0b51055c0e17f5fe3f3c3c177a9d"
url: "https://pub.dev"
source: hosted
version: "3.3.7"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: dfdf1172f35fc0b0132bc5ec815aed52c07643ee56732e6807ca7dc12f7fce86
url: "https://pub.dev"
source: hosted
version: "3.6.7"
firebase_storage:
dependency: "direct main"
description:
name: firebase_storage
sha256: "4ceb092cd14c3bce0dc8cd4046754cd1e5e5c1977155e286b512b3f84fe1c03e"
url: "https://pub.dev"
source: hosted
version: "11.2.8"
firebase_storage_platform_interface:
dependency: transitive
description:
name: firebase_storage_platform_interface
sha256: "88f8b8bb7181eef125536297f11b28171f59d2f53a59610e3966c2e38be3de4c"
url: "https://pub.dev"
source: hosted
version: "4.4.7"
firebase_storage_web:
dependency: transitive
description:
name: firebase_storage_web
sha256: bd05589cf17a8d5e2a90bdffcdab434c97e27bc37ca0056bfa90accf0366a7b0
url: "https://pub.dev"
source: hosted
version: "3.6.8"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
url: "https://pub.dev"
source: hosted
version: "8.1.3"
flutter_driver:
dependency: transitive
description: flutter
@@ -176,53 +88,16 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
keyboard_actions:
dependency: "direct main"
description:
name: keyboard_actions
sha256: "31e0ab2a706ac8f58887efa60efc1f19aecdf37d8ab0f665a0f156d1fbeab650"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
lints:
dependency: transitive
description:
@@ -251,18 +126,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.9.1"
nut_player:
dependency: "direct main"
description:
@@ -270,13 +137,6 @@ packages:
relative: true
source: path
version: "0.0.1"
nut_player_android:
dependency: transitive
description:
path: "../../nut_player_android"
relative: true
source: path
version: "0.0.1"
nut_player_ios:
dependency: transitive
description:
@@ -291,22 +151,6 @@ packages:
relative: true
source: path
version: "0.0.1"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
path:
dependency: transitive
description:
@@ -319,10 +163,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.0"
plugin_platform_interface:
dependency: transitive
description:
@@ -339,62 +183,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.4"
provider:
dependency: transitive
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
url: "https://pub.dev"
source: hosted
version: "6.0.5"
screen_brightness:
dependency: "direct main"
description:
name: screen_brightness
sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
screen_brightness_android:
dependency: transitive
description:
name: screen_brightness_android
sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf"
url: "https://pub.dev"
source: hosted
version: "0.1.0+2"
screen_brightness_ios:
dependency: transitive
description:
name: screen_brightness_ios
sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_macos:
dependency: transitive
description:
name: screen_brightness_macos
sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
screen_brightness_platform_interface:
dependency: transitive
description:
name: screen_brightness_platform_interface
sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_windows:
dependency: transitive
description:
name: screen_brightness_windows
sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
sky_engine:
dependency: transitive
description: flutter
@@ -412,18 +200,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.11.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@@ -452,18 +240,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "0.6.0"
vector_math:
dependency: transitive
description:
@@ -476,18 +256,18 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f
url: "https://pub.dev"
source: hosted
version: "11.10.0"
version: "11.7.1"
web:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.1.4-beta"
webdriver:
dependency: transitive
description:
@@ -496,14 +276,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
win32:
dependency: transitive
description:
name: win32
sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
sdks:
dart: ">=3.2.0-194.0.dev <4.0.0"
dart: ">=3.1.2 <4.0.0"
flutter: ">=3.3.0"
-8
View File
@@ -16,10 +16,6 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.1
package_info_plus: ^4.1.0
keyboard_actions: ^4.2.0
screen_brightness: ^0.2.2
nut_player:
# When depending on this package from a real application you should use:
@@ -32,9 +28,6 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
firebase_core: ^2.17.0
firebase_storage: ^11.2.8
firebase_crashlytics: ^3.3.7
dev_dependencies:
integration_test:
@@ -62,7 +55,6 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
- assets/skinIcons/
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
+3 -13
View File
@@ -1,15 +1,5 @@
export 'src/legacy/closed_caption_file.dart';
export 'src/legacy/video_player_value.dart';
export 'src/legacy/video_player_controller.dart';
export 'src/legacy/video_scrubber.dart';
export 'src/legacy/video_progress_indicator.dart';
// new one
export 'src/widget/video_player.dart';
export 'src/controller/video_player_controller.dart';
export 'src/player_version_observer.dart';
//
export 'src/provider/provider.dart';
export 'src/provider/common_provider.dart';
export 'src/provider/json_provider/json_provider.dart';
export 'src/model/content_type.dart';
export 'src/model/player_content.dart';
export 'src/model/player_statistic_record.dart';
export 'src/model/player_subtitle_record.dart';
export 'src/legacy/video_progress_indicator.dart';
@@ -1,423 +0,0 @@
import 'package:flutter/services.dart';
import 'package:nut_player/src/provider/common_provider.dart';
import 'package:nut_player/src/provider/provider.dart';
import '../legacy/video_player_value.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart'
show PlayerId, VideoEvent, VideoEventType, PlatformHTTPMethod, PlatformSubtitleType;
import '../model/content_type.dart';
import '../model/platform_player_content_impl.dart';
import '../model/player_statistic_record.dart';
import '../model/player_subtitle_record.dart';
import '../platform/nut_player_platform.dart';
class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
// MARK: - Private properties
Provider _provider;
PlayerId? _playerId;
Timer? _timer;
bool _isDisposed = false;
bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized;
Completer<void>? _creationHandler;
_VideoAppLifeCycleObserver? _lifeCycleObserver;
StreamSubscription<VideoEvent>? _eventSubscription;
// MARK: - Constructors
// Private constructor
VideoPlayerController._(this._provider, super.value);
@override
Future<void> dispose() async {
if (_isDisposed) {
return;
}
final playerId = _playerId;
if (playerId != null) {
final completionHandler = _creationHandler;
if (completionHandler != null) {
await completionHandler.future;
}
_timer?.cancel();
_timer = null;
await _eventSubscription?.cancel();
_eventSubscription = null;
await nutPlayerPlatform.dispose(playerId);
_lifeCycleObserver?.dispose();
_lifeCycleObserver = null;
}
_isDisposed = true;
super.dispose();
}
// MARK: - Factories
factory VideoPlayerController.network(String urlPath, {bool isLive = false}) {
return VideoPlayerController._(
CommonProvider.url(urlPath, isLive: isLive),
const VideoPlayerValue(duration: Duration.zero)
);
}
factory VideoPlayerController.content({
required ContentType content,
List<PlayerStatisticRecord> statistics = const [],
List<PlayerSubtitleRecord> subtitles = const []
}) {
return VideoPlayerController._(CommonProvider.content(
content: content, statistics: statistics, subtitles: subtitles),
const VideoPlayerValue(duration: Duration.zero)
);
}
factory VideoPlayerController.provider(Provider provider) {
return VideoPlayerController._(provider, const VideoPlayerValue(duration: Duration.zero));
}
// MARK: - Public properties
PlayerId? get playerId => _playerId;
Stream<String> get logStream => nutPlayerPlatform.logStream();
Stream<Object> get skinActionStream => nutPlayerPlatform.skinActionStream();
/// The position in the current video.
Future<Duration?> get position async {
final playerId = _playerId;
if (_isDisposed || playerId == null) {
return null;
}
return nutPlayerPlatform.getPosition(playerId);
}
// MARK: - Public methods
@override
void removeListener(VoidCallback listener) {
// Prevent VideoPlayer from causing an exception to be thrown when attempting to
// remove its own listener after the controller has already been disposed.
if (!_isDisposed) {
super.removeListener(listener);
}
}
Future<void> initialize({Map<String, Object>? params}) async {
final bool allowBackgroundPlayback;
if (params != null && params["enablePip"] is bool) {
allowBackgroundPlayback = params["enablePip"] as bool;
} else {
allowBackgroundPlayback = false;
}
if (!allowBackgroundPlayback && _lifeCycleObserver == null) {
_lifeCycleObserver = _VideoAppLifeCycleObserver(this);
_lifeCycleObserver?.initialize();
}
final playerContent = await _provider.retrieveContent();
final platformContent = PlatformPlayerContentImpl(
content: createPlatformContent(playerContent.content),
statistics: playerContent.statistics.map((record) => PlatformStatisticRecordImpl(
name: record.name,
urlTemplate: record.urlTemplate,
start: record.start,
delay: record.delay,
count: record.count,
method: record.method == HTTPMethod.get ? PlatformHTTPMethod.get : PlatformHTTPMethod.post,
body: record.body
)).toList(),
subtitles: playerContent.subtitles.map((record) => PlatformSubtitleRecordImpl(
title: record.title,
type: PlatformSubtitleType.srt,
url: record.url,
language: record.language,
)).toList()
);
final playerId = await nutPlayerPlatform.create(content: platformContent, params: params);
_playerId = playerId;
_creationHandler?.complete(null);
_creationHandler = null;
final Completer<void> initializingCompleter = Completer<void>();
void eventListener(VideoEvent event) {
if (_isDisposed) {
return;
}
switch (event.eventType) {
case VideoEventType.initialized:
value = value.copyWith(
isInitialized: true,
isCompleted: false,
);
initializingCompleter.complete(null);
break;
case VideoEventType.ready:
value = value.copyWith(
duration: event.duration,
size: event.size,
subtitles: event.subtitles,
rotationCorrection: event.rotationCorrection,
errorDescription: null,
);
_applyVolume();
break;
case VideoEventType.completed:
pause().then((void pauseResult) => seek(value.duration));
value = value.copyWith(isCompleted: true);
break;
case VideoEventType.bufferingUpdate:
value = value.copyWith(buffered: event.buffered);
break;
case VideoEventType.bufferingStart:
value = value.copyWith(isBuffering: true);
break;
case VideoEventType.bufferingEnd:
value = value.copyWith(isBuffering: false);
break;
case VideoEventType.isPlayingStateUpdate:
if (event.isPlaying ?? false) {
value = value.copyWith(isPlaying: event.isPlaying, isCompleted: false);
} else {
value = value.copyWith(isPlaying: event.isPlaying);
}
break;
case VideoEventType.didFetchQualities:
value = value.copyWith(qualities: event.qualities);
break;
case VideoEventType.unknown:
break;
}
}
void errorListener(Object obj) {
final PlatformException e = obj as PlatformException;
value = VideoPlayerValue.erroneous(e.message!);
_timer?.cancel();
if (!initializingCompleter.isCompleted) {
initializingCompleter.completeError(obj);
}
}
_eventSubscription = nutPlayerPlatform
.videoEventsFor(playerId)
.listen(eventListener, onError: errorListener);
return initializingCompleter.future;
}
/// Play video.
Future<void> play() async {
if (value.position == value.duration) {
await seek(Duration.zero);
}
value = value.copyWith(isPlaying: true);
await _applyPlayPause();
}
/// Pauses the video.
Future<void> pause() async {
value = value.copyWith(isPlaying: false);
await _applyPlayPause();
}
/// Завершение видео.
Future<void> end() async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
await nutPlayerPlatform.end(playerId);
}
/// Seek video.
Future<void> seek(Duration position) async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
if (position > value.duration) {
position = value.duration;
} else if (position < Duration.zero) {
position = Duration.zero;
}
await nutPlayerPlatform.seek(playerId, position);
_updatePosition(position);
}
Future<void> setVolume(double volume) async {
value = value.copyWith(volume: volume.clamp(0.0, 1.0));
await _applyVolume();
}
/// Sets subtitle by the id.
Future<void> setSubtitles(String subtitleID) async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
await nutPlayerPlatform.setSubtitle(playerId, subtitleID);
}
// Установить качество по идентификатору
Future<void> setQuality(String qualityID) async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
await nutPlayerPlatform.setQuality(playerId, qualityID);
}
Future<void> setLog(String limitOutputLevel) async {
await nutPlayerPlatform.setLimitOutputLevel(limitOutputLevel);
}
Future<void> setBrightness(double brightness) async {
await ScreenBrightness().setScreenBrightness(brightness);
}
Future<void> setPlaybackSpeed(double speed) async {
if (speed < 0) {
throw ArgumentError.value(
speed,
'Negative playback speeds are generally unsupported.',
);
} else if (speed == 0) {
throw ArgumentError.value(
speed,
'Zero playback speed is generally unsupported. Consider using [pause].',
);
}
value = value.copyWith(playbackSpeed: speed);
await _applyPlaybackSpeed();
}
// MARK: - Private methods
Future<void> _applyPlayPause() async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
if (value.isPlaying) {
await nutPlayerPlatform.play(playerId);
// Cancel previous timer.
_timer?.cancel();
_timer = Timer.periodic(
const Duration(milliseconds: 500),
(Timer timer) async {
if (_isDisposed) {
return;
}
final Duration? newPosition = await position;
if (newPosition == null) {
return;
}
_updatePosition(newPosition);
},
);
// This ensures that the correct playback speed is always applied when
// playing back. This is necessary because we do not set playback speed
// when paused.
await _applyPlaybackSpeed();
} else {
_timer?.cancel();
await nutPlayerPlatform.pause(playerId);
}
}
Future<void> _applyPlaybackSpeed() async {
final playerId = _playerId;
if (_isDisposedOrNotInitialized || playerId == null) {
return;
}
// Setting the playback speed on iOS will trigger the video to play. We
// prevent this from happening by not applying the playback speed until
// the video is manually played from Flutter.
if (!value.isPlaying) {
return;
}
await nutPlayerPlatform.setPlaybackSpeed(playerId, value.playbackSpeed);
}
Future<void> _applyVolume() async {
final playerId = _playerId;
final volume = value.volume;
if (_isDisposedOrNotInitialized ||
playerId == null ||
volume == null) {
return;
}
await nutPlayerPlatform.setVolume(playerId, volume);
}
void _updatePosition(Duration position) {
value = value.copyWith(
position: position,
isCompleted: position == value.duration,
);
}
}
class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver {
_VideoAppLifeCycleObserver(this._controller);
bool _wasPlayingBeforePause = false;
final VideoPlayerController _controller;
void initialize() {
final value = _ambiguate(WidgetsBinding.instance);
if (value != null) {
value.addObserver(this);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_wasPlayingBeforePause = _controller.value.isPlaying;
_controller.pause();
} else if (state == AppLifecycleState.resumed) {
if (_wasPlayingBeforePause) {
_controller.play();
}
}
}
void dispose() {
final value = _ambiguate(WidgetsBinding.instance);
if (value != null) {
value.removeObserver(this);
}
}
}
T? _ambiguate<T>(T? value) => value;
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
/// Widget for displaying closed captions on top of a video.
///
/// If [text] is null, this widget will not display anything.
///
/// If [textStyle] is supplied, it will be used to style the text in the closed
/// caption.
///
/// Note: in order to have closed captions, you need to specify a
/// [VideoPlayerController.closedCaptionFile].
///
/// Usage:
///
/// ```dart
/// Stack(children: <Widget>[
/// VideoPlayer(_controller),
/// ClosedCaption(text: _controller.value.caption.text),
/// ]),
/// ```
class ClosedCaption extends StatelessWidget {
/// Creates a a new closed caption, designed to be used with
/// [VideoPlayerValue.caption].
///
/// If [text] is null or empty, nothing will be displayed.
const ClosedCaption({super.key, this.text, this.textStyle});
/// The text that will be shown in the closed caption, or null if no caption
/// should be shown.
/// If the text is empty the caption will not be shown.
final String? text;
/// Specifies how the text in the closed caption should look.
///
/// If null, defaults to [DefaultTextStyle.of(context).style] with size 36
/// font colored white.
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
final String? text = this.text;
if (text == null || text.isEmpty) {
return const SizedBox.shrink();
}
final TextStyle effectiveTextStyle = textStyle ??
DefaultTextStyle.of(context).style.copyWith(
fontSize: 36.0,
color: Colors.white,
);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xB8000000),
borderRadius: BorderRadius.circular(2.0),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0),
child: Text(text, style: effectiveTextStyle),
),
),
),
);
}
}
@@ -0,0 +1,86 @@
import 'package:flutter/foundation.dart' show immutable, objectRuntimeType;
/// A structured representation of a parsed closed caption file.
///
/// A closed caption file includes a list of captions, each with a start and end
/// time for when the given closed caption should be displayed.
///
/// The [captions] are a list of all captions in a file, in the order that they
/// appeared in the file.
///
/// See:
/// * [SubRipCaptionFile].
/// * [WebVTTCaptionFile].
abstract class ClosedCaptionFile {
/// The full list of captions from a given file.
///
/// The [captions] will be in the order that they appear in the given file.
List<Caption> get captions;
}
/// A representation of a single caption.
///
/// A typical closed captioning file will include several [Caption]s, each
/// linked to a start and end time.
@immutable
class Caption {
/// Creates a new [Caption] object.
///
/// This is not recommended for direct use unless you are writing a parser for
/// a new closed captioning file type.
const Caption({
required this.number,
required this.start,
required this.end,
required this.text,
});
/// The number that this caption was assigned.
final int number;
/// When in the given video should this [Caption] begin displaying.
final Duration start;
/// When in the given video should this [Caption] be dismissed.
final Duration end;
/// The actual text that should appear on screen to be read between [start]
/// and [end].
final String text;
/// A no caption object. This is a caption with [start] and [end] durations of zero,
/// and an empty [text] string.
static const Caption none = Caption(
number: 0,
start: Duration.zero,
end: Duration.zero,
text: '',
);
@override
String toString() {
return '${objectRuntimeType(this, 'Caption')}('
'number: $number, '
'start: $start, '
'end: $end, '
'text: $text)';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Caption &&
runtimeType == other.runtimeType &&
number == other.number &&
start == other.start &&
end == other.end &&
text == other.text;
@override
int get hashCode => Object.hash(
number,
start,
end,
text,
);
}
@@ -1,11 +1,522 @@
/*
class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:io';
import 'dart:async';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart'
show DataSource, NutPlayerPlatform, PlayerId, DataSourceType, VideoFormat, VideoEvent, VideoEventType;
import 'dart:math' as math;
import 'video_player_value.dart';
import 'closed_caption_file.dart';
NutPlayerPlatform? _lastNutPlayerPlatform;
NutPlayerPlatform get _nutPlayerPlatform {
final NutPlayerPlatform currentInstance = NutPlayerPlatform.instance;
if (_lastNutPlayerPlatform != currentInstance) {
// This will clear all open videos on the platform when a full restart is
// performed.
currentInstance.init();
_lastNutPlayerPlatform = currentInstance;
}
return currentInstance;
}
/// Widget that displays the video controlled by [controller].
class VideoPlayer extends StatefulWidget {
/// Uses the given [controller] for all video rendered in this widget.
const VideoPlayer(this.controller, {super.key});
/// The [VideoPlayerController] responsible for the video being rendered in
/// this widget.
final VideoPlayerController controller;
@override
State<VideoPlayer> createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
_VideoPlayerState() {
_listener = () {
final int newPlayerId = widget.controller.playerId;
if (newPlayerId != _playerId) {
setState(() {
_playerId = newPlayerId;
});
}
};
}
late VoidCallback _listener;
late PlayerId _playerId;
@override
void initState() {
super.initState();
_playerId = widget.controller.playerId;
// Need to listen for initialization events since the actual texture ID
// becomes available after asynchronous initialization finishes.
widget.controller.addListener(_listener);
}
@override
void didUpdateWidget(VideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.controller.removeListener(_listener);
_playerId = widget.controller.playerId;
widget.controller.addListener(_listener);
}
@override
void deactivate() {
super.deactivate();
widget.controller.removeListener(_listener);
}
@override
Widget build(BuildContext context) {
return _playerId == VideoPlayerController.kUninitializedPlayerId
? Container()
: _VideoPlayerWithRotation(
rotation: widget.controller.value.rotationCorrection,
child: _nutPlayerPlatform.buildView(_playerId),
);
}
}
class _VideoPlayerWithRotation extends StatelessWidget {
const _VideoPlayerWithRotation({required this.rotation, required this.child});
final int rotation;
final Widget child;
@override
Widget build(BuildContext context) => rotation == 0
? child
: Transform.rotate(
angle: rotation * math.pi / 180,
child: child,
);
}
/// Controls a platform video player, and provides updates when the state is
/// changing.
///
/// Instances must be initialized with initialize.
///
/// The video is displayed in a Flutter app by creating a [VideoPlayer] widget.
///
/// To reclaim the resources used by the player call [dispose].
///
/// After [dispose] all further calls are ignored.
class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
/// Constructs a [VideoPlayerController] playing a video from an asset.
///
/// The name of the asset is given by the [dataSource] argument and must not be
/// null. The [package] argument must be non-null when the asset comes from a
/// package and null otherwise.
VideoPlayerController.asset(this.dataSource,
{this.package,
Future<ClosedCaptionFile>? closedCaptionFile})
: _closedCaptionFileFuture = closedCaptionFile,
dataSourceType = DataSourceType.asset,
format = null,
httpHeaders = const <String, String>{},
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a network video.
///
/// The URI for the video is given by the [dataSource] argument.
///
/// **Android only**: The [formatHint] option allows the caller to override
/// the video format detection code.
///
/// [httpHeaders] option allows to specify HTTP headers
/// for the request to the [dataSource].
VideoPlayerController.network(
this.dataSource, {
this.format,
Future<ClosedCaptionFile>? closedCaptionFile,
this.httpHeaders = const <String, String>{},
}) : _closedCaptionFileFuture = closedCaptionFile,
dataSourceType = DataSourceType.network,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a network video.
///
/// The URI for the video is given by the [dataSource] argument.
///
/// **Android only**: The [formatHint] option allows the caller to override
/// the video format detection code.
///
/// [httpHeaders] option allows to specify HTTP headers
/// for the request to the [dataSource].
VideoPlayerController.networkUrl(
Uri url, {
this.format,
Future<ClosedCaptionFile>? closedCaptionFile,
this.httpHeaders = const <String, String>{},
}) : _closedCaptionFileFuture = closedCaptionFile,
dataSource = url.toString(),
dataSourceType = DataSourceType.network,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a video from a file.
///
/// This will load the file from a file:// URI constructed from [file]'s path.
/// [httpHeaders] option allows to specify HTTP headers, mainly used for hls files like (m3u8).
VideoPlayerController.file(File file,
{Future<ClosedCaptionFile>? closedCaptionFile,
this.httpHeaders = const <String, String>{}})
: _closedCaptionFileFuture = closedCaptionFile,
dataSource = Uri.file(file.absolute.path).toString(),
dataSourceType = DataSourceType.file,
package = null,
format = null,
super(const VideoPlayerValue(duration: Duration.zero));
/// Constructs a [VideoPlayerController] playing a video from a contentUri.
///
/// This will load the video from the input content-URI.
/// This is supported on Android only.
VideoPlayerController.contentUri(Uri contentUri,
{Future<ClosedCaptionFile>? closedCaptionFile})
: assert(defaultTargetPlatform == TargetPlatform.android,
'VideoPlayerController.contentUri is only supported on Android.'),
_closedCaptionFileFuture = closedCaptionFile,
dataSource = contentUri.toString(),
dataSourceType = DataSourceType.contentUri,
package = null,
format = null,
httpHeaders = const <String, String>{},
super(const VideoPlayerValue(duration: Duration.zero));
/// The URI to the video file. This will be in different formats depending on
/// the [DataSourceType] of the original video.
final String dataSource;
/// HTTP headers used for the request to the [dataSource].
/// Only for [VideoPlayerController.network].
/// Always empty for other video types.
final Map<String, String> httpHeaders;
/// **Android only**. Will override the platform's generic file format
/// detection with whatever is set here.
final VideoFormat? format;
/// Describes the type of data source this [VideoPlayerController]
/// is constructed with.
final DataSourceType dataSourceType;
/// Only set for [asset] videos. The package that the asset was loaded from.
final String? package;
Future<ClosedCaptionFile>? _closedCaptionFileFuture;
ClosedCaptionFile? _closedCaptionFile;
Timer? _timer;
bool _isDisposed = false;
Completer<void>? _creatingCompleter;
StreamSubscription<dynamic>? _eventSubscription;
_VideoAppLifeCycleObserver? _lifeCycleObserver;
/// The id of a texture that hasn't been initialized.
@visibleForTesting
static const PlayerId kUninitializedPlayerId = -1;
PlayerId _playerId = kUninitializedPlayerId;
/// This is just exposed for testing. It shouldn't be used by anyone depending
/// on the plugin.
@visibleForTesting
PlayerId get playerId => _playerId;
/// Attempts to open the given [dataSource] and load metadata about the video.
Future<void> initialize() async {
const bool allowBackgroundPlayback = false;
if (!allowBackgroundPlayback) {
_lifeCycleObserver = _VideoAppLifeCycleObserver(this);
}
_lifeCycleObserver?.initialize();
_creatingCompleter = Completer<void>();
late DataSource dataSourceDescription;
switch (dataSourceType) {
case DataSourceType.asset:
dataSourceDescription = DataSource(
sourceType: DataSourceType.asset,
format: VideoFormat.other,
asset: dataSource,
package: package,
);
break;
case DataSourceType.network:
dataSourceDescription = DataSource(
sourceType: DataSourceType.network,
uri: dataSource,
format: format ?? VideoFormat.other,
httpHeaders: httpHeaders,
);
break;
case DataSourceType.file:
dataSourceDescription = DataSource(
sourceType: DataSourceType.file,
format: VideoFormat.other,
uri: dataSource,
httpHeaders: httpHeaders,
);
break;
case DataSourceType.contentUri:
dataSourceDescription = DataSource(
sourceType: DataSourceType.contentUri,
format: VideoFormat.other,
uri: dataSource,
);
break;
}
_playerId = (await _nutPlayerPlatform.create(dataSourceDescription));
_creatingCompleter?.complete(null);
final Completer<void> initializingCompleter = Completer<void>();
void eventListener(VideoEvent event) {
if (_isDisposed) {
return;
}
switch (event.eventType) {
case VideoEventType.initialized:
value = value.copyWith(
duration: event.duration,
size: event.size,
rotationCorrection: event.rotationCorrection,
isInitialized: event.duration != null,
errorDescription: null,
);
initializingCompleter.complete(null);
_applyVolume();
_applyPlayPause();
break;
case VideoEventType.completed:
// In this case we need to stop _timer, set isPlaying=false, and
// position=value.duration. Instead of setting the values directly,
// we use pause() and seekTo() to ensure the platform stops playing
// and seeks to the last frame of the video.
pause().then((void pauseResult) => seekTo(value.duration));
break;
case VideoEventType.bufferingUpdate:
value = value.copyWith(buffered: event.buffered);
break;
case VideoEventType.bufferingStart:
value = value.copyWith(isBuffering: true);
break;
case VideoEventType.bufferingEnd:
value = value.copyWith(isBuffering: false);
break;
case VideoEventType.isPlayingStateUpdate:
value = value.copyWith(isPlaying: event.isPlaying);
break;
case VideoEventType.unknown:
break;
}
}
if (_closedCaptionFileFuture != null) {
await _updateClosedCaptionWithFuture(_closedCaptionFileFuture);
}
void errorListener(Object obj) {
final PlatformException e = obj as PlatformException;
value = VideoPlayerValue.erroneous(e.message!);
_timer?.cancel();
if (!initializingCompleter.isCompleted) {
initializingCompleter.completeError(obj);
}
}
_eventSubscription = _nutPlayerPlatform
.videoEventsFor(_playerId)
.listen(eventListener, onError: errorListener);
return initializingCompleter.future;
}
@override
Future<void> dispose() async {
if (_isDisposed) {
return;
}
if (_creatingCompleter != null) {
await _creatingCompleter!.future;
if (!_isDisposed) {
_isDisposed = true;
_timer?.cancel();
await _eventSubscription?.cancel();
await _nutPlayerPlatform.dispose(_playerId);
}
_lifeCycleObserver?.dispose();
}
_isDisposed = true;
super.dispose();
}
/// Starts playing the video.
///
/// If the video is at the end, this method starts playing from the beginning.
///
/// This method returns a future that completes as soon as the "play" command
/// has been sent to the platform, not when playback itself is totally
/// finished.
Future<void> play() async {
if (value.position == value.duration) {
await seekTo(Duration.zero);
}
value = value.copyWith(isPlaying: true);
await _applyPlayPause();
}
/// Pauses the video.
Future<void> pause() async {
value = value.copyWith(isPlaying: false);
await _applyPlayPause();
}
Future<void> _applyPlayPause() async {
if (_isDisposedOrNotInitialized) {
return;
}
if (value.isPlaying) {
await _nutPlayerPlatform.play(_playerId);
// Cancel previous timer.
_timer?.cancel();
_timer = Timer.periodic(
const Duration(milliseconds: 500),
(Timer timer) async {
if (_isDisposed) {
return;
}
final Duration? newPosition = await position;
if (newPosition == null) {
return;
}
_updatePosition(newPosition);
},
);
// This ensures that the correct playback speed is always applied when
// playing back. This is necessary because we do not set playback speed
// when paused.
await _applyPlaybackSpeed();
} else {
_timer?.cancel();
await _nutPlayerPlatform.pause(_playerId);
}
}
Future<void> _applyVolume() async {
if (_isDisposedOrNotInitialized) {
return;
}
await _nutPlayerPlatform.setVolume(_playerId, value.volume);
}
Future<void> _applyPlaybackSpeed() async {
if (_isDisposedOrNotInitialized) {
return;
}
// Setting the playback speed on iOS will trigger the video to play. We
// prevent this from happening by not applying the playback speed until
// the video is manually played from Flutter.
if (!value.isPlaying) {
return;
}
await _nutPlayerPlatform.setPlaybackSpeed(
_playerId,
value.playbackSpeed,
);
}
/// The position in the current video.
Future<Duration?> get position async {
if (_isDisposed) {
return null;
}
return _nutPlayerPlatform.getPosition(_playerId);
}
/// Sets the video's current timestamp to be at [moment]. The next
/// time the video is played it will resume from the given [moment].
///
/// If [moment] is outside of the video's full range it will be automatically
/// and silently clamped.
Future<void> seekTo(Duration position) async {
if (_isDisposedOrNotInitialized) {
return;
}
if (position > value.duration) {
position = value.duration;
} else if (position < Duration.zero) {
position = Duration.zero;
}
await _nutPlayerPlatform.seek(_playerId, position);
_updatePosition(position);
}
/// Sets the audio volume of [this].
///
/// [volume] indicates a value between 0.0 (silent) and 1.0 (full volume) on a
/// linear scale.
Future<void> setVolume(double volume) async {
value = value.copyWith(volume: volume.clamp(0.0, 1.0));
await _applyVolume();
}
/// Sets the playback speed of [this].
///
/// [speed] indicates a speed value with different platforms accepting
/// different ranges for speed values. The [speed] must be greater than 0.
///
/// The values will be handled as follows:
/// * On web, the audio will be muted at some speed when the browser
/// determines that the sound would not be useful anymore. For example,
/// "Gecko mutes the sound outside the range `0.25` to `5.0`" (see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate).
/// * On Android, some very extreme speeds will not be played back accurately.
/// Instead, your video will still be played back, but the speed will be
/// clamped by ExoPlayer (but the values are allowed by the player, like on
/// web).
/// * On iOS, you can sometimes not go above `2.0` playback speed on a video.
/// An error will be thrown for if the option is unsupported. It is also
/// possible that your specific video cannot be slowed down, in which case
/// the plugin also reports errors.
Future<void> setPlaybackSpeed(double speed) async {
if (speed < 0) {
throw ArgumentError.value(
speed,
'Negative playback speeds are generally unsupported.',
);
} else if (speed == 0) {
throw ArgumentError.value(
speed,
'Zero playback speed is generally unsupported. Consider using [pause].',
);
}
value = value.copyWith(playbackSpeed: speed);
await _applyPlaybackSpeed();
}
/// Sets the caption offset.
///
/// The [offset] will be used when getting the correct caption for a specific position.
/// The [offset] can be positive or negative.
///
/// The values will be handled as follows:
/// * 0: This is the default behaviour. No offset will be applied.
/// * >0: The caption will have a negative offset. So you will get caption text from the past.
/// * <0: The caption will have a positive offset. So you will get caption text from the future.
void setCaptionOffset(Duration offset) {
value = value.copyWith(
captionOffset: offset,
@@ -13,6 +524,13 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
);
}
/// The closed caption based on the current [position] in the video.
///
/// If there are no closed captions at the current [position], this will
/// return an empty [Caption].
///
/// If no [closedCaptionFile] was specified, this will always return an empty
/// [Caption].
Caption _getCaptionAt(Duration position) {
if (_closedCaptionFile == null) {
return Caption.none;
@@ -50,4 +568,56 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
_closedCaptionFile = await closedCaptionFile;
value = value.copyWith(caption: _getCaptionAt(value.position));
}
}*/
void _updatePosition(Duration position) {
value = value.copyWith(
position: position,
caption: _getCaptionAt(position),
);
}
@override
void removeListener(VoidCallback listener) {
// Prevent VideoPlayer from causing an exception to be thrown when attempting to
// remove its own listener after the controller has already been disposed.
if (!_isDisposed) {
super.removeListener(listener);
}
}
bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized;
}
///
class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver {
_VideoAppLifeCycleObserver(this._controller);
bool _wasPlayingBeforePause = false;
final VideoPlayerController _controller;
void initialize() {
_ambiguate(WidgetsBinding.instance)!.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_wasPlayingBeforePause = _controller.value.isPlaying;
_controller.pause();
} else if (state == AppLifecycleState.resumed) {
if (_wasPlayingBeforePause) {
_controller.play();
}
}
}
void dispose() {
_ambiguate(WidgetsBinding.instance)!.removeObserver(this);
}
}
/// This allows a value of type T or T? to be treated as a value of type T?.
///
/// We use this so that APIs that have become non-nullable can still be used
/// with `!` and `?` on the stable branch.
T? _ambiguate<T>(T? value) => value;
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart' show DurationRange;
import 'closed_caption_file.dart';
/// The duration, current position, buffering state, error state and settings
/// of a [VideoPlayerController].
@@ -12,18 +13,17 @@ class VideoPlayerValue {
required this.duration,
this.size = Size.zero,
this.position = Duration.zero,
this.subtitles,
this.qualities,
this.caption = Caption.none,
this.captionOffset = Duration.zero,
this.buffered = const <DurationRange>[],
this.isInitialized = false,
this.isPlaying = false,
this.isLooping = false,
this.isBuffering = false,
this.volume,
this.volume = 1.0,
this.playbackSpeed = 1.0,
this.rotationCorrection = 0,
this.errorDescription,
this.isCompleted = false,
});
/// Returns an instance for a video that hasn't been loaded.
@@ -49,12 +49,16 @@ class VideoPlayerValue {
/// The current playback position.
final Duration position;
/// Current available subtitles where key is the id of the subtitles and value is their title.
/// If no subtitles, the dictionary will be empty.
final Map<String, String>? subtitles;
/// The [Caption] that should be displayed based on the current [position].
///
/// This field will never be null. If there is no caption for the current
/// [position], this will be a [Caption.none] object.
final Caption caption;
/// Текущие доступные качества видео. Если их нет, массив словарей будет пустым.
final List<Map<String, dynamic>>? qualities;
/// The [Duration] that should be used to offset the current [position] to get the correct [Caption].
///
/// Defaults to Duration.zero.
final Duration captionOffset;
/// The currently buffered ranges.
final List<DurationRange> buffered;
@@ -69,7 +73,7 @@ class VideoPlayerValue {
final bool isBuffering;
/// The current volume of the playback.
final double? volume;
final double volume;
/// The current speed of the playback.
final double playbackSpeed;
@@ -79,8 +83,6 @@ class VideoPlayerValue {
/// If [hasError] is false this is `null`.
final String? errorDescription;
final bool isCompleted;
/// The [size] of the currently loaded video.
final Size size;
@@ -117,8 +119,8 @@ class VideoPlayerValue {
Duration? duration,
Size? size,
Duration? position,
Map<String, String>? subtitles,
List<Map<String, dynamic>>? qualities,
Caption? caption,
Duration? captionOffset,
List<DurationRange>? buffered,
bool? isInitialized,
bool? isPlaying,
@@ -128,14 +130,13 @@ class VideoPlayerValue {
double? playbackSpeed,
int? rotationCorrection,
String? errorDescription = _defaultErrorDescription,
bool? isCompleted,
}) {
return VideoPlayerValue(
duration: duration ?? this.duration,
size: size ?? this.size,
position: position ?? this.position,
subtitles: subtitles ?? this.subtitles,
qualities: qualities ?? this.qualities,
caption: caption ?? this.caption,
captionOffset: captionOffset ?? this.captionOffset,
buffered: buffered ?? this.buffered,
isInitialized: isInitialized ?? this.isInitialized,
isPlaying: isPlaying ?? this.isPlaying,
@@ -147,7 +148,6 @@ class VideoPlayerValue {
errorDescription: errorDescription != _defaultErrorDescription
? errorDescription
: this.errorDescription,
isCompleted: isCompleted ?? this.isCompleted,
);
}
@@ -157,8 +157,8 @@ class VideoPlayerValue {
'duration: $duration, '
'size: $size, '
'position: $position, '
'subtitles: $subtitles, '
'qualities: $qualities, '
'caption: $caption, '
'captionOffset: $captionOffset, '
'buffered: [${buffered.join(', ')}], '
'isInitialized: $isInitialized, '
'isPlaying: $isPlaying, '
@@ -166,8 +166,7 @@ class VideoPlayerValue {
'isBuffering: $isBuffering, '
'volume: $volume, '
'playbackSpeed: $playbackSpeed, '
'errorDescription: $errorDescription, '
'isCompleted: $isCompleted), ';
'errorDescription: $errorDescription)';
}
@override
@@ -177,8 +176,8 @@ class VideoPlayerValue {
runtimeType == other.runtimeType &&
duration == other.duration &&
position == other.position &&
subtitles == other.subtitles &&
listEquals(qualities, other.qualities) &&
caption == other.caption &&
captionOffset == other.captionOffset &&
listEquals(buffered, other.buffered) &&
isPlaying == other.isPlaying &&
isLooping == other.isLooping &&
@@ -188,15 +187,14 @@ class VideoPlayerValue {
errorDescription == other.errorDescription &&
size == other.size &&
rotationCorrection == other.rotationCorrection &&
isInitialized == other.isInitialized &&
isCompleted == other.isCompleted;
isInitialized == other.isInitialized;
@override
int get hashCode => Object.hash(
duration,
position,
subtitles,
qualities,
caption,
captionOffset,
buffered,
isPlaying,
isLooping,
@@ -207,6 +205,5 @@ class VideoPlayerValue {
size,
rotationCorrection,
isInitialized,
isCompleted,
);
}
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'video_progress_colors.dart';
import '../controller/video_player_controller.dart';
import 'video_player_controller.dart';
import 'video_scrubber.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart' show DurationRange;
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import '../controller/video_player_controller.dart';
import 'video_player_controller.dart';
/// A scrubber to control [VideoPlayerController]s
class VideoScrubber extends StatefulWidget {
@@ -35,7 +35,7 @@ class _VideoScrubberState extends State<VideoScrubber> {
final Offset tapPos = box.globalToLocal(globalPosition);
final double relative = tapPos.dx / box.size.width;
final Duration position = controller.value.duration * relative;
controller.seek(position);
controller.seekTo(position);
}
return GestureDetector(
@@ -1,15 +0,0 @@
import '../../nut_player.dart';
class CommonPlayerContent implements PlayerContent {
@override
ContentType content;
@override
List<PlayerStatisticRecord> statistics;
@override
List<PlayerSubtitleRecord> subtitles;
CommonPlayerContent({required this.content, this.statistics = const [], this.subtitles = const []});
}
@@ -1,27 +0,0 @@
sealed class ContentType {}
final class AutoContentType extends ContentType {
final String urlPath;
AutoContentType({required this.urlPath});
}
final class HlsContentType extends ContentType {
final String urlPath;
final bool isLive;
HlsContentType({required this.urlPath, required this.isLive});
}
final class DashContentType extends ContentType {
final String urlPath;
DashContentType({required this.urlPath});
}
final class Mp4ContentType extends ContentType {
final String urlPath;
final bool isLoop;
Mp4ContentType({required this.urlPath, this.isLoop = false});
}
@@ -1,62 +0,0 @@
import 'package:nut_player/src/model/content_type.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart';
PlatformContentType createPlatformContent(ContentType type) {
final contentType = type;
if (contentType is AutoContentType) {
return PlatformAutoContentType(urlPath: contentType.urlPath);
} else if (contentType is HlsContentType) {
return PlatformHlsContentType(urlPath: contentType.urlPath, isLive: contentType.isLive);
} else if (contentType is DashContentType) {
return PlatformDashContentType(urlPath: contentType.urlPath);
} else if (contentType is Mp4ContentType) {
return PlatformMp4ContentType(urlPath: contentType.urlPath, isLoop: contentType.isLoop);
} else {
throw Error();
}
}
class PlatformPlayerContentImpl extends PlatformPlayerContent {
@override PlatformContentType content;
@override List<PlatformPlayerStatisticRecord> statistics;
@override List<PlatformPlayerSubtitleRecord> subtitles;
PlatformPlayerContentImpl({required this.content, required this.statistics, required this.subtitles});
}
class PlatformStatisticRecordImpl extends PlatformPlayerStatisticRecord {
@override String name;
@override String urlTemplate;
@override double start;
@override double delay;
@override int count;
@override PlatformHTTPMethod method;
@override String? body;
PlatformStatisticRecordImpl({
required this.name,
required this.urlTemplate,
required this.start,
required this.delay,
required this.count,
required this.method,
this.body
});
}
class PlatformSubtitleRecordImpl extends PlatformPlayerSubtitleRecord {
@override String title;
@override PlatformSubtitleType type;
@override String url;
@override String language;
PlatformSubtitleRecordImpl({
required this.title,
required this.type,
required this.url,
required this.language,
});
}
@@ -1,9 +0,0 @@
import './content_type.dart';
import './player_statistic_record.dart';
import './player_subtitle_record.dart';
abstract class PlayerContent {
ContentType get content;
List<PlayerStatisticRecord> get statistics;
List<PlayerSubtitleRecord> get subtitles;
}
@@ -1,23 +0,0 @@
enum HTTPMethod { get, post }
abstract class PlayerStatisticRecord {
/// Тип счетчика, определяет логику по которой будет запрашиваться счетчик.
/// Может быть несколько счетчиков с одинаковым типом, в этом случае их нужно запросить все.
String get name;
/// Ссылка для запроса счетчика.
/// Может содержать набор динамических параметров.
/// Ссылка может быть указана как относительная, без явного указания протокола (http/https).
/// В этом случае при запросе ссылки протокол нужно будет добавить.
String get urlTemplate;
/// Время в секундах когда нужно запросить счетчик во время проигрывания основного видео.
double get start;
/// Время в секундах между запросами счетчика во время проигрывания основного видео.
double get delay;
/// Максимальное кол-во запросов счетчика во время проигрывания основного видео.
int get count;
/// Метод запроса счетчика статистики.
HTTPMethod get method;
/// Содержимое запроса в случае отправки запроса через post.
/// Может содержать набор динамических параметров.
String? get body;
}
@@ -1,14 +0,0 @@
enum SubtitleType { srt, unknown }
abstract class PlayerSubtitleRecord {
/// Название субтитров.
/// Используется для вывода в меню плеера
String get title;
/// Тип субтитров
SubtitleType get type;
/// Ссылка на файл субтитров
String get url;
/// Язык на котором отображаются субтитры.
/// Соответствует стандарту ISO 639-2
String get language;
}
@@ -1,14 +0,0 @@
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart' show NutPlayerPlatform;
// Активная платформа плеера (android, ios, web)
NutPlayerPlatform? _lastNutPlayerPlatform;
NutPlayerPlatform get nutPlayerPlatform {
final NutPlayerPlatform currentInstance = NutPlayerPlatform.instance;
if (_lastNutPlayerPlatform != currentInstance) {
// This will clear all open videos on the platform when a full restart is
// performed.
currentInstance.init();
_lastNutPlayerPlatform = currentInstance;
}
return currentInstance;
}
@@ -1,27 +0,0 @@
import 'dart:async';
import 'package:flutter/services.dart';
class PlayerVersionObserver {
final MethodChannel _pluginChannel = const MethodChannel('tech.nut/plugin');
final _versionStreamController = StreamController<String>();
Stream<String> get versionStream => _versionStreamController.stream.asBroadcastStream();
PlayerVersionObserver() {
_pluginChannel.setMethodCallHandler(_pluginVersionHandler);
_pluginChannel.invokeMethod("getPlayerVersion");
}
Future<dynamic> _pluginVersionHandler(MethodCall call) async {
switch (call.method) {
case 'pluginVersion':
final version = call.arguments['version'];
if (version != null) {
_versionStreamController.add(version);
}
break;
default:
break;
}
}
}
@@ -1,38 +0,0 @@
import 'package:nut_player/nut_player.dart';
import 'package:nut_player/src/model/common_player_content.dart';
class CommonProvider implements Provider {
final PlayerContent content;
// MARK: - Constructors
CommonProvider({required this.content});
factory CommonProvider.url(String urlPath, {bool isLive = false, bool isLoop = false}) {
final ContentType content;
if (urlPath.endsWith('m3u8')) {
content = HlsContentType(urlPath: urlPath, isLive: isLive);
} else {
content = Mp4ContentType(urlPath: urlPath, isLoop: isLoop);
}
final playerContent = CommonPlayerContent(content: content);
return CommonProvider(content: playerContent);
}
factory CommonProvider.content({
required ContentType content,
List<PlayerStatisticRecord> statistics = const [],
List<PlayerSubtitleRecord> subtitles = const []
}) {
final playerContent = CommonPlayerContent(content: content, statistics: statistics, subtitles: subtitles);
return CommonProvider(content: playerContent);
}
// MARK: - Provider
@override
Future<PlayerContent> retrieveContent() {
return Future.value(content);
}
}
@@ -1,78 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:nut_player/nut_player.dart';
import 'package:nut_player/src/provider/json_provider/model/json_content.dart';
import 'package:nut_player/src/provider/json_provider/model/json_error.dart';
import 'package:nut_player/src/provider/json_provider/model/json_statistics.dart';
import 'package:nut_player/src/provider/json_provider/model/json_subtitles.dart';
import 'package:nut_player/src/provider/json_provider/parsing/response.dart';
class JsonProvider extends Provider {
final String urlPath;
JsonProvider(this.urlPath);
@override
Future<PlayerContent> retrieveContent() async {
final response = await http.get(Uri.parse(urlPath));
if (response.statusCode == 200) {
return _handleSucceedResponse(response);
} else {
throw JsonBadStatusCodeError();
}
}
JsonContent _handleSucceedResponse(http.Response httpResponse) {
var response = Response.fromJson(jsonDecode(httpResponse.body) as Map<String, dynamic>);
if (response.playbacks.isEmpty) { throw JsonNoPlaybacksError(); }
var uri = Uri.tryParse(response.playbacks.first.streamUrl);
if (uri != null) {
final statistics = response.statistics.map((stat) =>
JsonStatistics(
stat.name,
stat.urlTemplate,
stat.start,
stat.delay,
stat.count,
stat.method == "get" ? HTTPMethod.get : HTTPMethod.post,
stat.body
)
);
final subtitles = response.subtitles.map((sub) =>
JsonSubtitles(
sub.title,
sub.type == "srt" ? SubtitleType.srt : SubtitleType.unknown,
sub.url,
sub.language
)
);
final ContentType? contentType;
final urlPath = uri.toString();
if (urlPath.endsWith('.mp4')) {
contentType = Mp4ContentType(urlPath: urlPath);
} else if (urlPath.endsWith('.m3u8')) {
final isLive = response.playbacks.first.isLive;
contentType = HlsContentType(urlPath: urlPath, isLive: isLive);
} else {
contentType = null;
}
if (contentType != null) {
return JsonContent(
contentType,
statistics.toList(),
subtitles.toList()
);
} else {
throw JsonUnknownFormatError();
}
} else {
throw JsonIncorrectUrlError();
}
}
}
@@ -1,14 +0,0 @@
import 'package:nut_player/nut_player.dart';
class JsonContent extends PlayerContent {
@override
ContentType content;
@override
List<PlayerStatisticRecord> statistics;
@override
List<PlayerSubtitleRecord> subtitles;
JsonContent(this.content, this.statistics, this.subtitles);
}
@@ -1,5 +0,0 @@
class JsonBadStatusCodeError extends Error {}
class JsonNoPlaybacksError extends Error {}
class JsonIncorrectUrlError extends Error {}
class JsonUnknownFormatError extends Error {}
@@ -1,26 +0,0 @@
import 'package:nut_player/nut_player.dart';
class JsonStatistics extends PlayerStatisticRecord {
@override
String name;
@override
String urlTemplate;
@override
double start;
@override
double delay;
@override
int count;
@override
HTTPMethod method;
@override
String? body;
JsonStatistics(this.name, this.urlTemplate, this.start, this.delay, this.count, this.method, this.body);
}
@@ -1,17 +0,0 @@
import 'package:nut_player/nut_player.dart';
class JsonSubtitles extends PlayerSubtitleRecord {
@override
String title;
@override
SubtitleType type;
@override
String url;
@override
String language;
JsonSubtitles(this.title, this.type, this.url, this.language);
}
@@ -1,19 +0,0 @@
class PlaybackRecord {
final String streamType;
final String streamUrl;
final bool isLive;
final bool isVideo;
final bool isAudio;
PlaybackRecord({required this.streamType, required this.streamUrl, required this.isLive, required this.isVideo, required this.isAudio});
factory PlaybackRecord.fromJson(dynamic data) {
return PlaybackRecord(
streamType: data['stream_type'],
streamUrl: data['stream_url'],
isLive: data['is_live'],
isVideo: data['is_video'],
isAudio: data['is_audio']
);
}
}
@@ -1,23 +0,0 @@
import 'statistic_record.dart';
import 'subtitle_record.dart';
import 'playback_record.dart';
class Response {
final List<PlaybackRecord> playbacks;
final List<StatisticRecord> statistics;
final List<PlayerSubtitleRecord> subtitles;
Response({required this.playbacks, required this.statistics, required this.subtitles});
factory Response.fromJson(dynamic data) {
var playbacks = data['playback'].map((item) => PlaybackRecord.fromJson(item)).toList();
var statistics = data['stat'].map((item) => StatisticRecord.fromJson(item)).toList();
var subtitles = data['subtitle'].map((item) => PlayerSubtitleRecord.fromJson(item)).toList();
return Response(
playbacks: List<PlaybackRecord>.from(playbacks),
statistics: List<StatisticRecord>.from(statistics),
subtitles: List<PlayerSubtitleRecord>.from(subtitles)
);
}
}
@@ -1,23 +0,0 @@
class StatisticRecord {
final String name;
final String urlTemplate;
final double start;
final double delay;
final int count;
final String method;
final String? body;
StatisticRecord({required this.name, required this.urlTemplate, required this.start, required this.delay, required this.count, required this.method, this.body});
factory StatisticRecord.fromJson(dynamic data) {
return StatisticRecord(
name: data['name'],
urlTemplate: data['url_template'],
start: data['start'] == null ? 0.0 : data['start'].toDouble(),
delay: data['delay'] == null ? 0.0 : data['delay'].toDouble(),
count: data['count'],
method: data['method'],
body: data['body'],
);
}
}

Some files were not shown because too many files have changed in this diff Show More