Compare commits
261 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9b86c1667 | |||
| 15b3028bce | |||
| 76f3ddb9ea | |||
| 77113197a9 | |||
| 872762bb3f | |||
| 95908a72f4 | |||
| 7b3a76233a | |||
| 5d9c5b8fcf | |||
| dc85ce231a | |||
| c79f69073b | |||
| ab7a54b7c5 | |||
| 9e958f1d36 | |||
| a9160da8cb | |||
| b41357c3c4 | |||
| 97a8fcf335 | |||
| ffb022fcfa | |||
| 6e52bc2dfd | |||
| ccade48858 | |||
| 387d453ccb | |||
| 4e4ee0d099 | |||
| 67d12b034c | |||
| b8daeb904b | |||
| 8223afa372 | |||
| 9af7d065ec | |||
| d8a3800e2b | |||
| 5f60751d10 | |||
| 5c93e4a032 | |||
| 5c3385bc6b | |||
| a57268c778 | |||
| 9d607dadf6 | |||
| 31e27434a9 | |||
| 7df0d1b9ca | |||
| af40337fc1 | |||
| 6e5294e3c3 | |||
| 2835c49184 | |||
| 0fa0e04150 | |||
| f2ae773d71 | |||
| e52362282f | |||
| 92d1a5801f | |||
| 914c011e02 | |||
| 33141b40d1 | |||
| c1cd13e674 | |||
| f6f2f3cb7e | |||
| 7dff0bf671 | |||
| cef78d59dc | |||
| 9dce72e693 | |||
| 303d3731c3 | |||
| a6587d3fd5 | |||
| f68dce0fd8 | |||
| c5158de172 | |||
| 99f23ad4b5 | |||
| 277736a437 | |||
| 4e353591ad | |||
| 118472fee2 | |||
| 40d24b600f | |||
| fdd9279c31 | |||
| eef04bec28 | |||
| 400f1807ba | |||
| 306843379a | |||
| e58b10f27a | |||
| 2cc84fb3cf | |||
| 833a478637 | |||
| ca6c2505ba | |||
| b2d8872735 | |||
| da10b69931 | |||
| 56f61be4bb | |||
| 60fdf1df73 | |||
| 6be8b44149 | |||
| 69b10f1285 | |||
| 2127c8442c | |||
| 358b60dc58 | |||
| 69e4e62231 | |||
| 591d3ae284 | |||
| db11344284 | |||
| 58737dd089 | |||
| 30c6758b1e | |||
| 7d67135987 | |||
| 576b2303f2 | |||
| c4b503a9c8 | |||
| 0b0b7e5c7e | |||
| df2912ad9c | |||
| fd4e69caaf | |||
| d0ef98cafe | |||
| d63df8ad06 | |||
| abb6f0f208 | |||
| 8258729684 | |||
| 9b63e0bcfd | |||
| 3ca84c7c24 | |||
| ab08b7fc6a | |||
| ccacc43b0e | |||
| 03809c18ce | |||
| 90a366d4bb | |||
| a411d071ac | |||
| a4b9a00955 | |||
| 03562349cc | |||
| b81385a069 | |||
| 1c9541306f | |||
| 6b81a92ddc | |||
| 645eede99d | |||
| 6c2fde9513 | |||
| 863b0bb2ed | |||
| 4f01af6d90 | |||
| 6c27b8e33e | |||
| 3a186c2380 | |||
| 9ce0d7909b | |||
| 5aa0d814aa | |||
| 8f78121b6a | |||
| 5a5cdc7a3b | |||
| 998b59fc26 | |||
| 3b309987cc | |||
| f6c9f33759 | |||
| b01dcdc14f | |||
| b8477b6fcc | |||
| 39ea894911 | |||
| d8810095ac | |||
| a8e47899fc | |||
| 4ade88a340 | |||
| bf086be36c | |||
| 39657f4c86 | |||
| 1b36d750c5 | |||
| c91621c095 | |||
| e21f11a793 | |||
| 7d323b1a74 | |||
| 316d55affd | |||
| 1893e6a915 | |||
| a88e02de31 | |||
| 3ca470f2bc | |||
| 79a8b11ca4 | |||
| 3a4b783d0b | |||
| 1be9b03ed9 | |||
| aa6eda389e | |||
| 0afeb548c8 | |||
| 641ab5d5a2 | |||
| 730d54a1da | |||
| e1379ff0d9 | |||
| e236fe641c | |||
| c50b1f9ead | |||
| ca3b52190f | |||
| 672c4930e7 | |||
| ca85dc91cc | |||
| c1c077b90b | |||
| 4fb4f23f3f | |||
| 7fb17a36b4 | |||
| a1d32a9060 | |||
| ec6e9ab536 | |||
| 2803ea99b7 | |||
| eac47f5b38 | |||
| 180830b694 | |||
| 15effa1df3 | |||
| f4d176ae7a | |||
| 836c94e9fb | |||
| 6da3a02280 | |||
| ce5b8251a3 | |||
| 7928ebe45d | |||
| b660ac2a86 | |||
| 799fde61cf | |||
| ae978837ec | |||
| 913190d8bc | |||
| 82c3f27c33 | |||
| f178b33df9 | |||
| fbda0b09a0 | |||
| 766c2cfd76 | |||
| 78703aa2c3 | |||
| 05f73fd37d | |||
| a7ec09aa62 | |||
| 8204cae8c5 | |||
| 32deef424c | |||
| 58c46ab346 | |||
| 4035f4bab0 | |||
| bce1efc309 | |||
| 2d48e2148a | |||
| 8fb9cd8572 | |||
| 6389d37053 | |||
| 27ea2e8436 | |||
| 533fe0b4b1 | |||
| 1701cb5400 | |||
| d4864e48f7 | |||
| 6c4ef795a7 | |||
| dd38843002 | |||
| f8c24c071b | |||
| 7d5b876bda | |||
| 4d31c24998 | |||
| 130ba3a1ef | |||
| cfcecd3403 | |||
| 2e859291e8 | |||
| 168a5044bd | |||
| d5d8ebc328 | |||
| 899efd763c | |||
| 92b4be7113 | |||
| e1af5417c1 | |||
| 9e082faecb | |||
| 86cc2824c8 | |||
| fe3d7bb507 | |||
| edd46e50eb | |||
| 885d75a18e | |||
| 909f35c1b9 | |||
| 61db66797f | |||
| e23a4db2df | |||
| b76ac97214 | |||
| bb59249479 | |||
| e4f7655c8b | |||
| 624ef8c9a8 | |||
| a10a917742 | |||
| a9d948a19e | |||
| f035153cb6 | |||
| a9afdca7ea | |||
| 6fd970aaf4 | |||
| d0c557692e | |||
| 054b7f0b2b | |||
| 35c5b2d8e7 | |||
| c3d699e385 | |||
| af004e86ab | |||
| 640392fcd0 | |||
| 6edcade8a5 | |||
| 3d0ba6e34c | |||
| aea1f455cc | |||
| 84a0cdfbd4 | |||
| 5d85c0a6db | |||
| f00efb27da | |||
| 21519397a6 | |||
| c489533ce4 | |||
| 6dfbd5cf27 | |||
| 3e9a152e40 | |||
| 63db8a4a10 | |||
| 4832b88906 | |||
| 0c725023c0 | |||
| 7234595a5b | |||
| 8432d03bbd | |||
| b9a56be791 | |||
| 7f925801c7 | |||
| cb38ad2fc9 | |||
| 71a8f84fdc | |||
| 73ad617c66 | |||
| 571fcb783f | |||
| c748ba6644 | |||
| 6abb28bd4b | |||
| 1a4000688a | |||
| f96b300ef4 | |||
| f9b432cc4c | |||
| b4a9e6c225 | |||
| c6f06652f4 | |||
| 46e493dd4e | |||
| 7d416fa3d2 | |||
| f9e2fa7578 | |||
| 136aa5d593 | |||
| 2f5d985ac1 | |||
| 66f04ec833 | |||
| e69984cbf1 | |||
| 8101052ab2 | |||
| 0f4fcd2342 | |||
| c55db303a0 | |||
| 29b9e8a6b7 | |||
| 8d2d0b3fb0 | |||
| ff441f97ce | |||
| dee04293ee | |||
| 50e2608f94 | |||
| 4432a4326c | |||
| e12f6886bf | |||
| 43eda35216 | |||
| 6d89641fa8 | |||
| 290fe952dc |
+60
-37
@@ -1,21 +1,18 @@
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
# Build template
|
||||
.BuildApplication: &BuildApplication
|
||||
when: on_success
|
||||
allow_failure: false
|
||||
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
|
||||
tags:
|
||||
- macos
|
||||
- flutter
|
||||
stage: build
|
||||
|
||||
Build:
|
||||
<<: *BuildApplication
|
||||
only:
|
||||
refs:
|
||||
- develop
|
||||
- tags
|
||||
artifacts:
|
||||
paths:
|
||||
- ./Result
|
||||
@@ -25,11 +22,14 @@ Build:
|
||||
- ./toolchain/build_platforms.sh nut_player device
|
||||
|
||||
ManualBuild:
|
||||
<<: *BuildApplication
|
||||
when: manual
|
||||
only:
|
||||
refs:
|
||||
- /^feature/
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME =~ /^feature/
|
||||
when: manual
|
||||
allow_failure: false
|
||||
tags:
|
||||
- macos
|
||||
- flutter
|
||||
stage: build
|
||||
artifacts:
|
||||
paths:
|
||||
- ./Result
|
||||
@@ -39,8 +39,13 @@ ManualBuild:
|
||||
- ./toolchain/build_platforms.sh nut_player device
|
||||
|
||||
Deploy:
|
||||
when: on_success
|
||||
allow_failure: false
|
||||
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
|
||||
tags:
|
||||
- macos
|
||||
- flutter
|
||||
@@ -54,10 +59,6 @@ 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
|
||||
@@ -65,8 +66,10 @@ Deploy:
|
||||
- ./toolchain/deploy_Android.sh nut_player
|
||||
|
||||
Deploy_Feature:
|
||||
when: manual
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_REF_NAME =~ /^feature/
|
||||
when: manual
|
||||
allow_failure: false
|
||||
tags:
|
||||
- macos
|
||||
- flutter
|
||||
@@ -80,9 +83,6 @@ 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,14 +90,16 @@ Deploy_Feature:
|
||||
- ./toolchain/deploy_iOS.sh nut_player ${IPA_DEPLOY_TARGET} ${IPA_PATH} ${IPA_FILE} iOS
|
||||
|
||||
notifyMessengerFail:
|
||||
when: on_failure
|
||||
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
|
||||
needs:
|
||||
- job: Deploy
|
||||
artifacts: false
|
||||
only:
|
||||
refs:
|
||||
- develop
|
||||
- tags
|
||||
stage: notify
|
||||
tags:
|
||||
- macos
|
||||
@@ -107,14 +109,16 @@ notifyMessengerFail:
|
||||
- ./toolchain/notification.sh FAIL
|
||||
|
||||
notifyMessengerSuccess:
|
||||
when: on_success
|
||||
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
|
||||
needs:
|
||||
- job: Deploy
|
||||
artifacts: false
|
||||
only:
|
||||
refs:
|
||||
- develop
|
||||
- tags
|
||||
stage: notify
|
||||
tags:
|
||||
- macos
|
||||
@@ -122,3 +126,22 @@ 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"
|
||||
+11
-14
@@ -3,46 +3,43 @@ 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:
|
||||
when: on_success
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
|
||||
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
|
||||
@@ -1,5 +1,8 @@
|
||||
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
|
||||
@@ -11,3 +14,4 @@ stages:
|
||||
include:
|
||||
- '.before-merge-request.yml'
|
||||
- '.after-merge-request.yml'
|
||||
- '.triggered.yml'
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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
|
||||
|
||||
@@ -5,27 +5,42 @@ plugins {
|
||||
id 'com.google.gms.google-services'
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
def flutterSdkVersions = new Properties()
|
||||
def flutterSdkVersionsFile = rootProject.file('flutterSdkVersions.properties')
|
||||
if (flutterSdkVersionsFile.exists()) {
|
||||
flutterSdkVersionsFile.withReader('UTF-8') { reader ->
|
||||
flutterSdkVersions.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
def flutterVersionCode = flutterSdkVersions.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
def flutterVersionName = flutterSdkVersions.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 flutter.compileSdkVersion
|
||||
compileSdkVersion flutterCompileSdkVersion.toInteger()
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@@ -46,8 +61,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 flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
minSdkVersion flutterMinSdkVersion
|
||||
targetSdkVersion flutterTargetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@@ -57,6 +72,8 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,4 +82,7 @@ flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {}
|
||||
dependencies {
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.3.1"))
|
||||
implementation(platform("com.google.firebase:firebase-storage-ktx"))
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<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,8 +1,10 @@
|
||||
<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:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -30,4 +32,5 @@
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
@@ -6,9 +6,9 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
classpath 'com.android.tools.build:gradle:8.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:+'
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
// [required] aar plugin// [required] aar plugin
|
||||
url "${project(':nut_player_android').projectDir}/build"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
flutter.versionName=1.0
|
||||
flutter.versionCode=1
|
||||
flutter.compileSdkVersion=33
|
||||
flutter.minSdkVersion=21
|
||||
flutter.targetSdkVersion=33
|
||||
@@ -1,3 +1,7 @@
|
||||
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-7.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 629 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
@@ -1,28 +1,163 @@
|
||||
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
|
||||
- nut_player (0.0.1):
|
||||
- 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 (from `.symlinks/plugins/nut_player/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:
|
||||
:path: ".symlinks/plugins/nut_player/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
|
||||
nut_player: fac5f2edd6e1927a28413a98d968ff8f67078da2
|
||||
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
|
||||
nut_player_ios: c6964e0278d1a01a40929de47bbc7b8bf5bbc8d8
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
|
||||
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
|
||||
PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189
|
||||
PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.13.0
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
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 */; };
|
||||
@@ -15,7 +14,9 @@
|
||||
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 */; };
|
||||
E6DD470C591A4891FD8CC5D5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -42,18 +43,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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
91791C232148EFBE23EB2CB5 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -61,10 +62,11 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -72,7 +74,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
02000B77C8E488D819458EB8 /* Pods_Runner.framework in Frameworks */,
|
||||
A4D9603085CECA05CC013C25 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -80,13 +82,22 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E6DD470C591A4891FD8CC5D5 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
9D6C432C130B5C4CCD4F030E /* 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 = (
|
||||
@@ -98,25 +109,16 @@
|
||||
40FDE73F8AC767B1B188CE85 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
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 */,
|
||||
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 */,
|
||||
);
|
||||
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 = (
|
||||
@@ -136,7 +138,7 @@
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
40FDE73F8AC767B1B188CE85 /* Pods */,
|
||||
54C6C191361639295A13AFCC /* Frameworks */,
|
||||
182DEA855A45E43DC6B6F1B3 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -152,6 +154,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B173FFAB2ACEB6A8009C6CB7 /* GoogleService-Info.plist */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
@@ -171,7 +174,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
4EFE922145174E30D57418A9 /* [CP] Check Pods Manifest.lock */,
|
||||
12508058F119E39A22113F76 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
CE669F37B5D4B4901DDD5C29 /* Frameworks */,
|
||||
@@ -190,14 +193,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
C6C9A172B337EB12851E53A3 /* [CP] Check Pods Manifest.lock */,
|
||||
FA61F88D205F0748E04DA12A /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
944C5DFBA06E9AB19BABF039 /* [CP] Embed Pods Frameworks */,
|
||||
E82AFA085809CE8304618B2B /* [CP] Embed Pods Frameworks */,
|
||||
5C8120DFBB6562B7085522ED /* [firebase_crashlytics] Crashlytics Upload Symbols */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -261,6 +265,7 @@
|
||||
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 */,
|
||||
);
|
||||
@@ -269,23 +274,7 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
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 */ = {
|
||||
12508058F119E39A22113F76 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -307,22 +296,44 @@
|
||||
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;
|
||||
};
|
||||
944C5DFBA06E9AB19BABF039 /* [CP] Embed Pods Frameworks */ = {
|
||||
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 = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
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 = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
shellScript = "\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --flutter-project \"$PROJECT_DIR/firebase_app_id_file.json\" ";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -339,7 +350,24 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
||||
};
|
||||
C6C9A172B337EB12851E53A3 /* [CP] Check Pods Manifest.lock */ = {
|
||||
E82AFA085809CE8304618B2B /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FA61F88D205F0748E04DA12A /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -452,7 +480,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -471,6 +499,7 @@
|
||||
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",
|
||||
@@ -490,7 +519,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6B6C4FD9C0BBEBB441A65F3A /* Pods-RunnerTests.debug.xcconfig */;
|
||||
baseConfigurationReference = 6385803DFF5615AB5A426D76 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -508,7 +537,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 41C5B4FECBC1AB22501D4D1F /* Pods-RunnerTests.release.xcconfig */;
|
||||
baseConfigurationReference = AE4F8A509E834A1E6019B4A1 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -524,7 +553,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FE9E9C027F59BE61D9D85175 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
baseConfigurationReference = ED0594A0BE28CA6FB21AC6AE /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -585,7 +614,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -634,7 +663,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -655,6 +684,7 @@
|
||||
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",
|
||||
@@ -687,6 +717,7 @@
|
||||
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",
|
||||
|
||||
@@ -1,8 +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>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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>
|
||||
@@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -24,18 +26,30 @@
|
||||
<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.storyboard</string>
|
||||
<string>LaunchScreen</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>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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',
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'src/features/main_screen/presentation/home.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
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 '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));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
final double brightness;
|
||||
|
||||
const MyApp({required this.brightness, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Home(),
|
||||
return RepositoryProvider(
|
||||
create: (context) => SettingsRepository.createDefaults(brightness),
|
||||
child: const CupertinoApp( //Для теста убрать const
|
||||
theme: CupertinoThemeData(
|
||||
brightness: Brightness.light,
|
||||
primaryColor: CupertinoColors.link
|
||||
),
|
||||
home: Home()//Bместо Home() подставить PlayerView()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,6 @@
|
||||
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,17 +1,47 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class OptionData {
|
||||
late Key key;
|
||||
late String title;
|
||||
late bool isSelected;
|
||||
final Key key;
|
||||
final String title;
|
||||
final String? value;
|
||||
final bool isLive;
|
||||
final bool isSelected;
|
||||
|
||||
OptionData({required this.key, required this.title, this.isSelected = false});
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
class NumericOptionData {
|
||||
late Key key;
|
||||
late String title;
|
||||
late int value;
|
||||
final Key key;
|
||||
final String title;
|
||||
final int value;
|
||||
final Function(int)? onChange;
|
||||
|
||||
NumericOptionData({required this.key, required this.title, required this.value});
|
||||
const NumericOptionData({required this.key, required this.title, required this.value, this.onChange});
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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 OptionData data;
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
final Function? onTap;
|
||||
|
||||
const CheckmarkListOption(this.data, this.onTap, {super.key});
|
||||
const CheckmarkListOption(this.title, this.isSelected, this.onTap, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoListTile(
|
||||
key: data.key,
|
||||
key: key,
|
||||
onTap: () { if (onTap != null) { onTap!(); } },
|
||||
trailing: data.isSelected ? const Icon(CupertinoIcons.check_mark, color: CupertinoColors.link) : null,
|
||||
title: Text(data.title, style: const TextStyle(fontSize: 17))
|
||||
trailing: isSelected ? const Icon(CupertinoIcons.check_mark, color: CupertinoColors.link) : null,
|
||||
title: Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,106 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:nut_player_example/src/common/models/option_data.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
|
||||
class InputView extends StatelessWidget {
|
||||
class InputView extends StatefulWidget {
|
||||
final String title;
|
||||
final int value;
|
||||
final Function(int)? newSelection;
|
||||
|
||||
final NumericOptionData data;
|
||||
const InputView(this.title, this.value, this.newSelection, {super.key});
|
||||
|
||||
@override
|
||||
State<InputView> createState() => _InputViewState();
|
||||
}
|
||||
|
||||
class _InputViewState extends State<InputView> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
|
||||
InputView(this.data, {super.key});
|
||||
final FocusNode _nodeText = FocusNode();
|
||||
late String _textBefore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_controller.text = '${data.value}';
|
||||
_controller.text = '${widget.value}';
|
||||
_textBefore = '${widget.value}';
|
||||
return CupertinoListTile(
|
||||
key: data.key,
|
||||
key: widget.key,
|
||||
trailing: SizedBox(
|
||||
width: 155,
|
||||
child: CupertinoTextField(
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
maxLength: 10,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.number,
|
||||
controller: _controller,
|
||||
decoration: null,
|
||||
),
|
||||
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;
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
title: Text(data.title, style: const TextStyle(fontSize: 17))
|
||||
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))
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,70 @@
|
||||
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, {super.key});
|
||||
const OptionsView(this.title, this.options, this.selectedIndex, this.newSelection, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoListTile(
|
||||
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)));
|
||||
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)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _showAlertDialog(BuildContext context) {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
barrierColor: CupertinoColors.black.withOpacity(0.5),
|
||||
builder: (BuildContext context) => CupertinoActionSheet(
|
||||
title: Text(title),
|
||||
actions: <CupertinoActionSheetAction>[...options.map((value) {
|
||||
return CupertinoActionSheetAction(
|
||||
child: Text(value.title),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
);
|
||||
}).toList() + [
|
||||
CupertinoActionSheetAction(
|
||||
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(
|
||||
isDefaultAction: true,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Отмена'),
|
||||
)]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
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 {}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
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);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
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']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
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']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
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,11 +1,13 @@
|
||||
import 'package:flutter/cupertino.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';
|
||||
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';
|
||||
|
||||
class Buttons extends StatelessWidget {
|
||||
final bool _isReady;
|
||||
|
||||
const Buttons(this._isReady, {super.key});
|
||||
const Buttons({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -16,43 +18,45 @@ class Buttons extends StatelessWidget {
|
||||
// КНОПКА ПОСМОТРЕТЬ ДЕМО
|
||||
Container(
|
||||
width: double.infinity,
|
||||
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),
|
||||
))),
|
||||
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),
|
||||
));
|
||||
}
|
||||
)),
|
||||
|
||||
//КНОПКА НАСТРОЙКИ ПЛЕЕРА
|
||||
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: () {
|
||||
Navigator.push(context,
|
||||
CupertinoPageRoute(builder: (BuildContext context) { return const SettingsView();})
|
||||
);
|
||||
context.read<MainBloc>().add(OpenSettingsEvent());
|
||||
},
|
||||
child: const Text(
|
||||
'Настройки плеера',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
color: CupertinoColors.link,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600),
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
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';
|
||||
|
||||
enum InputType { url, json }
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
class Home extends StatelessWidget {
|
||||
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(
|
||||
@@ -26,78 +29,130 @@ class _HomeState extends State<Home> {
|
||||
border: null,
|
||||
backgroundColor: CupertinoColors.white,
|
||||
),
|
||||
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;
|
||||
});
|
||||
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();
|
||||
}
|
||||
},
|
||||
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,117 +1,81 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import '../../../common/models/option_data.dart';
|
||||
import '../../../common/extensions/options_list_extension.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nut_player_example/src/features/main_screen/domain/json_bloc/json_bloc.dart';
|
||||
import 'options_list.dart';
|
||||
import 'url_input.dart';
|
||||
|
||||
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';
|
||||
class JSONView extends StatelessWidget {
|
||||
const JSONView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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) {
|
||||
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);
|
||||
});
|
||||
}),
|
||||
]);
|
||||
return BlocBuilder<JsonBloc, JsonState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: _buildAllWidgets(context, state, null)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _resetSecondStep() {
|
||||
setState(() {
|
||||
_firebaseConfigs.unselectAll();
|
||||
_waysToGetUrlpath.unselectAll();
|
||||
_resetThirdStep();
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
void _resetThirdStep() {
|
||||
setState(() {
|
||||
_linksExamples.unselectAll();
|
||||
_currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
|
||||
if (widget.isReady != null) {
|
||||
widget.isReady!(false);
|
||||
}
|
||||
});
|
||||
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,
|
||||
(selectedIndex) {
|
||||
final createIndexed = state.createIndexedEvent;
|
||||
if (createIndexed != null) {
|
||||
context.read<JsonBloc>().add(createIndexed(selectedIndex));
|
||||
}
|
||||
},
|
||||
key: state.key
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLoader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
width: double.infinity,
|
||||
height: 80,
|
||||
child: const CupertinoActivityIndicator(radius: 20),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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';
|
||||
|
||||
@@ -8,11 +10,12 @@ 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.title, this.type, this.onSelection, {super.key});
|
||||
const OptionsList(this.options, this.selectedOptionIndex, this.title, this.type, this.onSelection, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -27,6 +30,7 @@ class OptionsList extends StatelessWidget {
|
||||
Text(title!,
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CupertinoColors.black)),
|
||||
@@ -47,24 +51,32 @@ class OptionsList extends StatelessWidget {
|
||||
color: CupertinoColors.separator);
|
||||
},
|
||||
itemBuilder: (_, int newIndex) {
|
||||
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;
|
||||
}
|
||||
return BlocBuilder<JsonBloc, JsonState>(
|
||||
builder: (context, state) {
|
||||
return _buildListOptionWidget(type, newIndex, selectedOptionIndex, context.read<JsonBloc>());
|
||||
},
|
||||
);
|
||||
}))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,9 +3,10 @@ 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.onTap, {super.key});
|
||||
const RadioListOption(this.data, this.isSelected, this.onTap, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -18,10 +19,10 @@ class RadioListOption extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
border: Border.all(color: CupertinoColors.link, width: 2.5),
|
||||
color: data.isSelected ? CupertinoColors.link : CupertinoColors.white
|
||||
color: isSelected ? CupertinoColors.link : CupertinoColors.white
|
||||
),
|
||||
),
|
||||
title: Text(data.title, style: const TextStyle(fontSize: 17))
|
||||
title: Text(data.title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,27 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class URLInput extends StatefulWidget {
|
||||
class URLInput extends StatelessWidget {
|
||||
final String title;
|
||||
final String placeholder;
|
||||
final Function(bool)? isTextFieldEmpty;
|
||||
final String initialText;
|
||||
final Function(String)? newText;
|
||||
|
||||
const URLInput(this.title, this.placeholder, this.isTextFieldEmpty, {super.key});
|
||||
URLInput(this.title, this.initialText, this.newText, {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(widget.title,
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CupertinoColors.black)),
|
||||
@@ -50,6 +48,7 @@ class _URLInputState extends State<URLInput> {
|
||||
onPressed: _getClipboardText,
|
||||
child: const Text('Вставка',
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CupertinoColors.link))),
|
||||
@@ -57,6 +56,7 @@ class _URLInputState extends State<URLInput> {
|
||||
onPressed: _cleanTextField,
|
||||
child: const Text('Очистить',
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CupertinoColors.link)))
|
||||
@@ -69,20 +69,12 @@ class _URLInputState extends State<URLInput> {
|
||||
|
||||
void _getClipboardText() async {
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
setState(() {
|
||||
_controller.text = clipboardData?.text ?? '';
|
||||
if (widget.isTextFieldEmpty != null) {
|
||||
widget.isTextFieldEmpty!(_controller.text.isEmpty);
|
||||
}
|
||||
});
|
||||
_controller.text = clipboardData?.text ?? '';
|
||||
newText?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _cleanTextField() async {
|
||||
setState(() {
|
||||
_controller.text = '';
|
||||
if (widget.isTextFieldEmpty != null) {
|
||||
widget.isTextFieldEmpty!(true);
|
||||
}
|
||||
});
|
||||
_controller.text = '';
|
||||
newText?.call('');
|
||||
}
|
||||
}
|
||||
@@ -1,169 +1,108 @@
|
||||
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 StatefulWidget {
|
||||
final Function(bool)? isReady;
|
||||
class URLView extends StatelessWidget {
|
||||
|
||||
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;
|
||||
const URLView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
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);
|
||||
}
|
||||
})
|
||||
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);
|
||||
}
|
||||
});
|
||||
})
|
||||
]
|
||||
return BlocBuilder<UrlBloc, UrlState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: _buildAllWidgets(context, state, null)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _resetSecondStep() {
|
||||
setState(() {
|
||||
_videoExamples.unselectAll();
|
||||
_waysToGetUrl.unselectAll();
|
||||
_resetThirdStep();
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
void _resetThirdStep() {
|
||||
setState(() {
|
||||
_extraOptions.unselectAll();
|
||||
_resetFourthStep();
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void _resetFourthStep() {
|
||||
setState(() {
|
||||
_configs.unselectAll();
|
||||
_formats.unselectAll();
|
||||
_currentUrlPath = 'http://chest-101.gc.team:8000/play/opt/5e8c7f78f0fd49e1be859dd0dc262';
|
||||
if (widget.isReady != null) {
|
||||
widget.isReady!(false);
|
||||
}
|
||||
});
|
||||
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));
|
||||
}
|
||||
},
|
||||
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));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
class Subtitle {
|
||||
final String id;
|
||||
final String title;
|
||||
|
||||
Subtitle({required this.id, required this.title});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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,25 +8,24 @@ class PlayerButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 103,
|
||||
child: CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 20),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
return Expanded(
|
||||
child: CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 10),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
color: CupertinoColors.link,
|
||||
child: Text(title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w500
|
||||
)
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w500)
|
||||
),
|
||||
onPressed: () {
|
||||
if (onPressed != null) {
|
||||
onPressed!();
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
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(':');
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
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,173 +1,392 @@
|
||||
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 StatelessWidget {
|
||||
PlayerView({super.key});
|
||||
class PlayerView extends StatefulWidget {
|
||||
const PlayerView({super.key});
|
||||
|
||||
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);
|
||||
@override
|
||||
State<PlayerView> createState() => _PlayerViewState();
|
||||
}
|
||||
|
||||
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) {
|
||||
_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.';
|
||||
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
|
||||
]);
|
||||
return CupertinoPageScaffold(
|
||||
backgroundColor: CupertinoColors.extraLightBackgroundGray,
|
||||
navigationBar: const CupertinoNavigationBar(
|
||||
key: Key('PlayerAppBarID'),
|
||||
middle: Text('Демо NutPlayer',
|
||||
style: TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w600)
|
||||
),
|
||||
leading: CupertinoNavigationBarBackButton(previousPageTitle: 'Назад'),
|
||||
padding: EdgeInsetsDirectional.all(0),
|
||||
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),
|
||||
backgroundColor: CupertinoColors.white,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(aspectRatio: 16/10,
|
||||
// TODO: Встроить плеер
|
||||
child: Container(
|
||||
color: CupertinoColors.link,
|
||||
)
|
||||
),
|
||||
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());
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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)
|
||||
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)),
|
||||
),
|
||||
),
|
||||
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()]
|
||||
),
|
||||
children: _buildDynamicWidgets(PlayerViewController.playbackSettings, _bloc)
|
||||
),
|
||||
CupertinoListSection.insetGrouped(
|
||||
margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
|
||||
hasLeading: false,
|
||||
children: _buildWidgets([PlayerViewController.startPosition], _bloc)
|
||||
),
|
||||
|
||||
CupertinoListSection.insetGrouped(
|
||||
margin: const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
|
||||
hasLeading: false,
|
||||
children: <Widget>[InputView(_startPositionOption)]
|
||||
),
|
||||
if (context.read<SettingsRepository>().log != RepositoryLogType.off)
|
||||
_buildLog()
|
||||
]
|
||||
)
|
||||
)
|
||||
]);
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
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,
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
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: 'Отладка')
|
||||
};
|
||||
}
|
||||
|
||||
+170
-211
@@ -1,138 +1,40 @@
|
||||
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';
|
||||
|
||||
enum LogType { off, info, debug }
|
||||
class SettingsView extends StatelessWidget {
|
||||
|
||||
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: const CupertinoNavigationBar(
|
||||
key: Key('SettingsAppBarID'),
|
||||
middle: Text('Настройки', style: TextStyle(color: CupertinoColors.black, fontWeight: FontWeight.w600)),
|
||||
leading: CupertinoNavigationBarBackButton(previousPageTitle: 'Назад'),
|
||||
padding: EdgeInsetsDirectional.all(0),
|
||||
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),
|
||||
backgroundColor: CupertinoColors.white,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
child: BlocBuilder<SettingsBloc, SettingsState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<SettingsBloc>();
|
||||
return Column(
|
||||
children: [
|
||||
// НАСТРОЙКИ СКИНА
|
||||
CupertinoListSection.insetGrouped(
|
||||
@@ -140,14 +42,10 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
header: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
||||
child: Text('Выбор скина'.toUpperCase(),
|
||||
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
),
|
||||
),
|
||||
children: <Widget>[..._skinOptions.map((value) {
|
||||
return CheckmarkListOption(
|
||||
value, () {},
|
||||
);
|
||||
}).toList()]
|
||||
children: _buildSkinSettings(bloc)
|
||||
),
|
||||
|
||||
// НАСТРОЙКИ СТАНДАРТНОГО СКИНА
|
||||
@@ -156,18 +54,10 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
header: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
||||
child: Text('Настройки стандартного скина'.toUpperCase(),
|
||||
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
),
|
||||
),
|
||||
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()]
|
||||
children: _buildSettingsWidgets(SettingsInitialState.standardSkinSettings, bloc)
|
||||
),
|
||||
|
||||
// ВОСПРОИЗВЕДЕНИЕ
|
||||
@@ -176,28 +66,10 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
header: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
||||
child: Text('Воспроизведение'.toUpperCase(),
|
||||
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
),
|
||||
),
|
||||
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)]
|
||||
]
|
||||
children: _buildSettingsWidgets(SettingsInitialState.playbackOptions, bloc)
|
||||
),
|
||||
|
||||
// ДОПОЛНИТЕЛЬНО
|
||||
@@ -206,16 +78,16 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
header: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
||||
child: Text('Дополнительно'.toUpperCase(),
|
||||
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
style: const TextStyle(decoration: TextDecoration.none, 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(color: CupertinoColors.systemGrey, fontSize: 12, fontWeight: FontWeight.w400)
|
||||
style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 12, fontWeight: FontWeight.w400)
|
||||
),
|
||||
),
|
||||
children: [ToggleView(_extraOption)]
|
||||
children: _buildSettingsWidgets([SettingsInitialState.extraOption], bloc)
|
||||
),
|
||||
|
||||
// ЗАДЕРЖКИ
|
||||
@@ -224,12 +96,10 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
header: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
||||
child: Text('Настройка задержек (только для HLS)'.toUpperCase(),
|
||||
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
),
|
||||
),
|
||||
children: <Widget>[..._timeoutsOptions.map((value) {
|
||||
return InputView(value);
|
||||
}).toList()]
|
||||
children: _buildSettingsWidgets(SettingsInitialState.timeoutsOptions, bloc)
|
||||
),
|
||||
|
||||
// Логирование
|
||||
@@ -238,7 +108,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
header: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
||||
child: Text('Логирование'.toUpperCase(),
|
||||
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
),
|
||||
),
|
||||
children: [
|
||||
@@ -248,49 +118,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
color: CupertinoColors.systemGrey3,
|
||||
border: Border.all(color: CupertinoColors.systemGrey3)
|
||||
),
|
||||
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)
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
child: _buildLog(context)
|
||||
),
|
||||
]
|
||||
),
|
||||
@@ -301,7 +129,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
header: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
||||
child: Text('Firebase'.toUpperCase(),
|
||||
style: const TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
style: const TextStyle(decoration: TextDecoration.none, color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400)
|
||||
),
|
||||
),
|
||||
children: [
|
||||
@@ -311,9 +139,8 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
alignment: Alignment.centerLeft,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
child: const Text('Проверка падения приложения',
|
||||
style: TextStyle(color: CupertinoColors.black, fontSize: 17, fontWeight: FontWeight.w400)),
|
||||
onPressed: (){}
|
||||
child: const Text('Проверка падения приложения', style: TextStyle(decoration: TextDecoration.none, color: CupertinoColors.black, fontSize: 17, fontWeight: FontWeight.w400)),
|
||||
onPressed: () { bloc.add(CrashEvent()); }
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -322,13 +149,145 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
// ВЕРСИЯ
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: const Text('VERSION 1.2.0 (1063)',
|
||||
style: TextStyle(color: CupertinoColors.systemGrey, fontSize: 13, fontWeight: FontWeight.w400))
|
||||
child: FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
return _buildVersion(snapshot, bloc.state);
|
||||
})
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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,31 +1,24 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:nut_player_example/src/common/models/option_data.dart';
|
||||
|
||||
class ToggleView extends StatefulWidget {
|
||||
final OptionData data;
|
||||
class ToggleView extends StatelessWidget {
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
final Function(bool)? onChange;
|
||||
|
||||
const ToggleView(this.data, {super.key});
|
||||
|
||||
@override
|
||||
State<ToggleView> createState() => _ToggleViewState();
|
||||
}
|
||||
|
||||
class _ToggleViewState extends State<ToggleView> {
|
||||
const ToggleView({super.key, required this.title, required this.isSelected, this.onChange});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoListTile(
|
||||
key: widget.data.key,
|
||||
key: key,
|
||||
trailing: CupertinoSwitch(
|
||||
value: widget.data.isSelected,
|
||||
value: isSelected,
|
||||
activeColor: CupertinoColors.activeGreen,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
widget.data.isSelected = value;
|
||||
});
|
||||
onChange?.call(value);
|
||||
},
|
||||
),
|
||||
title: Text(widget.data.title, style: const TextStyle(fontSize: 17))
|
||||
title: Text(title, style: const TextStyle(decoration: TextDecoration.none, fontSize: 17))
|
||||
);
|
||||
}
|
||||
}
|
||||
+252
-17
@@ -1,6 +1,14 @@
|
||||
# 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:
|
||||
@@ -9,6 +17,14 @@ 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:
|
||||
@@ -37,10 +53,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.2"
|
||||
version: "1.18.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -65,11 +81,83 @@ 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
|
||||
@@ -88,16 +176,53 @@ 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:
|
||||
@@ -126,10 +251,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.10.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nut_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -137,6 +270,20 @@ 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:
|
||||
path: "../../nut_player_ios"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
nut_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -144,6 +291,22 @@ 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:
|
||||
@@ -156,10 +319,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -176,6 +339,62 @@ 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
|
||||
@@ -193,18 +412,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -233,10 +452,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
version: "0.6.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -249,18 +476,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f
|
||||
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.7.1"
|
||||
version: "11.10.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4-beta"
|
||||
version: "0.3.0"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -269,6 +496,14 @@ 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.1.2 <4.0.0"
|
||||
dart: ">=3.2.0-194.0.dev <4.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
||||
@@ -16,6 +16,10 @@ 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:
|
||||
@@ -28,6 +32,9 @@ 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:
|
||||
@@ -47,7 +54,6 @@ dev_dependencies:
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
@@ -56,6 +62,7 @@ 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
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
export 'src/legacy/closed_caption_file.dart';
|
||||
export 'src/legacy/video_player_value.dart';
|
||||
export 'src/legacy/video_player_value.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';
|
||||
@@ -0,0 +1,423 @@
|
||||
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;
|
||||
@@ -1,69 +0,0 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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,522 +1,11 @@
|
||||
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,
|
||||
@@ -524,13 +13,6 @@ 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;
|
||||
@@ -568,56 +50,4 @@ 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,7 +1,6 @@
|
||||
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].
|
||||
@@ -13,17 +12,18 @@ class VideoPlayerValue {
|
||||
required this.duration,
|
||||
this.size = Size.zero,
|
||||
this.position = Duration.zero,
|
||||
this.caption = Caption.none,
|
||||
this.captionOffset = Duration.zero,
|
||||
this.subtitles,
|
||||
this.qualities,
|
||||
this.buffered = const <DurationRange>[],
|
||||
this.isInitialized = false,
|
||||
this.isPlaying = false,
|
||||
this.isLooping = false,
|
||||
this.isBuffering = false,
|
||||
this.volume = 1.0,
|
||||
this.volume,
|
||||
this.playbackSpeed = 1.0,
|
||||
this.rotationCorrection = 0,
|
||||
this.errorDescription,
|
||||
this.isCompleted = false,
|
||||
});
|
||||
|
||||
/// Returns an instance for a video that hasn't been loaded.
|
||||
@@ -49,16 +49,12 @@ class VideoPlayerValue {
|
||||
/// The current playback position.
|
||||
final Duration position;
|
||||
|
||||
/// 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;
|
||||
/// 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 [Duration] that should be used to offset the current [position] to get the correct [Caption].
|
||||
///
|
||||
/// Defaults to Duration.zero.
|
||||
final Duration captionOffset;
|
||||
/// Текущие доступные качества видео. Если их нет, массив словарей будет пустым.
|
||||
final List<Map<String, dynamic>>? qualities;
|
||||
|
||||
/// The currently buffered ranges.
|
||||
final List<DurationRange> buffered;
|
||||
@@ -73,7 +69,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;
|
||||
@@ -83,6 +79,8 @@ 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;
|
||||
|
||||
@@ -119,8 +117,8 @@ class VideoPlayerValue {
|
||||
Duration? duration,
|
||||
Size? size,
|
||||
Duration? position,
|
||||
Caption? caption,
|
||||
Duration? captionOffset,
|
||||
Map<String, String>? subtitles,
|
||||
List<Map<String, dynamic>>? qualities,
|
||||
List<DurationRange>? buffered,
|
||||
bool? isInitialized,
|
||||
bool? isPlaying,
|
||||
@@ -130,13 +128,14 @@ 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,
|
||||
caption: caption ?? this.caption,
|
||||
captionOffset: captionOffset ?? this.captionOffset,
|
||||
subtitles: subtitles ?? this.subtitles,
|
||||
qualities: qualities ?? this.qualities,
|
||||
buffered: buffered ?? this.buffered,
|
||||
isInitialized: isInitialized ?? this.isInitialized,
|
||||
isPlaying: isPlaying ?? this.isPlaying,
|
||||
@@ -148,6 +147,7 @@ class VideoPlayerValue {
|
||||
errorDescription: errorDescription != _defaultErrorDescription
|
||||
? errorDescription
|
||||
: this.errorDescription,
|
||||
isCompleted: isCompleted ?? this.isCompleted,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ class VideoPlayerValue {
|
||||
'duration: $duration, '
|
||||
'size: $size, '
|
||||
'position: $position, '
|
||||
'caption: $caption, '
|
||||
'captionOffset: $captionOffset, '
|
||||
'subtitles: $subtitles, '
|
||||
'qualities: $qualities, '
|
||||
'buffered: [${buffered.join(', ')}], '
|
||||
'isInitialized: $isInitialized, '
|
||||
'isPlaying: $isPlaying, '
|
||||
@@ -166,7 +166,8 @@ class VideoPlayerValue {
|
||||
'isBuffering: $isBuffering, '
|
||||
'volume: $volume, '
|
||||
'playbackSpeed: $playbackSpeed, '
|
||||
'errorDescription: $errorDescription)';
|
||||
'errorDescription: $errorDescription, '
|
||||
'isCompleted: $isCompleted), ';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -176,8 +177,8 @@ class VideoPlayerValue {
|
||||
runtimeType == other.runtimeType &&
|
||||
duration == other.duration &&
|
||||
position == other.position &&
|
||||
caption == other.caption &&
|
||||
captionOffset == other.captionOffset &&
|
||||
subtitles == other.subtitles &&
|
||||
listEquals(qualities, other.qualities) &&
|
||||
listEquals(buffered, other.buffered) &&
|
||||
isPlaying == other.isPlaying &&
|
||||
isLooping == other.isLooping &&
|
||||
@@ -187,14 +188,15 @@ class VideoPlayerValue {
|
||||
errorDescription == other.errorDescription &&
|
||||
size == other.size &&
|
||||
rotationCorrection == other.rotationCorrection &&
|
||||
isInitialized == other.isInitialized;
|
||||
isInitialized == other.isInitialized &&
|
||||
isCompleted == other.isCompleted;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
duration,
|
||||
position,
|
||||
caption,
|
||||
captionOffset,
|
||||
subtitles,
|
||||
qualities,
|
||||
buffered,
|
||||
isPlaying,
|
||||
isLooping,
|
||||
@@ -205,5 +207,6 @@ class VideoPlayerValue {
|
||||
size,
|
||||
rotationCorrection,
|
||||
isInitialized,
|
||||
isCompleted,
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'video_progress_colors.dart';
|
||||
import 'video_player_controller.dart';
|
||||
import '../controller/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 'video_player_controller.dart';
|
||||
import '../controller/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.seekTo(position);
|
||||
controller.seek(position);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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 []});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
enum SubtitleType { srt, unknown }
|
||||
|
||||
abstract class PlayerSubtitleRecord {
|
||||
/// Название субтитров.
|
||||
/// Используется для вывода в меню плеера
|
||||
String get title;
|
||||
/// Тип субтитров
|
||||
SubtitleType get type;
|
||||
/// Ссылка на файл субтитров
|
||||
String get url;
|
||||
/// Язык на котором отображаются субтитры.
|
||||
/// Соответствует стандарту ISO 639-2
|
||||
String get language;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
class JsonBadStatusCodeError extends Error {}
|
||||
class JsonNoPlaybacksError extends Error {}
|
||||
class JsonIncorrectUrlError extends Error {}
|
||||
class JsonUnknownFormatError extends Error {}
|
||||
@@ -0,0 +1,26 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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
Reference in New Issue
Block a user