Merge branch 'dev' into dependabot/pub/dev/flutter_native_splash-2.3.10
@ -1,4 +1,4 @@
|
||||
{
|
||||
"flutterSdkVersion": "3.16.0",
|
||||
"flutterSdkVersion": "3.19.1",
|
||||
"flavors": {}
|
||||
}
|
||||
4
.github/workflows/spotube-release-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to release (x.x.x)
|
||||
default: 3.4.0
|
||||
default: 3.4.1
|
||||
required: true
|
||||
channel:
|
||||
type: choice
|
||||
@ -26,7 +26,7 @@ on:
|
||||
default: true
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: '3.16.3'
|
||||
FLUTTER_VERSION: '3.19.1'
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [3.5.0](https://personal.github.com/krtirtho/spotube/compare/v3.4.0...v3.5.0) (2024-01-27)
|
||||
## [3.4.1](https://personal.github.com/krtirtho/spotube/compare/v3.4.0...v3.4.1) (2024-01-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
1
Makefile
@ -28,6 +28,7 @@ publishaur:
|
||||
|
||||
innoinstall:
|
||||
powershell curl -o build\installer.exe http://files.jrsoftware.org/is/6/innosetup-${INNO_VERSION}.exe
|
||||
powershell git clone https://github.com/DomGries/InnoDependencyInstaller.git build\inno-depend
|
||||
powershell build\installer.exe /verysilent /allusers /dir=build\iscc
|
||||
|
||||
inno:
|
||||
|
||||
@ -31,4 +31,6 @@ linter:
|
||||
analyzer:
|
||||
enable-experiment:
|
||||
- records
|
||||
- patterns
|
||||
- patterns
|
||||
errors:
|
||||
invalid_annotation_target: ignore
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
BIN
assets/logos/songlink-transparent.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/logos/songlink.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@ -196,7 +196,7 @@ SPEC CHECKSUMS:
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
||||
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
@ -221,6 +221,6 @@ SPEC CHECKSUMS:
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
|
||||
PODFILE CHECKSUM: e36c7ad9836dfd8d22934c7680185432a658e28f
|
||||
PODFILE CHECKSUM: 5129d2e80ab0dfc533f262cedf032011b1dfe4fd
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@ -406,7 +406,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@ -1056,6 +1056,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1078,6 +1079,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1099,6 +1101,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1198,6 +1201,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1294,6 +1298,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1387,6 +1392,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1408,6 +1414,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1430,6 +1437,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1452,6 +1460,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1473,6 +1482,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1494,6 +1504,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1515,6 +1526,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1614,6 +1626,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1636,6 +1649,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1732,6 +1746,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1753,6 +1768,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1846,6 +1862,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1867,6 +1884,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1888,6 +1906,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1910,6 +1929,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1932,6 +1952,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1954,6 +1975,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1975,6 +1997,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1996,6 +2019,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2017,6 +2041,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2038,6 +2063,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2059,6 +2085,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2158,6 +2185,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2180,6 +2208,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2202,6 +2231,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2298,6 +2328,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2319,6 +2350,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2340,6 +2372,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2433,6 +2466,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2454,6 +2488,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2475,6 +2510,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@ -1,66 +1,70 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Spotube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app require access to the device camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require access to the device microphone</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Spotube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app require access to the device camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require access to the device microphone</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false />
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
</dict>
|
||||
</plist>
|
||||
@ -41,6 +41,10 @@
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
@ -1,66 +1,70 @@
|
||||
<?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>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Spotube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Spotube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app require access to the device camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require access to the device microphone</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app require access to the device camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require access to the device microphone</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -1,66 +1,70 @@
|
||||
<?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>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Spotube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Spotube</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>spotube</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app require access to the device camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require access to the device microphone</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app require access to the device camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require access to the device microphone</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app require access to the photo library</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -9,6 +9,21 @@
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class $AssetsLogosGen {
|
||||
const $AssetsLogosGen();
|
||||
|
||||
/// File path: assets/logos/songlink-transparent.png
|
||||
AssetGenImage get songlinkTransparent =>
|
||||
const AssetGenImage('assets/logos/songlink-transparent.png');
|
||||
|
||||
/// File path: assets/logos/songlink.png
|
||||
AssetGenImage get songlink =>
|
||||
const AssetGenImage('assets/logos/songlink.png');
|
||||
|
||||
/// List of all assets
|
||||
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
||||
}
|
||||
|
||||
class $AssetsTutorialGen {
|
||||
const $AssetsTutorialGen();
|
||||
|
||||
@ -37,6 +52,7 @@ class Assets {
|
||||
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
|
||||
static const AssetGenImage likedTracks =
|
||||
AssetGenImage('assets/liked-tracks.jpg');
|
||||
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
||||
static const AssetGenImage placeholder =
|
||||
AssetGenImage('assets/placeholder.png');
|
||||
static const AssetGenImage spotubeHeroBanner =
|
||||
|
||||
@ -4,8 +4,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:spotube/components/player/player_controls.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/components/player/player_controls.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
@ -64,6 +64,7 @@ class HomeTabIntent extends Intent {
|
||||
class HomeTabAction extends Action<HomeTabIntent> {
|
||||
@override
|
||||
invoke(intent) {
|
||||
final router = intent.ref.read(routerProvider);
|
||||
switch (intent.tab) {
|
||||
case HomeTabs.browse:
|
||||
router.go("/");
|
||||
|
||||
@ -6,6 +6,11 @@ class ISOLanguageName {
|
||||
required this.name,
|
||||
required this.nativeName,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$name ($nativeName)";
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment the languages as we add support for them
|
||||
|
||||
@ -2,8 +2,10 @@ import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/foundation.dart' hide Category;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart' hide Search;
|
||||
import 'package:spotube/pages/album/album.dart';
|
||||
import 'package:spotube/pages/getting_started/getting_started.dart';
|
||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||
import 'package:spotube/pages/home/genres/genres.dart';
|
||||
import 'package:spotube/pages/home/home.dart';
|
||||
@ -18,6 +20,8 @@ import 'package:spotube/pages/settings/blacklist.dart';
|
||||
import 'package:spotube/pages/settings/about.dart';
|
||||
import 'package:spotube/pages/settings/logs.dart';
|
||||
import 'package:spotube/pages/track/track.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/components/shared/spotube_page_route.dart';
|
||||
import 'package:spotube/pages/artist/artist.dart';
|
||||
@ -31,157 +35,180 @@ import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
||||
|
||||
final rootNavigatorKey = Catcher2.navigatorKey;
|
||||
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final router = GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: [
|
||||
ShellRoute(
|
||||
navigatorKey: shellRouteNavigatorKey,
|
||||
builder: (context, state, child) => RootApp(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
pageBuilder: (context, state) => const SpotubePage(child: HomePage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "genres",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: GenrePage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "genre/:categoryId",
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: GenrePlaylistsPage(
|
||||
category: state.extra as Category,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/search",
|
||||
name: "Search",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: SearchPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/library",
|
||||
name: "Library",
|
||||
final routerProvider = Provider((ref) {
|
||||
return GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: [
|
||||
ShellRoute(
|
||||
navigatorKey: shellRouteNavigatorKey,
|
||||
builder: (context, state, child) => RootApp(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
redirect: (context, state) async {
|
||||
final authNotifier =
|
||||
ref.read(AuthenticationNotifier.provider.notifier);
|
||||
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
||||
|
||||
if (json["cookie"] == null &&
|
||||
!KVStoreService.doneGettingStarted) {
|
||||
return "/getting-started";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LibraryPage()),
|
||||
const SpotubePage(child: HomePage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "generate",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "result",
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: PlaylistGenerateResultPage(
|
||||
state:
|
||||
state.extra as PlaylistGenerateResultRouteState,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
GoRoute(
|
||||
path: "/lyrics",
|
||||
name: "Lyrics",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LyricsPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/settings",
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: SettingsPage(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "blacklist",
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const BlackListPage(),
|
||||
path: "genres",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: GenrePage()),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
GoRoute(
|
||||
path: "logs",
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const LogsPage(),
|
||||
path: "genre/:categoryId",
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: GenrePlaylistsPage(
|
||||
category: state.extra as Category,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "about",
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const AboutSpotube(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/search",
|
||||
name: "Search",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: SearchPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/library",
|
||||
name: "Library",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LibraryPage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "generate",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "result",
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: PlaylistGenerateResultPage(
|
||||
state:
|
||||
state.extra as PlaylistGenerateResultRouteState,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
GoRoute(
|
||||
path: "/lyrics",
|
||||
name: "Lyrics",
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LyricsPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/settings",
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: SettingsPage(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/album/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.extra is AlbumSimple);
|
||||
return SpotubePage(
|
||||
child: AlbumPage(album: state.extra as AlbumSimple),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/artist/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.pathParameters["id"] != null);
|
||||
return SpotubePage(child: ArtistPage(state.pathParameters["id"]!));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/playlist/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.extra is PlaylistSimple);
|
||||
return SpotubePage(
|
||||
child: state.pathParameters["id"] == "user-liked-tracks"
|
||||
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
|
||||
: PlaylistPage(playlist: state.extra as PlaylistSimple),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/track/:id",
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.pathParameters["id"]!;
|
||||
return SpotubePage(
|
||||
child: TrackPage(trackId: id),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/mini-player",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "blacklist",
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const BlackListPage(),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
GoRoute(
|
||||
path: "logs",
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const LogsPage(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "about",
|
||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||
child: const AboutSpotube(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/album/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.extra is AlbumSimple);
|
||||
return SpotubePage(
|
||||
child: AlbumPage(album: state.extra as AlbumSimple),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/artist/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.pathParameters["id"] != null);
|
||||
return SpotubePage(
|
||||
child: ArtistPage(state.pathParameters["id"]!));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/playlist/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.extra is PlaylistSimple);
|
||||
return SpotubePage(
|
||||
child: state.pathParameters["id"] == "user-liked-tracks"
|
||||
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
|
||||
: PlaylistPage(playlist: state.extra as PlaylistSimple),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/track/:id",
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.pathParameters["id"]!;
|
||||
return SpotubePage(
|
||||
child: TrackPage(trackId: id),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
||||
GoRoute(
|
||||
path: "/mini-player",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login-tutorial",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: LoginTutorial(),
|
||||
GoRoute(
|
||||
path: "/getting-started",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: GettingStarting(),
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/lastfm-login",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LastFMLoginPage()),
|
||||
),
|
||||
],
|
||||
);
|
||||
GoRoute(
|
||||
path: "/login",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login-tutorial",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: LoginTutorial(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/lastfm-login",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) =>
|
||||
const SpotubePage(child: LastFMLoginPage()),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@ -111,4 +111,8 @@ abstract class SpotubeIcons {
|
||||
static const wikipedia = SimpleIcons.wikipedia;
|
||||
static const discord = SimpleIcons.discord;
|
||||
static const youtube = SimpleIcons.youtube;
|
||||
static const radio = FeatherIcons.radio;
|
||||
static const github = SimpleIcons.github;
|
||||
static const openCollective = SimpleIcons.opencollective;
|
||||
static const anonymous = FeatherIcons.user;
|
||||
}
|
||||
|
||||
31
lib/components/getting_started/blur_card.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class BlurCard extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
const BlurCard({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,6 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
@ -69,22 +72,27 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final group in friendGroup)
|
||||
Row(
|
||||
children: [
|
||||
for (final friend in group)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FriendItem(friend: friend),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: PointerDeviceKind.values.toSet(),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final group in friendGroup)
|
||||
Row(
|
||||
children: [
|
||||
for (final friend in group)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FriendItem(friend: friend),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -50,10 +50,11 @@ enum SortBy {
|
||||
none,
|
||||
ascending,
|
||||
descending,
|
||||
artist,
|
||||
album,
|
||||
newest,
|
||||
oldest,
|
||||
duration,
|
||||
artist,
|
||||
album,
|
||||
}
|
||||
|
||||
final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
||||
|
||||
@ -25,6 +25,7 @@ import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PlayerView extends HookConsumerWidget {
|
||||
final PanelController panelController;
|
||||
@ -94,10 +95,10 @@ class PlayerView extends HookConsumerWidget {
|
||||
|
||||
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) async {
|
||||
panelController.close();
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await panelController.close();
|
||||
return false;
|
||||
},
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme.copyWith(color: bodyTextColor),
|
||||
@ -137,6 +138,21 @@ class PlayerView extends HookConsumerWidget {
|
||||
onPressed: panelController.close,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Assets.logos.songlink.image(
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
tooltip: context.l10n.song_link,
|
||||
onPressed: currentTrack == null
|
||||
? null
|
||||
: () {
|
||||
final url =
|
||||
"https://song.link/s/${currentTrack.id}";
|
||||
|
||||
launchUrlString(url);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||
tooltip: context.l10n.details,
|
||||
|
||||
@ -15,8 +15,6 @@ class InterScrollbar extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (DesktopTools.platform.isDesktop) return child;
|
||||
|
||||
return DraggableScrollbar.semicircle(
|
||||
|
||||
@ -48,6 +48,11 @@ class SortTracksDropdown extends StatelessWidget {
|
||||
enabled: value != SortBy.oldest,
|
||||
title: Text(context.l10n.sort_oldest),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: SortBy.duration,
|
||||
enabled: value != SortBy.duration,
|
||||
title: Text(context.l10n.sort_duration),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: SortBy.artist,
|
||||
enabled: value != SortBy.artist,
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
|
||||
import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
@ -20,12 +23,16 @@ import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
enum TrackOptionValue {
|
||||
album,
|
||||
share,
|
||||
songlink,
|
||||
addToPlaylist,
|
||||
addToQueue,
|
||||
removeFromPlaylist,
|
||||
@ -36,6 +43,7 @@ enum TrackOptionValue {
|
||||
favorite,
|
||||
details,
|
||||
download,
|
||||
startRadio,
|
||||
}
|
||||
|
||||
class TrackOptions extends HookConsumerWidget {
|
||||
@ -82,11 +90,85 @@ class TrackOptions extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void actionStartRadio(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Track track,
|
||||
) async {
|
||||
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
final spotify = ref.read(spotifyProvider);
|
||||
final query = "${track.name} Radio";
|
||||
final pages = await QueryClient.of(context)
|
||||
.fetchInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
|
||||
job: SearchQueries.queryJob(query),
|
||||
args: (
|
||||
spotify: spotify,
|
||||
searchType: SearchType.playlist,
|
||||
query: query,
|
||||
),
|
||||
) ??
|
||||
[];
|
||||
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
||||
.toList()
|
||||
.cast<PlaylistSimple>();
|
||||
|
||||
final artists = track.artists!.map((e) => e.name);
|
||||
|
||||
final radio = radios.firstWhere(
|
||||
(e) {
|
||||
final validPlaylists =
|
||||
artists.where((a) => e.description!.contains(a!));
|
||||
return e.name == "${track.name} Radio" &&
|
||||
(validPlaylists.length >= 2 ||
|
||||
validPlaylists.length == artists.length) &&
|
||||
e.owner?.displayName == "Spotify";
|
||||
},
|
||||
orElse: () => radios.first,
|
||||
);
|
||||
|
||||
bool replaceQueue = false;
|
||||
|
||||
if (context.mounted && playlist.tracks.isNotEmpty) {
|
||||
replaceQueue = await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n.how_to_start_radio,
|
||||
message: context.l10n.replace_queue_question,
|
||||
okText: context.l10n.replace,
|
||||
cancelText: context.l10n.add_to_queue,
|
||||
);
|
||||
}
|
||||
|
||||
if (replaceQueue || playlist.tracks.isEmpty) {
|
||||
await playback.stop();
|
||||
await playback.load([track], autoPlay: true);
|
||||
|
||||
// we don't have to add those tracks as useEndlessPlayback will do it for us
|
||||
return;
|
||||
} else {
|
||||
await playback.addTrack(track);
|
||||
}
|
||||
|
||||
final tracks =
|
||||
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
|
||||
|
||||
await playback.addTracks(
|
||||
tracks.toList()
|
||||
..removeWhere((e) {
|
||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||
return e.id == track.id || isDuplicate;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final router = GoRouter.of(context);
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
@ -198,6 +280,10 @@ class TrackOptions extends HookConsumerWidget {
|
||||
case TrackOptionValue.share:
|
||||
actionShare(context, track);
|
||||
break;
|
||||
case TrackOptionValue.songlink:
|
||||
final url = "https://song.link/s/${track.id}";
|
||||
await launchUrlString(url);
|
||||
break;
|
||||
case TrackOptionValue.details:
|
||||
showDialog(
|
||||
context: context,
|
||||
@ -207,6 +293,9 @@ class TrackOptions extends HookConsumerWidget {
|
||||
case TrackOptionValue.download:
|
||||
await downloadManager.addToQueue(track);
|
||||
break;
|
||||
case TrackOptionValue.startRadio:
|
||||
actionStartRadio(context, ref, track);
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
|
||||
@ -287,12 +376,18 @@ class TrackOptions extends HookConsumerWidget {
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
if (auth != null)
|
||||
if (auth != null) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.startRadio,
|
||||
leading: const Icon(SpotubeIcons.radio),
|
||||
title: Text(context.l10n.start_a_radio),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
],
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
@ -331,6 +426,15 @@ class TrackOptions extends HookConsumerWidget {
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.songlink,
|
||||
leading: Assets.logos.songlinkTransparent.image(
|
||||
width: 22,
|
||||
height: 22,
|
||||
color: colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
title: Text(context.l10n.song_link),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
|
||||
@ -70,9 +70,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
||||
tooltip: props.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
onPressed: () {
|
||||
props.onHeart?.call();
|
||||
if (isUserPlaylist) {
|
||||
onPressed: () async {
|
||||
final shouldPop = await props.onHeart?.call();
|
||||
if (isUserPlaylist && shouldPop == true && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart';
|
||||
@ -13,6 +15,7 @@ class TrackView extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final props = InheritedTrackView.of(context);
|
||||
final controller = useScrollController();
|
||||
|
||||
return Scaffold(
|
||||
appBar: DesktopTools.platform.isDesktop
|
||||
@ -29,14 +32,18 @@ class TrackView extends HookConsumerWidget {
|
||||
extendBodyBehindAppBar: true,
|
||||
body: RefreshIndicator(
|
||||
onRefresh: props.pagination.onRefresh,
|
||||
child: const CustomScrollView(
|
||||
slivers: [
|
||||
TrackViewFlexHeader(),
|
||||
SliverAnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 500),
|
||||
child: TrackViewBodySection(),
|
||||
),
|
||||
],
|
||||
child: InterScrollbar(
|
||||
controller: controller,
|
||||
child: CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: const [
|
||||
TrackViewFlexHeader(),
|
||||
SliverAnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 500),
|
||||
child: TrackViewBodySection(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -62,7 +64,7 @@ class InheritedTrackView extends InheritedWidget {
|
||||
final String shareUrl;
|
||||
|
||||
// events
|
||||
final VoidCallback? onHeart; // if null heart button will hidden
|
||||
final FutureOr<bool?> Function()? onHeart; // if null heart button will hidden
|
||||
|
||||
const InheritedTrackView({
|
||||
super.key,
|
||||
|
||||
@ -19,6 +19,8 @@ void useDeepLinking(WidgetRef ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
useEffect(() {
|
||||
void uriListener(List<SharedFile> files) async {
|
||||
for (final file in files) {
|
||||
|
||||
103
lib/hooks/configurators/use_endless_playback.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
|
||||
void useEndlessPlayback(WidgetRef ref) {
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final endlessPlayback =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
|
||||
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (!endlessPlayback || auth == null) return null;
|
||||
|
||||
void listener(int index) async {
|
||||
try {
|
||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
if (index != playlist.tracks.length - 1) return;
|
||||
|
||||
final track = playlist.tracks.last;
|
||||
|
||||
final query = "${track.name} Radio";
|
||||
final pages = await queryClient.fetchInfiniteQueryJob<List<Page>,
|
||||
dynamic, int, SearchParams>(
|
||||
job: SearchQueries.queryJob(query),
|
||||
args: (
|
||||
spotify: spotify,
|
||||
searchType: SearchType.playlist,
|
||||
query: query
|
||||
),
|
||||
) ??
|
||||
[];
|
||||
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
||||
.toList()
|
||||
.cast<PlaylistSimple>();
|
||||
|
||||
final artists = track.artists!.map((e) => e.name);
|
||||
|
||||
final radio = radios.firstWhere(
|
||||
(e) {
|
||||
final validPlaylists =
|
||||
artists.where((a) => e.description!.contains(a!));
|
||||
return e.name == "${track.name} Radio" &&
|
||||
(validPlaylists.length >= 2 ||
|
||||
validPlaylists.length == artists.length) &&
|
||||
e.owner?.displayName != "Spotify";
|
||||
},
|
||||
orElse: () => radios.first,
|
||||
);
|
||||
|
||||
final tracks =
|
||||
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
|
||||
|
||||
await playback.addTracks(
|
||||
tracks.toList()
|
||||
..removeWhere((e) {
|
||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||
return e.id == track.id || isDuplicate;
|
||||
}),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes user can change settings for which the currentIndexChanged
|
||||
// might not be called. So we need to check if the current track is the
|
||||
// last track and if it is then we need to call the listener manually.
|
||||
if (playlist.active == playlist.tracks.length - 1 &&
|
||||
audioPlayer.isPlaying) {
|
||||
listener(playlist.active!);
|
||||
}
|
||||
|
||||
final subscription =
|
||||
audioPlayer.currentIndexChangedStream.listen(listener);
|
||||
|
||||
return subscription.cancel;
|
||||
},
|
||||
[
|
||||
spotify,
|
||||
playback,
|
||||
queryClient,
|
||||
playlist.tracks,
|
||||
endlessPlayback,
|
||||
auth,
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -41,6 +41,7 @@
|
||||
"sort_z_a": "Sort by Z-A",
|
||||
"sort_artist": "Sort by Artist",
|
||||
"sort_album": "Sort by Album",
|
||||
"sort_duration": "Sort by Duration",
|
||||
"sort_tracks": "Sort Tracks",
|
||||
"currently_downloading": "Currently Downloading ({tracks_length})",
|
||||
"cancel_all": "Cancel All",
|
||||
@ -286,5 +287,31 @@
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Explore Genres",
|
||||
"friends": "Friends",
|
||||
"no_lyrics_available": "Sorry, unable find lyrics for this track"
|
||||
"no_lyrics_available": "Sorry, unable find lyrics for this track",
|
||||
"start_a_radio": "Start a Radio",
|
||||
"how_to_start_radio": "How do you want to start the radio?",
|
||||
"replace_queue_question": "Do you want to replace the current queue or append to it?",
|
||||
"endless_playback": "Endless Playback",
|
||||
"delete_playlist": "Delete Playlist",
|
||||
"delete_playlist_confirmation": "Are you sure you want to delete this playlist?",
|
||||
"local_tracks": "Local Tracks",
|
||||
"song_link": "Song Link",
|
||||
"skip_this_nonsense": "Skip this nonsense",
|
||||
"freedom_of_music": "“Freedom of Music”",
|
||||
"freedom_of_music_palm": "“Freedom of Music in the palm of your hand”",
|
||||
"get_started": "Let's get started",
|
||||
"youtube_source_description": "Recommended and works best.",
|
||||
"piped_source_description": "Feeling free? Same as YouTube but a lot free.",
|
||||
"jiosaavn_source_description": "Best for South Asian region.",
|
||||
"highest_quality": "Highest Quality: {quality}",
|
||||
"select_audio_source": "Select Audio Source",
|
||||
"endless_playback_description": "Automatically append new songs\nto the end of the queue",
|
||||
"choose_your_region": "Choose your region",
|
||||
"choose_your_region_description": "This will help Spotube show you the right content\nfor your location.",
|
||||
"choose_your_language": "Choose your language",
|
||||
"help_project_grow": "Help this project grow",
|
||||
"help_project_grow_description": "Spotube is an open-source project. You can help this project grow by contributing to the project, reporting bugs, or suggesting new features.",
|
||||
"contribute_on_github": "Contribute on GitHub",
|
||||
"donate_on_open_collective": "Donate on Open Collective",
|
||||
"browse_anonymously": "Browse Anonymously"
|
||||
}
|
||||
@ -1,107 +1,107 @@
|
||||
{
|
||||
"guest": "Gast",
|
||||
"browse": "Bladeren",
|
||||
"search": "Zoek op",
|
||||
"search": "Zoeken",
|
||||
"library": "Bibliotheek",
|
||||
"lyrics": "Liedteksten",
|
||||
"lyrics": "Teksten",
|
||||
"settings": "Instellingen",
|
||||
"genre_categories_filter": "Categorieën of genres filteren...",
|
||||
"genre_categories_filter": "Categorieën of genres filteren…",
|
||||
"genre": "Genre",
|
||||
"personalized": "Gepersonaliseerd",
|
||||
"featured": "Aanbevolen",
|
||||
"new_releases": "Nieuwe uitgaves",
|
||||
"songs": "Liedjes",
|
||||
"playing_track": "{track} afspelen",
|
||||
"queue_clear_alert": "Dit zal de huidige wachtrij wissen. {track_length} tracks worden verwijderd\nWilt u doorgaan?",
|
||||
"queue_clear_alert": "Dit zal de huidige wachtrij wissen. {track_length} nummers worden verwijderd\nWil je doorgaan?",
|
||||
"load_more": "Meer laden",
|
||||
"playlists": "Afspeellijsten",
|
||||
"artists": "Kunstenaars",
|
||||
"artists": "Artiesten",
|
||||
"albums": "Albums",
|
||||
"tracks": "Nummers",
|
||||
"downloads": "Downloads",
|
||||
"filter_playlists": "Filter uw afspeellijsten...",
|
||||
"filter_playlists": "Afspeellijsten filteren…",
|
||||
"liked_tracks": "Geliefde tracks",
|
||||
"liked_tracks_description": "Al je favoriete nummers",
|
||||
"create_playlist": "Afspeellijst maken",
|
||||
"create_a_playlist": "Een afspeellijst maken",
|
||||
"create_playlist": "Afspeellijst aanmaken",
|
||||
"create_a_playlist": "Een afspeellijst aanmaken",
|
||||
"update_playlist": "Afspeellijst bijwerken",
|
||||
"create": "Maak",
|
||||
"create": "Aanmaken",
|
||||
"cancel": "Annuleren",
|
||||
"update": "Bijwerken",
|
||||
"playlist_name": "Afspeellijstnaam",
|
||||
"playlist_name": "Naam afspeellijst",
|
||||
"name_of_playlist": "Naam van de afspeellijst",
|
||||
"description": "Beschrijving",
|
||||
"public": "Openbaar",
|
||||
"collaborative": "Samenwerkend",
|
||||
"search_local_tracks": "Lokale nummers zoeken...",
|
||||
"play": "Speel",
|
||||
"search_local_tracks": "Lokale nummers zoeken…",
|
||||
"play": "Afspelen",
|
||||
"delete": "Wissen",
|
||||
"none": "Geen",
|
||||
"sort_a_z": "Sorteren op A-Z",
|
||||
"sort_z_a": "Sorteren op Z-A",
|
||||
"sort_artist": "Sorteren op kunstenaar",
|
||||
"sort_artist": "Sorteren op artiest",
|
||||
"sort_album": "Sorteren op album",
|
||||
"sort_tracks": "Nummers sorteren",
|
||||
"currently_downloading": "Momenteel aan het downloaden ({tracks_length})",
|
||||
"cancel_all": "Alle annuleren",
|
||||
"filter_artist": "Kunstenaars filteren...",
|
||||
"filter_artist": "Artiesten filteren…",
|
||||
"followers": "{followers} volgers",
|
||||
"add_artist_to_blacklist": "Kunstenaar toevoegen aan zwarte lijst",
|
||||
"add_artist_to_blacklist": "Artiest toevoegen aan zwarte lijst",
|
||||
"top_tracks": "Topsporen",
|
||||
"fans_also_like": "Liefhebbers willen ook",
|
||||
"loading": "Aan het laden...",
|
||||
"artist": "Kunstenaar",
|
||||
"blacklisted": "Op de zwarte lijst",
|
||||
"following": "Op volg",
|
||||
"loading": "Laden…",
|
||||
"artist": "Artiest",
|
||||
"blacklisted": "Zwarte lijst",
|
||||
"following": "Volgen",
|
||||
"follow": "Volgen",
|
||||
"artist_url_copied": "URL artiest gekopieerd naar klembord",
|
||||
"added_to_queue": "{tracks} tracks toegevoegd aan wachtrij",
|
||||
"filter_albums": "Albums filteren...",
|
||||
"added_to_queue": "{tracks} nummers toegevoegd aan wachtrij",
|
||||
"filter_albums": "Albums filteren…",
|
||||
"synced": "Gesynchroniseerd",
|
||||
"plain": "Eenvoudig",
|
||||
"shuffle": "Schuifelen",
|
||||
"search_tracks": "Zoek nummers...",
|
||||
"released": "Vrijgegeven",
|
||||
"shuffle": "Willekeurig",
|
||||
"search_tracks": "Nummers zoeken…",
|
||||
"released": "Uitgegeven",
|
||||
"error": "Fout {error}",
|
||||
"title": "Titel",
|
||||
"time": "Tijd",
|
||||
"more_actions": "Meer acties",
|
||||
"download_count": "({count}) downloads",
|
||||
"add_count_to_playlist": "Voeg ({count}) toe aan afspeellijst",
|
||||
"add_count_to_queue": "Voeg ({count}) toe aan wachtrij",
|
||||
"play_count_next": "Speel ({count}) volgende",
|
||||
"add_count_to_playlist": "({count}) aan afspeellijst toevoegen",
|
||||
"add_count_to_queue": "({count}) aan wachtrij toevoegen",
|
||||
"play_count_next": "Volgende ({count}) afspelen",
|
||||
"album": "Album",
|
||||
"copied_to_clipboard": "{data} naar klembord gekopieerd",
|
||||
"add_to_following_playlists": "Voeg {track} toe aan volgende afspeellijsten",
|
||||
"add_to_following_playlists": "{track} aan volgende afspeellijsten toevoegen",
|
||||
"add": "Toevoegen",
|
||||
"added_track_to_queue": "{track} toegevoegd aan wachtrij",
|
||||
"added_track_to_queue": "{track} aan wachtrij toegevoegd",
|
||||
"add_to_queue": "Toevoegen aan wachtrij",
|
||||
"track_will_play_next": "{track} zal hierna spelen",
|
||||
"track_will_play_next": "{track} wordt hierna afgespeeld",
|
||||
"play_next": "Volgende afspelen",
|
||||
"removed_track_from_queue": "{track} uit wachtrij verwijderd",
|
||||
"remove_from_queue": "Verwijderen uit wachtrij",
|
||||
"remove_from_favorites": "Verwijderen uit favorieten",
|
||||
"removed_track_from_queue": "{track} van wachtrij verwijderd",
|
||||
"remove_from_queue": "Van wachtrij verwijderen",
|
||||
"remove_from_favorites": "Van favorieten verwijderen",
|
||||
"save_as_favorite": "Opslaan als favoriet",
|
||||
"add_to_playlist": "Toevoegen aan afspeellijst",
|
||||
"remove_from_playlist": "Verwijderen uit afspeellijst",
|
||||
"add_to_blacklist": "Toevoegen aan zwarte lijst",
|
||||
"remove_from_blacklist": "Verwijderen uit zwarte lijst",
|
||||
"add_to_playlist": "Aan afspeellijst toevoegen",
|
||||
"remove_from_playlist": "Van afspeellijst verwijderen",
|
||||
"add_to_blacklist": "Aan zwarte lijst toevoegen",
|
||||
"remove_from_blacklist": "Van zwarte lijst verwijderen",
|
||||
"share": "Delen",
|
||||
"mini_player": "Minispeler",
|
||||
"slide_to_seek": "Schuif om vooruit of achteruit te zoeken",
|
||||
"slide_to_seek": "Schuiven om vooruit of achteruit te zoeken",
|
||||
"shuffle_playlist": "Afspeellijst schuifelen",
|
||||
"unshuffle_playlist": "Afspeellijst onschuifelen",
|
||||
"previous_track": "Vorige nummer",
|
||||
"next_track": "Volgende nummer",
|
||||
"pause_playback": "Weergave pauzeren",
|
||||
"resume_playback": "Weergave hervatten",
|
||||
"loop_track": "Nummer loopen",
|
||||
"pause_playback": "Afspelen pauzeren",
|
||||
"resume_playback": "Afspelen hervatten",
|
||||
"loop_track": "Nummer herhalen",
|
||||
"repeat_playlist": "Afspeellijst herhalen",
|
||||
"queue": "Wachtrij",
|
||||
"alternative_track_sources": "Alternatieve nummerbronnen",
|
||||
"download_track": "Nummer downloaden",
|
||||
"tracks_in_queue": "{tracks} tracks in wachtrij",
|
||||
"clear_all": "Wis alles",
|
||||
"tracks_in_queue": "{tracks} nummers in wachtrij",
|
||||
"clear_all": "Alles wissen",
|
||||
"show_hide_ui_on_hover": "UI tonen/verbergen bij zweven",
|
||||
"always_on_top": "Altijd bovenaan",
|
||||
"exit_mini_player": "Minispeler afsluiten",
|
||||
@ -111,7 +111,7 @@
|
||||
"connect_with_spotify": "Verbinden met Spotify",
|
||||
"logout": "Afmelden",
|
||||
"logout_of_this_account": "Afmelden van dit account",
|
||||
"language_region": "Taal & Regio",
|
||||
"language_region": "Taal & regio",
|
||||
"language": "Taal",
|
||||
"system_default": "Systeemstandaard",
|
||||
"market_place_region": "Marktplaats-regio",
|
||||
@ -119,76 +119,78 @@
|
||||
"appearance": "Uiterlijk",
|
||||
"layout_mode": "Opmaakmodus",
|
||||
"override_layout_settings": "Instellingen voor responsieve opmaakmodus opheffen",
|
||||
"adaptive": "Aanpassingsgericht",
|
||||
"adaptive": "Adaptief",
|
||||
"compact": "Compact",
|
||||
"extended": "Uitgebreide",
|
||||
"extended": "Uitgebreid",
|
||||
"theme": "Thema",
|
||||
"dark": "Donker",
|
||||
"light": "Licht",
|
||||
"system": "Systeem",
|
||||
"accent_color": "Accentkleur",
|
||||
"sync_album_color": "Albumkleur synchroniseren",
|
||||
"sync_album_color_description": "Gebruikt de overheersende kleur van het albumartikel als accentkleur",
|
||||
"sync_album_color_description": "Gebruikt de overheersende kleur van het album als accentkleur",
|
||||
"playback": "Weergave",
|
||||
"audio_quality": "Audiokwaliteit",
|
||||
"high": "Hoog",
|
||||
"low": "Laag",
|
||||
"pre_download_play": "Vooraf downloaden en spelen",
|
||||
"pre_download_play": "Vooraf downloaden en afspelen",
|
||||
"pre_download_play_description": "In plaats van audio te streamen, kun je bytes downloaden en afspelen (aanbevolen voor gebruikers met een hogere bandbreedte)",
|
||||
"skip_non_music": "Niet-muzieksegmenten overslaan (SponsorBlock)",
|
||||
"blacklist_description": "Nummers en artiesten op de zwarte lijst",
|
||||
"wait_for_download_to_finish": "Wacht tot de huidige download is voltooid",
|
||||
"desktop": "Bureaublad",
|
||||
"close_behavior": "Sluitgedrag",
|
||||
"close": "Sluit af",
|
||||
"minimize_to_tray": "Minimaliseren naar lade",
|
||||
"close": "Afsluiten",
|
||||
"minimize_to_tray": "Minimaliseren naar systeemvak",
|
||||
"show_tray_icon": "Systeemvakpictogram tonen",
|
||||
"about": "Over",
|
||||
"u_love_spotube": "We weten dat jullie van Spotube houden",
|
||||
"u_love_spotube": "We weten dat je van Spotube houd",
|
||||
"check_for_updates": "Controleren op updates",
|
||||
"about_spotube": "Over Spotube",
|
||||
"blacklist": "Zwarte lijst",
|
||||
"please_sponsor": "Sponsor/Doneer a.u.b.",
|
||||
"spotube_description": "Spotube, een lichtgewicht, cross-platform, vrij-voor-alles Spotify-client",
|
||||
"version": "Versie",
|
||||
"build_number": "Beeldnummer",
|
||||
"founder": "Stichter",
|
||||
"build_number": "Bouwnummer",
|
||||
"founder": "Grondlegger",
|
||||
"repository": "Opslagplaats",
|
||||
"bug_issues": "Bug+problemen",
|
||||
"made_with": "Gemaakt met ❤️ in Bangladesh🇧🇩",
|
||||
"made_with": "Met ❤️ gemaakt in Bangladesh🇧🇩",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Licentie",
|
||||
"add_spotify_credentials": "Voeg je spotify-referenties toe om te beginnen",
|
||||
"credentials_will_not_be_shared_disclaimer": "Maakt u geen zorgen, uw gegevens worden niet verzameld of gedeeld met anderen.",
|
||||
"know_how_to_login": "Weet u niet hoe u dit moet doen?",
|
||||
"follow_step_by_step_guide": "Volg de stap voor stap gids",
|
||||
"add_spotify_credentials": "Voeg om te beginnen je spotify-aanmeldgegevens toe",
|
||||
"credentials_will_not_be_shared_disclaimer": "Maak je geen zorgen, je gegevens worden niet verzameld of gedeeld met anderen.",
|
||||
"know_how_to_login": "Weet je niet hoe je dit moet doen?",
|
||||
"follow_step_by_step_guide": "Volg de stapsgewijze handleiding",
|
||||
"spotify_cookie": "Spotify {name} Cookie",
|
||||
"cookie_name_cookie": "{name} Cookie",
|
||||
"fill_in_all_fields": "Vul alle velden in a.u.b.",
|
||||
"submit": "Verzenden",
|
||||
"exit": "Ga weg",
|
||||
"exit": "Afronden",
|
||||
"previous": "Vorige",
|
||||
"next": "Volgende",
|
||||
"done": "Klaar",
|
||||
"step_1": "Stap 1",
|
||||
"first_go_to": "Ga eerst naar",
|
||||
"login_if_not_logged_in": "en Inloggen/Aanmelden als u niet bent ingelogd",
|
||||
"login_if_not_logged_in": "en Inloggen/Aanmelden als je niet bent ingelogd",
|
||||
"step_2": "Stap 2",
|
||||
"step_2_steps": "1. Zodra je bent aangemeld, druk je op F12 of klik je met de rechtermuisknop > Inspect om de Browser devtools te openen.\n2. Ga vervolgens naar het tabblad \"Toepassing\" (Chrome, Edge, Brave enz..) of naar het tabblad \"Opslag\" (Firefox, Palemoon enz..).\n3. Ga naar de sectie \"Cookies\" en vervolgens naar de subsectie \"https://accounts.spotify.com\".",
|
||||
"step_3": "Stap 3",
|
||||
"step_3_steps": "De waarde van cookie \"sp_dc\" kopiëren",
|
||||
"success_emoji": "Succes🥳",
|
||||
"success_message": "Je bent nu succesvol ingelogd met je Spotify account. Goed gedaan, maat!",
|
||||
"success_message": "Je bent nu ingelogd met je Spotify account. Goed gedaan!",
|
||||
"step_4": "Stap 4",
|
||||
"step_4_steps": "De gekopieerde waarde \"sp_dc\" plakken",
|
||||
"something_went_wrong": "Er ging iets mis",
|
||||
"piped_instance": "Piped-serverinstantie",
|
||||
"piped_description": "De Piped-serverinstantie die moet worden gebruikt voor het matchen van sporen",
|
||||
"piped_description": "De Piped-serverinstantie die moet worden gebruikt voor overeenkomstige nummers",
|
||||
"piped_warning": "Sommige werken misschien niet goed. Dus gebruik ze op eigen risico",
|
||||
"generate_playlist": "Afspeellijst genereren",
|
||||
"track_exists": "Nummer {track} bestaat al",
|
||||
"replace_downloaded_tracks": "Alle gedownloade nummers vervangen",
|
||||
"skip_download_tracks": "Downloaden van alle gedownloade nummers overslaan",
|
||||
"do_you_want_to_replace": "Wil je de bestaande nummer vervangen?",
|
||||
"do_you_want_to_replace": "Wil je het bestaande nummer vervangen?",
|
||||
"replace": "Vervangen",
|
||||
"skip": "Overslaan",
|
||||
"select_up_to_count_type": "Selecteer tot {count} {type}",
|
||||
@ -196,13 +198,13 @@
|
||||
"add_genres": "Genres toevoegen",
|
||||
"country": "Land",
|
||||
"number_of_tracks_generate": "Aantal nummers om te genereren",
|
||||
"acousticness": "Akoesticiteit",
|
||||
"acousticness": "Akoestiek",
|
||||
"danceability": "Dansbaarheid",
|
||||
"energy": "Energie",
|
||||
"instrumentalness": "Instrumentaliteit",
|
||||
"liveness": "Levendigheid",
|
||||
"loudness": "Luidheid",
|
||||
"speechiness": "Sprakeligheid",
|
||||
"speechiness": "Spraak",
|
||||
"valence": "Valentie",
|
||||
"popularity": "Populariteit",
|
||||
"key": "Sleutel",
|
||||
@ -217,16 +219,16 @@
|
||||
"max": "Max",
|
||||
"target": "Doel",
|
||||
"moderate": "Matig",
|
||||
"deselect_all": "Alles deselecteren",
|
||||
"deselect_all": "Selectie opheffen",
|
||||
"select_all": "Alles selecteren",
|
||||
"are_you_sure": "Weet je het zeker?",
|
||||
"generating_playlist": "Je aangepaste afspeellijst genereren...",
|
||||
"generating_playlist": "Aangepaste afspeellijst genereren…",
|
||||
"selected_count_tracks": "{count} nummers geselecteerd",
|
||||
"download_warning": "Als je alle Tracks in bulk downloadt, ben je duidelijk bezig met muziekpiraterij en breng je schade toe aan de creatieve muziekmaatschappij. Ik hoop dat je je hiervan bewust bent. Probeer altijd het harde werk van artiesten te respecteren en te steunen.",
|
||||
"download_ip_ban_warning": "BTW, je IP-adres kan worden geblokkeerd op YouTube als gevolg van buitensporige downloadverzoeken dan normaal. IP blokkering betekent dat je YouTube niet kunt gebruiken (zelfs als je ingelogd bent) voor tenminste 2-3 maanden vanaf dat IP apparaat. Spotube is niet verantwoordelijk als dit ooit gebeurt.",
|
||||
"download_warning": "Als je alle nummers in bulk downloadt, ben je duidelijk bezig met muziekpiraterij en breng je schade toe aan de creatieve muziekmaatschappij. Ik hoop dat je je hiervan bewust bent. Probeer altijd het harde werk van artiesten te respecteren en te steunen.",
|
||||
"download_ip_ban_warning": "BTW, je IP-adres kan worden geblokkeerd op YouTube als gevolg van buitensporige downloadverzoeken. IP-blokkering betekent dat je YouTube niet kunt gebruiken (zelfs als je ingelogd bent) voor tenminste 2-3 maanden vanaf dat IP-apparaat. Spotube is niet verantwoordelijk als dit ooit gebeurt.",
|
||||
"by_clicking_accept_terms": "Door op 'accepteren' te klikken ga je akkoord met de volgende voorwaarden:",
|
||||
"download_agreement_1": "Ik weet dat ik muziek illegaal verveel. Ik ben en crimineel.",
|
||||
"download_agreement_2": "Ik steun de kunstenaar waar ik kan en ik doe dit alleen omdat ik geen geld heb om hun kunst te kopen.",
|
||||
"download_agreement_1": "Ik weet dat ik muziek illegaal donload. Ik ben slecht.",
|
||||
"download_agreement_2": "Ik steun de artiest waar ik kan en ik doe dit alleen omdat ik geen geld heb om hun kunst te kopen.",
|
||||
"download_agreement_3": "Ik ben me er volledig van bewust dat mijn IP geblokkeerd kan worden op YouTube & ik houd Spotube of zijn eigenaars/contributeurs niet verantwoordelijk voor ongelukken die veroorzaakt worden door mijn huidige actie.",
|
||||
"decline": "Weigeren",
|
||||
"accept": "Accepteren",
|
||||
@ -247,45 +249,42 @@
|
||||
"custom_hours": "Aangepaste uren",
|
||||
"logs": "Logboeken",
|
||||
"developers": "Ontwikkelaars",
|
||||
"not_logged_in": "U bent niet aangemeld",
|
||||
"not_logged_in": "Je bent niet aangemeld",
|
||||
"search_mode": "Zoekmodus",
|
||||
"youtube_api_type": "API-type",
|
||||
"ok": "Oké",
|
||||
"failed_to_encrypt": "Versleuteling mislukt",
|
||||
"encryption_failed_warning": "Spotube gebruikt encryptie om je gegevens veilig op te slaan. Maar dat is niet gelukt. Dus zal het terugvallen op onveilige opslag.\nAls je linux gebruikt, zorg er dan voor dat je een geheim-dienst (gnome-keyring, kde-wallet, keepassxc etc) hebt geïnstalleerd.",
|
||||
"querying_info": "Info opvragen...",
|
||||
"encryption_failed_warning": "Spotube gebruikt versleuteling om je gegevens veilig op te slaan. Maar dat is niet gelukt. Dus zal het terugvallen op onveilige opslag.\nAls je linux gebruikt, zorg er dan voor dat je een geheim-dienst (gnome-keyring, kde-wallet, keepassxc etc) hebt geïnstalleerd.",
|
||||
"querying_info": "Info opvragen…",
|
||||
"piped_api_down": "Piped API is uit",
|
||||
"piped_down_error_instructions": "De Piped-instantie {pipedInstance} is momenteel uitgevallen\n\nVerander de instantie of verander het 'API-type' naar de officiële YouTube API.\n\nZorg ervoor dat u de app herstart na de wijziging",
|
||||
"you_are_offline": "U bent momenteel offline",
|
||||
"connection_restored": "Uw internetverbinding is hersteld",
|
||||
"you_are_offline": "Je bent momenteel offline",
|
||||
"connection_restored": "Je internetverbinding is hersteld",
|
||||
"use_system_title_bar": "Systeemtitelbalk gebruiken",
|
||||
"crunching_results": "Resultaten kraken...",
|
||||
"search_to_get_results": "Zoek om resultaten te krijgen",
|
||||
"crunching_results": "Resultaten verwerken…",
|
||||
"search_to_get_results": "Zoeken naar resultaten",
|
||||
"use_amoled_mode": "Pikzwart donkerthema",
|
||||
"pitch_dark_theme": "AMOLED-modus",
|
||||
"normalize_audio": "Audio normaliseren",
|
||||
"change_cover": "Dekking wijzigen",
|
||||
"add_cover": "Dekking toevoegen",
|
||||
"change_cover": "Hoes aanpassen",
|
||||
"add_cover": "Hoes toevoegen",
|
||||
"restore_defaults": "Standaardwaarden herstellen",
|
||||
"download_music_codec": "Muziek-codec downloaden",
|
||||
"streaming_music_codec": "Muziek-codec streamen",
|
||||
"login_with_lastfm": "Aanmelden met Last.fm",
|
||||
"download_music_codec": "Download-codec",
|
||||
"streaming_music_codec": "Streaming-codec",
|
||||
"login_with_lastfm": "Inloggen met Last.fm",
|
||||
"connect": "Verbinden",
|
||||
"disconnect_lastfm": "Last.fm verbreken",
|
||||
"disconnect": "Ontkoppelen",
|
||||
"disconnect": "Verbeken",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord",
|
||||
"login": "Inloggen",
|
||||
"login_with_your_lastfm": "Inloggen met uw Last.fm account",
|
||||
"scrobble_to_lastfm": "Scrobbel naar Last.fm",
|
||||
"audio_source": "Audiobron",
|
||||
"login_with_your_lastfm": "Inloggen met je Last.fm account",
|
||||
"scrobble_to_lastfm": "Scrobbelen naar Last.fm",
|
||||
"go_to_album": "Ga naar album",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "Alles bekijken",
|
||||
"browse_all": "Alles doorbladeren",
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Verken genres",
|
||||
"step_3_steps": "Kopieer de waarde van de \"sp_dc\"-cookie",
|
||||
"step_4_steps": "Plak de gekopieerde waarde van \"sp_dc\"",
|
||||
"explore_genres": "Genres verkennen",
|
||||
"friends": "Vrienden",
|
||||
"no_lyrics_available": "Sorry, kan geen teksten vinden voor deze track"
|
||||
}
|
||||
"no_lyrics_available": "Sorry, geen teksten gevonden voor dit nummer"
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
/// yuri-val@github => Ukrainian
|
||||
/// energywave@github, ncvescera@github, OpenCode@github => Italian
|
||||
/// mdksec@github => Turkish
|
||||
/// SecularSteve@github => Dutch
|
||||
/// Stephan-P@github, SecularSteve@github => Dutch
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class L10n {
|
||||
|
||||
@ -29,6 +29,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/cli/cli.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/themes/theme.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
@ -68,6 +69,9 @@ Future<void> main(List<String> rawArgs) async {
|
||||
DiscordRPC.initialize();
|
||||
}
|
||||
|
||||
await KVStoreService.initialize();
|
||||
KVStoreService.doneGettingStarted = false;
|
||||
|
||||
final hiveCacheDir =
|
||||
kIsWeb ? null : (await getApplicationSupportDirectory()).path;
|
||||
|
||||
@ -184,6 +188,7 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
final locale = ref.watch(userPreferencesProvider.select((s) => s.locale));
|
||||
final paletteColor =
|
||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
useDisableBatteryOptimizations();
|
||||
useInitSysTray(ref);
|
||||
|
||||
94
lib/pages/getting_started/getting_started.dart
Normal file
@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/greeting.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/playback.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/region.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/support.dart';
|
||||
|
||||
class GettingStarting extends HookConsumerWidget {
|
||||
const GettingStarting({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
final pageController = usePageController();
|
||||
|
||||
final onNext = useCallback(() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}, [pageController]);
|
||||
|
||||
final onPrevious = useCallback(() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}, [pageController]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
actions: [
|
||||
ListenableBuilder(
|
||||
listenable: pageController,
|
||||
builder: (context, _) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: pageController.hasClients &&
|
||||
(pageController.page == 0 || pageController.page == 3)
|
||||
? const SizedBox()
|
||||
: TextButton(
|
||||
onPressed: () {
|
||||
pageController.animateToPage(
|
||||
3,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.skip_this_nonsense,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: Assets.bengaliPatternsBg.provider(),
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
colorScheme.background.withOpacity(0.2),
|
||||
BlendMode.srcOver,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
children: [
|
||||
GettingStartedPageGreetingSection(onNext: onNext),
|
||||
GettingStartedPageLanguageRegionSection(onNext: onNext),
|
||||
GettingStartedPagePlaybackSection(
|
||||
onNext: onNext,
|
||||
onPrevious: onPrevious,
|
||||
),
|
||||
const GettingStartedScreenSupportSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/pages/getting_started/sections/greeting.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class GettingStartedPageGreetingSection extends HookConsumerWidget {
|
||||
final VoidCallback onNext;
|
||||
const GettingStartedPageGreetingSection({super.key, required this.onNext});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme) = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Assets.spotubeLogoPng.image(height: 200),
|
||||
const Gap(24),
|
||||
Text(
|
||||
"Spotube",
|
||||
style:
|
||||
textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
kIsMobile
|
||||
? context.l10n.freedom_of_music_palm
|
||||
: context.l10n.freedom_of_music,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const Gap(84),
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onNext,
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(context.l10n.get_started),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
lib/pages/getting_started/sections/playback.dart
Normal file
@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
|
||||
final audioSourceToIconMap = {
|
||||
AudioSource.youtube: const Icon(
|
||||
SpotubeIcons.youtube,
|
||||
color: Colors.red,
|
||||
size: 30,
|
||||
),
|
||||
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 30),
|
||||
AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48),
|
||||
};
|
||||
|
||||
class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
||||
final VoidCallback onNext;
|
||||
final VoidCallback onPrevious;
|
||||
|
||||
const GettingStartedPagePlaybackSection({
|
||||
super.key,
|
||||
required this.onNext,
|
||||
required this.onPrevious,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :colorScheme, :dividerColor) =
|
||||
Theme.of(context);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
final preferencesNotifier = ref.read(userPreferencesProvider.notifier);
|
||||
|
||||
final audioSourceToDescription = useMemoized(
|
||||
() => {
|
||||
AudioSource.youtube: "${context.l10n.youtube_source_description}\n"
|
||||
"${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}",
|
||||
AudioSource.piped: context.l10n.piped_source_description,
|
||||
AudioSource.jiosaavn:
|
||||
"${context.l10n.jiosaavn_source_description}\n"
|
||||
"${context.l10n.highest_quality("320kbps mp")}",
|
||||
},
|
||||
[]);
|
||||
|
||||
return Center(
|
||||
child: BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(SpotubeIcons.album, size: 16),
|
||||
const Gap(8),
|
||||
Text(context.l10n.playback, style: textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.l10n.select_audio_source,
|
||||
style: textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
ToggleButtons(
|
||||
isSelected: [
|
||||
for (final source in AudioSource.values)
|
||||
preferences.audioSource == source,
|
||||
],
|
||||
onPressed: (index) {
|
||||
preferencesNotifier.setAudioSource(AudioSource.values[index]);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
children: [
|
||||
for (final source in AudioSource.values)
|
||||
SizedBox.square(
|
||||
dimension: 84,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
audioSourceToIconMap[source]!,
|
||||
const Gap(8),
|
||||
Text(
|
||||
source.name,
|
||||
style: textTheme.bodySmall!.copyWith(
|
||||
color: preferences.audioSource == source
|
||||
? colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
title: Align(
|
||||
alignment: switch (preferences.audioSource) {
|
||||
AudioSource.youtube => Alignment.centerLeft,
|
||||
AudioSource.piped => Alignment.center,
|
||||
AudioSource.jiosaavn => Alignment.centerRight,
|
||||
},
|
||||
child: Text(
|
||||
audioSourceToDescription[preferences.audioSource]!,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
ListTile(
|
||||
title: Text(context.l10n.endless_playback),
|
||||
subtitle: Text(
|
||||
context.l10n.endless_playback_description,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: dividerColor,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
preferencesNotifier
|
||||
.setEndlessPlayback(!preferences.endlessPlayback);
|
||||
},
|
||||
trailing: Switch(
|
||||
value: preferences.endlessPlayback,
|
||||
onChanged: (value) {
|
||||
preferencesNotifier.setEndlessPlayback(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(34),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.angleLeft),
|
||||
label: Text(context.l10n.previous),
|
||||
onPressed: onPrevious,
|
||||
),
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(context.l10n.next),
|
||||
onPressed: onNext,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
129
lib/pages/getting_started/sections/region.dart
Normal file
@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/language_codes.dart';
|
||||
import 'package:spotube/collections/spotify_markets.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/l10n/l10n.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
||||
final void Function() onNext;
|
||||
const GettingStartedPageLanguageRegionSection(
|
||||
{super.key, required this.onNext});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :dividerColor) = Theme.of(context);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
|
||||
return SafeArea(
|
||||
child: Center(
|
||||
child: BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
SpotubeIcons.language,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.l10n.language_region,
|
||||
style: textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(48),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.choose_your_region,
|
||||
style: textTheme.titleSmall,
|
||||
),
|
||||
Text(
|
||||
context.l10n.choose_your_region_description,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: dividerColor,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownMenu(
|
||||
initialSelection: preferences.recommendationMarket,
|
||||
onSelected: (value) {
|
||||
if (value == null) return;
|
||||
ref
|
||||
.read(userPreferencesProvider.notifier)
|
||||
.setRecommendationMarket(value);
|
||||
},
|
||||
hintText: preferences.recommendationMarket.name,
|
||||
label: Text(context.l10n.market_place_region),
|
||||
inputDecorationTheme:
|
||||
const InputDecorationTheme(isDense: true),
|
||||
dropdownMenuEntries: [
|
||||
for (final market in spotifyMarkets)
|
||||
DropdownMenuEntry(
|
||||
value: market.$1,
|
||||
label: market.$2,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(36),
|
||||
Text(
|
||||
context.l10n.choose_your_language,
|
||||
style: textTheme.titleSmall,
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownMenu(
|
||||
initialSelection: preferences.locale,
|
||||
onSelected: (locale) {
|
||||
if (locale == null) return;
|
||||
ref
|
||||
.read(userPreferencesProvider.notifier)
|
||||
.setLocale(locale);
|
||||
},
|
||||
hintText: context.l10n.system_default,
|
||||
label: Text(context.l10n.language),
|
||||
inputDecorationTheme:
|
||||
const InputDecorationTheme(isDense: true),
|
||||
dropdownMenuEntries: [
|
||||
DropdownMenuEntry(
|
||||
value: const Locale("system", "system"),
|
||||
label: context.l10n.system_default,
|
||||
),
|
||||
for (final locale in L10n.all)
|
||||
DropdownMenuEntry(
|
||||
value: locale,
|
||||
label: LanguageLocals.getDisplayLanguage(
|
||||
locale.languageCode)
|
||||
.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(48),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(context.l10n.next),
|
||||
onPressed: onNext,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
130
lib/pages/getting_started/sections/support.dart
Normal file
@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
||||
const GettingStartedScreenSupportSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(SpotubeIcons.heartFilled, color: Colors.pink),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.l10n.help_project_grow,
|
||||
style:
|
||||
textTheme.titleMedium?.copyWith(color: Colors.pink),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
Text(context.l10n.help_project_grow_description),
|
||||
const Gap(16),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.github),
|
||||
label: Text(context.l10n.contribute_on_github),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.black,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
await launchUrlString(
|
||||
"https://github.com/KRTirtho/spotube",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.openCollective),
|
||||
label: Text(context.l10n.donate_on_open_collective),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xff4cb7f6),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
await launchUrlString(
|
||||
"https://opencollective.com/spotube",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(48),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
colorScheme.primary,
|
||||
colorScheme.secondary,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(SpotubeIcons.anonymous),
|
||||
label: Text(context.l10n.browse_anonymously),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
KVStoreService.doneGettingStarted = true;
|
||||
context.go("/");
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.spotify),
|
||||
label: Text(context.l10n.connect_with_spotify),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xff1db954),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
KVStoreService.doneGettingStarted = true;
|
||||
context.push("/login");
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
@ -27,7 +27,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||
leading: ThemedButtonsTabBar(
|
||||
tabs: [
|
||||
Tab(text: " ${context.l10n.playlists} "),
|
||||
Tab(text: " ${context.l10n.tracks} "),
|
||||
Tab(text: " ${context.l10n.local_tracks} "),
|
||||
Tab(
|
||||
child: Badge(
|
||||
isLabelVisible: downloadingCount > 0,
|
||||
|
||||
@ -2,8 +2,11 @@ import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/infinite_query.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
@ -45,6 +48,8 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
],
|
||||
);
|
||||
|
||||
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
|
||||
|
||||
return InheritedTrackView(
|
||||
collectionId: playlist.id!,
|
||||
image: TypeConversionUtils.image_X_UrlString(
|
||||
@ -72,9 +77,20 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
shareUrl: playlist.externalUrls?.spotify ?? "",
|
||||
onHeart: () async {
|
||||
if (!isLikedQuery.hasData || togglePlaylistLike.isMutating) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await togglePlaylistLike.mutate(isLikedQuery.data!);
|
||||
final confirmed = isUserPlaylist
|
||||
? await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n.delete_playlist,
|
||||
message: context.l10n.delete_playlist_confirmation,
|
||||
)
|
||||
: true;
|
||||
if (confirmed) {
|
||||
await togglePlaylistLike.mutate(isLikedQuery.data!);
|
||||
return isUserPlaylist;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
child: const TrackView(),
|
||||
);
|
||||
|
||||
@ -15,6 +15,7 @@ import 'package:spotube/components/root/bottom_player.dart';
|
||||
import 'package:spotube/components/root/sidebar.dart';
|
||||
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||
import 'package:spotube/hooks/configurators/use_update_checker.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
@ -134,6 +135,8 @@ class RootApp extends HookConsumerWidget {
|
||||
// checks for latest version of the application
|
||||
useUpdateChecker(ref);
|
||||
|
||||
useEndlessPlayback(ref);
|
||||
|
||||
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
|
||||
|
||||
useEffect(() {
|
||||
@ -159,38 +162,47 @@ class RootApp extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Sidebar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
child: child,
|
||||
),
|
||||
extendBody: true,
|
||||
drawerScrimColor: Colors.transparent,
|
||||
endDrawer: DesktopTools.platform.isDesktop
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: theme.brightness == Brightness.light
|
||||
? null
|
||||
: kElevationToShadow[8],
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
top: 40,
|
||||
bottom: 100,
|
||||
),
|
||||
child: const PlayerQueue(floating: true),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomPlayer(),
|
||||
SpotubeNavigationBar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
),
|
||||
],
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (rootPaths[location] != 0) {
|
||||
onSelectIndexChanged(0);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Sidebar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
child: child,
|
||||
),
|
||||
extendBody: true,
|
||||
drawerScrimColor: Colors.transparent,
|
||||
endDrawer: DesktopTools.platform.isDesktop
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: theme.brightness == Brightness.light
|
||||
? null
|
||||
: kElevationToShadow[8],
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
top: 40,
|
||||
bottom: 100,
|
||||
),
|
||||
child: const PlayerQueue(floating: true),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomPlayer(),
|
||||
SpotubeNavigationBar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||
@ -10,7 +11,11 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
|
||||
class SettingsAppearanceSection extends HookConsumerWidget {
|
||||
const SettingsAppearanceSection({Key? key}) : super(key: key);
|
||||
final bool isGettingStarted;
|
||||
const SettingsAppearanceSection({
|
||||
Key? key,
|
||||
this.isGettingStarted = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -24,87 +29,101 @@ class SettingsAppearanceSection extends HookConsumerWidget {
|
||||
});
|
||||
}, []);
|
||||
|
||||
final children = [
|
||||
AdaptiveSelectTile<LayoutMode>(
|
||||
secondary: const Icon(SpotubeIcons.dashboard),
|
||||
title: Text(context.l10n.layout_mode),
|
||||
subtitle: Text(context.l10n.override_layout_settings),
|
||||
value: preferences.layoutMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
preferencesNotifier.setLayoutMode(value);
|
||||
}
|
||||
},
|
||||
options: [
|
||||
DropdownMenuItem(
|
||||
value: LayoutMode.adaptive,
|
||||
child: Text(context.l10n.adaptive),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: LayoutMode.compact,
|
||||
child: Text(context.l10n.compact),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: LayoutMode.extended,
|
||||
child: Text(context.l10n.extended),
|
||||
),
|
||||
],
|
||||
),
|
||||
AdaptiveSelectTile<ThemeMode>(
|
||||
secondary: const Icon(SpotubeIcons.darkMode),
|
||||
title: Text(context.l10n.theme),
|
||||
value: preferences.themeMode,
|
||||
options: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.dark,
|
||||
child: Text(context.l10n.dark),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.light,
|
||||
child: Text(context.l10n.light),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Text(context.l10n.system),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
preferencesNotifier.setThemeMode(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.amoled),
|
||||
title: Text(context.l10n.use_amoled_mode),
|
||||
subtitle: Text(context.l10n.pitch_dark_theme),
|
||||
value: preferences.amoledDarkTheme,
|
||||
onChanged: preferencesNotifier.setAmoledDarkTheme,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(SpotubeIcons.palette),
|
||||
title: Text(context.l10n.accent_color),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 5,
|
||||
),
|
||||
trailing: ColorTile.compact(
|
||||
color: preferences.accentColorScheme,
|
||||
onPressed: pickColorScheme(),
|
||||
isActive: true,
|
||||
),
|
||||
onTap: pickColorScheme(),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.colorSync),
|
||||
title: Text(context.l10n.sync_album_color),
|
||||
subtitle: Text(context.l10n.sync_album_color_description),
|
||||
value: preferences.albumColorSync,
|
||||
onChanged: preferencesNotifier.setAlbumColorSync,
|
||||
),
|
||||
];
|
||||
|
||||
if (isGettingStarted) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final child in children) ...[
|
||||
child,
|
||||
const Gap(16),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return SectionCardWithHeading(
|
||||
heading: context.l10n.appearance,
|
||||
children: [
|
||||
AdaptiveSelectTile<LayoutMode>(
|
||||
secondary: const Icon(SpotubeIcons.dashboard),
|
||||
title: Text(context.l10n.layout_mode),
|
||||
subtitle: Text(context.l10n.override_layout_settings),
|
||||
value: preferences.layoutMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
preferencesNotifier.setLayoutMode(value);
|
||||
}
|
||||
},
|
||||
options: [
|
||||
DropdownMenuItem(
|
||||
value: LayoutMode.adaptive,
|
||||
child: Text(context.l10n.adaptive),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: LayoutMode.compact,
|
||||
child: Text(context.l10n.compact),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: LayoutMode.extended,
|
||||
child: Text(context.l10n.extended),
|
||||
),
|
||||
],
|
||||
),
|
||||
AdaptiveSelectTile<ThemeMode>(
|
||||
secondary: const Icon(SpotubeIcons.darkMode),
|
||||
title: Text(context.l10n.theme),
|
||||
value: preferences.themeMode,
|
||||
options: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.dark,
|
||||
child: Text(context.l10n.dark),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.light,
|
||||
child: Text(context.l10n.light),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Text(context.l10n.system),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
preferencesNotifier.setThemeMode(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.amoled),
|
||||
title: Text(context.l10n.use_amoled_mode),
|
||||
subtitle: Text(context.l10n.pitch_dark_theme),
|
||||
value: preferences.amoledDarkTheme,
|
||||
onChanged: preferencesNotifier.setAmoledDarkTheme,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(SpotubeIcons.palette),
|
||||
title: Text(context.l10n.accent_color),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 5,
|
||||
),
|
||||
trailing: ColorTile.compact(
|
||||
color: preferences.accentColorScheme,
|
||||
onPressed: pickColorScheme(),
|
||||
isActive: true,
|
||||
),
|
||||
onTap: pickColorScheme(),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.colorSync),
|
||||
title: Text(context.l10n.sync_album_color),
|
||||
subtitle: Text(context.l10n.sync_album_color_description),
|
||||
value: preferences.albumColorSync,
|
||||
onChanged: preferencesNotifier.setAlbumColorSync,
|
||||
),
|
||||
],
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,6 +221,12 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
preferencesNotifier.setDownloadMusicCodec(value);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.repeat),
|
||||
title: Text(context.l10n.endless_playback),
|
||||
value: preferences.endlessPlayback,
|
||||
onChanged: preferencesNotifier.setEndlessPlayback,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -144,8 +144,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// Removing tracks that were not found to avoid queue interruption
|
||||
// TODO: Add a flag to enable/disable skip not found tracks
|
||||
if (e is TrackNotFoundException) {
|
||||
if (e is TrackNotFoundError) {
|
||||
final oldTrack =
|
||||
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
||||
await removeTrack(oldTrack!.id!);
|
||||
|
||||
@ -123,6 +123,10 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
audioPlayer.setAudioNormalization(normalize);
|
||||
}
|
||||
|
||||
void setEndlessPlayback(bool endless) {
|
||||
state = state.copyWith(endlessPlayback: endless);
|
||||
}
|
||||
|
||||
Future<String> _getDefaultDownloadDirectory() async {
|
||||
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
|
||||
part 'user_preferences_state.g.dart';
|
||||
part 'user_preferences_state.freezed.dart';
|
||||
|
||||
@JsonEnum()
|
||||
enum LayoutMode {
|
||||
@ -53,40 +54,48 @@ enum SearchMode {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
final class UserPreferences {
|
||||
@JsonKey(
|
||||
defaultValue: SourceQualities.high,
|
||||
unknownEnumValue: SourceQualities.high,
|
||||
)
|
||||
final SourceQualities audioQuality;
|
||||
@freezed
|
||||
class UserPreferences with _$UserPreferences {
|
||||
const factory UserPreferences({
|
||||
@Default(SourceQualities.high) SourceQualities audioQuality,
|
||||
@Default(true) bool albumColorSync,
|
||||
@Default(false) bool amoledDarkTheme,
|
||||
@Default(true) bool checkUpdate,
|
||||
@Default(false) bool normalizeAudio,
|
||||
@Default(true) bool showSystemTrayIcon,
|
||||
@Default(false) bool skipNonMusic,
|
||||
@Default(false) bool systemTitleBar,
|
||||
@Default(CloseBehavior.minimizeToTray) CloseBehavior closeBehavior,
|
||||
@Default(SpotubeColor(0xFF2196F3, name: "Blue"))
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue,
|
||||
)
|
||||
SpotubeColor accentColorScheme,
|
||||
@Default(LayoutMode.adaptive) LayoutMode layoutMode,
|
||||
@Default(Locale("system", "system"))
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue,
|
||||
)
|
||||
Locale locale,
|
||||
@Default(Market.US) Market recommendationMarket,
|
||||
@Default(SearchMode.youtube) SearchMode searchMode,
|
||||
@Default("") String downloadLocation,
|
||||
@Default("https://pipedapi.kavin.rocks") String pipedInstance,
|
||||
@Default(ThemeMode.system) ThemeMode themeMode,
|
||||
@Default(AudioSource.youtube) AudioSource audioSource,
|
||||
@Default(SourceCodecs.weba) SourceCodecs streamMusicCodec,
|
||||
@Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec,
|
||||
@Default(true) bool discordPresence,
|
||||
@Default(true) bool endlessPlayback,
|
||||
}) = _UserPreferences;
|
||||
factory UserPreferences.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserPreferencesFromJson(json);
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool albumColorSync;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool amoledDarkTheme;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool checkUpdate;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool normalizeAudio;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool showSystemTrayIcon;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool skipNonMusic;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool systemTitleBar;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: CloseBehavior.minimizeToTray,
|
||||
unknownEnumValue: CloseBehavior.minimizeToTray,
|
||||
)
|
||||
final CloseBehavior closeBehavior;
|
||||
factory UserPreferences.withDefaults() => UserPreferences.fromJson({});
|
||||
|
||||
static SpotubeColor _accentColorSchemeFromJson(Map<String, dynamic> json) {
|
||||
return SpotubeColor.fromString(json["color"]);
|
||||
@ -105,23 +114,6 @@ final class UserPreferences {
|
||||
return {"color": color.toString()};
|
||||
}
|
||||
|
||||
static SpotubeColor _defaultAccentColorScheme() =>
|
||||
const SpotubeColor(0xFF2196F3, name: "Blue");
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: UserPreferences._defaultAccentColorScheme,
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue,
|
||||
)
|
||||
final SpotubeColor accentColorScheme;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: LayoutMode.adaptive,
|
||||
unknownEnumValue: LayoutMode.adaptive,
|
||||
)
|
||||
final LayoutMode layoutMode;
|
||||
|
||||
static Locale _localeFromJson(Map<String, dynamic> json) {
|
||||
return Locale(json["languageCode"], json["countryCode"]);
|
||||
}
|
||||
@ -145,144 +137,4 @@ final class UserPreferences {
|
||||
|
||||
return json[key] as Map<String, dynamic>?;
|
||||
}
|
||||
|
||||
static Locale _defaultLocaleValue() => const Locale("system", "system");
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: UserPreferences._defaultLocaleValue,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
readValue: UserPreferences._localeReadValue,
|
||||
)
|
||||
final Locale locale;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: Market.US,
|
||||
unknownEnumValue: Market.US,
|
||||
)
|
||||
final Market recommendationMarket;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: SearchMode.youtube,
|
||||
unknownEnumValue: SearchMode.youtube,
|
||||
)
|
||||
final SearchMode searchMode;
|
||||
|
||||
@JsonKey(defaultValue: "")
|
||||
final String downloadLocation;
|
||||
|
||||
@JsonKey(defaultValue: "https://pipedapi.kavin.rocks")
|
||||
final String pipedInstance;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: ThemeMode.system,
|
||||
unknownEnumValue: ThemeMode.system,
|
||||
)
|
||||
final ThemeMode themeMode;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: AudioSource.youtube,
|
||||
unknownEnumValue: AudioSource.youtube,
|
||||
)
|
||||
final AudioSource audioSource;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: SourceCodecs.weba,
|
||||
unknownEnumValue: SourceCodecs.weba,
|
||||
)
|
||||
final SourceCodecs streamMusicCodec;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: SourceCodecs.m4a,
|
||||
unknownEnumValue: SourceCodecs.m4a,
|
||||
)
|
||||
final SourceCodecs downloadMusicCodec;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool discordPresence;
|
||||
|
||||
UserPreferences({
|
||||
required this.audioQuality,
|
||||
required this.albumColorSync,
|
||||
required this.amoledDarkTheme,
|
||||
required this.checkUpdate,
|
||||
required this.normalizeAudio,
|
||||
required this.showSystemTrayIcon,
|
||||
required this.skipNonMusic,
|
||||
required this.systemTitleBar,
|
||||
required this.closeBehavior,
|
||||
required this.accentColorScheme,
|
||||
required this.layoutMode,
|
||||
required this.locale,
|
||||
required this.recommendationMarket,
|
||||
required this.searchMode,
|
||||
required this.downloadLocation,
|
||||
required this.pipedInstance,
|
||||
required this.themeMode,
|
||||
required this.audioSource,
|
||||
required this.streamMusicCodec,
|
||||
required this.downloadMusicCodec,
|
||||
required this.discordPresence,
|
||||
});
|
||||
|
||||
factory UserPreferences.withDefaults() {
|
||||
return UserPreferences.fromJson({});
|
||||
}
|
||||
|
||||
factory UserPreferences.fromJson(Map<String, dynamic> json) {
|
||||
return _$UserPreferencesFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$UserPreferencesToJson(this);
|
||||
}
|
||||
|
||||
UserPreferences copyWith({
|
||||
ThemeMode? themeMode,
|
||||
SpotubeColor? accentColorScheme,
|
||||
bool? albumColorSync,
|
||||
bool? checkUpdate,
|
||||
SourceQualities? audioQuality,
|
||||
String? downloadLocation,
|
||||
LayoutMode? layoutMode,
|
||||
CloseBehavior? closeBehavior,
|
||||
bool? showSystemTrayIcon,
|
||||
Locale? locale,
|
||||
String? pipedInstance,
|
||||
SearchMode? searchMode,
|
||||
bool? skipNonMusic,
|
||||
AudioSource? audioSource,
|
||||
Market? recommendationMarket,
|
||||
bool? saveTrackLyrics,
|
||||
bool? amoledDarkTheme,
|
||||
bool? normalizeAudio,
|
||||
SourceCodecs? downloadMusicCodec,
|
||||
SourceCodecs? streamMusicCodec,
|
||||
bool? systemTitleBar,
|
||||
bool? discordPresence,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
accentColorScheme: accentColorScheme ?? this.accentColorScheme,
|
||||
albumColorSync: albumColorSync ?? this.albumColorSync,
|
||||
checkUpdate: checkUpdate ?? this.checkUpdate,
|
||||
audioQuality: audioQuality ?? this.audioQuality,
|
||||
downloadLocation: downloadLocation ?? this.downloadLocation,
|
||||
layoutMode: layoutMode ?? this.layoutMode,
|
||||
closeBehavior: closeBehavior ?? this.closeBehavior,
|
||||
showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon,
|
||||
locale: locale ?? this.locale,
|
||||
pipedInstance: pipedInstance ?? this.pipedInstance,
|
||||
searchMode: searchMode ?? this.searchMode,
|
||||
skipNonMusic: skipNonMusic ?? this.skipNonMusic,
|
||||
audioSource: audioSource ?? this.audioSource,
|
||||
recommendationMarket: recommendationMarket ?? this.recommendationMarket,
|
||||
amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme,
|
||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||
normalizeAudio: normalizeAudio ?? this.normalizeAudio,
|
||||
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
||||
systemTitleBar: systemTitleBar ?? this.systemTitleBar,
|
||||
discordPresence: discordPresence ?? this.discordPresence,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,697 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'user_preferences_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
UserPreferences _$UserPreferencesFromJson(Map<String, dynamic> json) {
|
||||
return _UserPreferences.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UserPreferences {
|
||||
SourceQualities get audioQuality => throw _privateConstructorUsedError;
|
||||
bool get albumColorSync => throw _privateConstructorUsedError;
|
||||
bool get amoledDarkTheme => throw _privateConstructorUsedError;
|
||||
bool get checkUpdate => throw _privateConstructorUsedError;
|
||||
bool get normalizeAudio => throw _privateConstructorUsedError;
|
||||
bool get showSystemTrayIcon => throw _privateConstructorUsedError;
|
||||
bool get skipNonMusic => throw _privateConstructorUsedError;
|
||||
bool get systemTitleBar => throw _privateConstructorUsedError;
|
||||
CloseBehavior get closeBehavior => throw _privateConstructorUsedError;
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor get accentColorScheme => throw _privateConstructorUsedError;
|
||||
LayoutMode get layoutMode => throw _privateConstructorUsedError;
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale get locale => throw _privateConstructorUsedError;
|
||||
Market get recommendationMarket => throw _privateConstructorUsedError;
|
||||
SearchMode get searchMode => throw _privateConstructorUsedError;
|
||||
String get downloadLocation => throw _privateConstructorUsedError;
|
||||
String get pipedInstance => throw _privateConstructorUsedError;
|
||||
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
||||
AudioSource get audioSource => throw _privateConstructorUsedError;
|
||||
SourceCodecs get streamMusicCodec => throw _privateConstructorUsedError;
|
||||
SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError;
|
||||
bool get discordPresence => throw _privateConstructorUsedError;
|
||||
bool get endlessPlayback => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$UserPreferencesCopyWith<UserPreferences> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $UserPreferencesCopyWith<$Res> {
|
||||
factory $UserPreferencesCopyWith(
|
||||
UserPreferences value, $Res Function(UserPreferences) then) =
|
||||
_$UserPreferencesCopyWithImpl<$Res, UserPreferences>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{SourceQualities audioQuality,
|
||||
bool albumColorSync,
|
||||
bool amoledDarkTheme,
|
||||
bool checkUpdate,
|
||||
bool normalizeAudio,
|
||||
bool showSystemTrayIcon,
|
||||
bool skipNonMusic,
|
||||
bool systemTitleBar,
|
||||
CloseBehavior closeBehavior,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor accentColorScheme,
|
||||
LayoutMode layoutMode,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale locale,
|
||||
Market recommendationMarket,
|
||||
SearchMode searchMode,
|
||||
String downloadLocation,
|
||||
String pipedInstance,
|
||||
ThemeMode themeMode,
|
||||
AudioSource audioSource,
|
||||
SourceCodecs streamMusicCodec,
|
||||
SourceCodecs downloadMusicCodec,
|
||||
bool discordPresence,
|
||||
bool endlessPlayback});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
|
||||
implements $UserPreferencesCopyWith<$Res> {
|
||||
_$UserPreferencesCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? audioQuality = null,
|
||||
Object? albumColorSync = null,
|
||||
Object? amoledDarkTheme = null,
|
||||
Object? checkUpdate = null,
|
||||
Object? normalizeAudio = null,
|
||||
Object? showSystemTrayIcon = null,
|
||||
Object? skipNonMusic = null,
|
||||
Object? systemTitleBar = null,
|
||||
Object? closeBehavior = null,
|
||||
Object? accentColorScheme = null,
|
||||
Object? layoutMode = null,
|
||||
Object? locale = null,
|
||||
Object? recommendationMarket = null,
|
||||
Object? searchMode = null,
|
||||
Object? downloadLocation = null,
|
||||
Object? pipedInstance = null,
|
||||
Object? themeMode = null,
|
||||
Object? audioSource = null,
|
||||
Object? streamMusicCodec = null,
|
||||
Object? downloadMusicCodec = null,
|
||||
Object? discordPresence = null,
|
||||
Object? endlessPlayback = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
audioQuality: null == audioQuality
|
||||
? _value.audioQuality
|
||||
: audioQuality // ignore: cast_nullable_to_non_nullable
|
||||
as SourceQualities,
|
||||
albumColorSync: null == albumColorSync
|
||||
? _value.albumColorSync
|
||||
: albumColorSync // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
amoledDarkTheme: null == amoledDarkTheme
|
||||
? _value.amoledDarkTheme
|
||||
: amoledDarkTheme // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
checkUpdate: null == checkUpdate
|
||||
? _value.checkUpdate
|
||||
: checkUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
normalizeAudio: null == normalizeAudio
|
||||
? _value.normalizeAudio
|
||||
: normalizeAudio // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
showSystemTrayIcon: null == showSystemTrayIcon
|
||||
? _value.showSystemTrayIcon
|
||||
: showSystemTrayIcon // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
skipNonMusic: null == skipNonMusic
|
||||
? _value.skipNonMusic
|
||||
: skipNonMusic // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
systemTitleBar: null == systemTitleBar
|
||||
? _value.systemTitleBar
|
||||
: systemTitleBar // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
closeBehavior: null == closeBehavior
|
||||
? _value.closeBehavior
|
||||
: closeBehavior // ignore: cast_nullable_to_non_nullable
|
||||
as CloseBehavior,
|
||||
accentColorScheme: null == accentColorScheme
|
||||
? _value.accentColorScheme
|
||||
: accentColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as SpotubeColor,
|
||||
layoutMode: null == layoutMode
|
||||
? _value.layoutMode
|
||||
: layoutMode // ignore: cast_nullable_to_non_nullable
|
||||
as LayoutMode,
|
||||
locale: null == locale
|
||||
? _value.locale
|
||||
: locale // ignore: cast_nullable_to_non_nullable
|
||||
as Locale,
|
||||
recommendationMarket: null == recommendationMarket
|
||||
? _value.recommendationMarket
|
||||
: recommendationMarket // ignore: cast_nullable_to_non_nullable
|
||||
as Market,
|
||||
searchMode: null == searchMode
|
||||
? _value.searchMode
|
||||
: searchMode // ignore: cast_nullable_to_non_nullable
|
||||
as SearchMode,
|
||||
downloadLocation: null == downloadLocation
|
||||
? _value.downloadLocation
|
||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
pipedInstance: null == pipedInstance
|
||||
? _value.pipedInstance
|
||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
themeMode: null == themeMode
|
||||
? _value.themeMode
|
||||
: themeMode // ignore: cast_nullable_to_non_nullable
|
||||
as ThemeMode,
|
||||
audioSource: null == audioSource
|
||||
? _value.audioSource
|
||||
: audioSource // ignore: cast_nullable_to_non_nullable
|
||||
as AudioSource,
|
||||
streamMusicCodec: null == streamMusicCodec
|
||||
? _value.streamMusicCodec
|
||||
: streamMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
downloadMusicCodec: null == downloadMusicCodec
|
||||
? _value.downloadMusicCodec
|
||||
: downloadMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
discordPresence: null == discordPresence
|
||||
? _value.discordPresence
|
||||
: discordPresence // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
endlessPlayback: null == endlessPlayback
|
||||
? _value.endlessPlayback
|
||||
: endlessPlayback // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$UserPreferencesImplCopyWith<$Res>
|
||||
implements $UserPreferencesCopyWith<$Res> {
|
||||
factory _$$UserPreferencesImplCopyWith(_$UserPreferencesImpl value,
|
||||
$Res Function(_$UserPreferencesImpl) then) =
|
||||
__$$UserPreferencesImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{SourceQualities audioQuality,
|
||||
bool albumColorSync,
|
||||
bool amoledDarkTheme,
|
||||
bool checkUpdate,
|
||||
bool normalizeAudio,
|
||||
bool showSystemTrayIcon,
|
||||
bool skipNonMusic,
|
||||
bool systemTitleBar,
|
||||
CloseBehavior closeBehavior,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor accentColorScheme,
|
||||
LayoutMode layoutMode,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale locale,
|
||||
Market recommendationMarket,
|
||||
SearchMode searchMode,
|
||||
String downloadLocation,
|
||||
String pipedInstance,
|
||||
ThemeMode themeMode,
|
||||
AudioSource audioSource,
|
||||
SourceCodecs streamMusicCodec,
|
||||
SourceCodecs downloadMusicCodec,
|
||||
bool discordPresence,
|
||||
bool endlessPlayback});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$UserPreferencesImplCopyWithImpl<$Res>
|
||||
extends _$UserPreferencesCopyWithImpl<$Res, _$UserPreferencesImpl>
|
||||
implements _$$UserPreferencesImplCopyWith<$Res> {
|
||||
__$$UserPreferencesImplCopyWithImpl(
|
||||
_$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? audioQuality = null,
|
||||
Object? albumColorSync = null,
|
||||
Object? amoledDarkTheme = null,
|
||||
Object? checkUpdate = null,
|
||||
Object? normalizeAudio = null,
|
||||
Object? showSystemTrayIcon = null,
|
||||
Object? skipNonMusic = null,
|
||||
Object? systemTitleBar = null,
|
||||
Object? closeBehavior = null,
|
||||
Object? accentColorScheme = null,
|
||||
Object? layoutMode = null,
|
||||
Object? locale = null,
|
||||
Object? recommendationMarket = null,
|
||||
Object? searchMode = null,
|
||||
Object? downloadLocation = null,
|
||||
Object? pipedInstance = null,
|
||||
Object? themeMode = null,
|
||||
Object? audioSource = null,
|
||||
Object? streamMusicCodec = null,
|
||||
Object? downloadMusicCodec = null,
|
||||
Object? discordPresence = null,
|
||||
Object? endlessPlayback = null,
|
||||
}) {
|
||||
return _then(_$UserPreferencesImpl(
|
||||
audioQuality: null == audioQuality
|
||||
? _value.audioQuality
|
||||
: audioQuality // ignore: cast_nullable_to_non_nullable
|
||||
as SourceQualities,
|
||||
albumColorSync: null == albumColorSync
|
||||
? _value.albumColorSync
|
||||
: albumColorSync // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
amoledDarkTheme: null == amoledDarkTheme
|
||||
? _value.amoledDarkTheme
|
||||
: amoledDarkTheme // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
checkUpdate: null == checkUpdate
|
||||
? _value.checkUpdate
|
||||
: checkUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
normalizeAudio: null == normalizeAudio
|
||||
? _value.normalizeAudio
|
||||
: normalizeAudio // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
showSystemTrayIcon: null == showSystemTrayIcon
|
||||
? _value.showSystemTrayIcon
|
||||
: showSystemTrayIcon // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
skipNonMusic: null == skipNonMusic
|
||||
? _value.skipNonMusic
|
||||
: skipNonMusic // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
systemTitleBar: null == systemTitleBar
|
||||
? _value.systemTitleBar
|
||||
: systemTitleBar // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
closeBehavior: null == closeBehavior
|
||||
? _value.closeBehavior
|
||||
: closeBehavior // ignore: cast_nullable_to_non_nullable
|
||||
as CloseBehavior,
|
||||
accentColorScheme: null == accentColorScheme
|
||||
? _value.accentColorScheme
|
||||
: accentColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as SpotubeColor,
|
||||
layoutMode: null == layoutMode
|
||||
? _value.layoutMode
|
||||
: layoutMode // ignore: cast_nullable_to_non_nullable
|
||||
as LayoutMode,
|
||||
locale: null == locale
|
||||
? _value.locale
|
||||
: locale // ignore: cast_nullable_to_non_nullable
|
||||
as Locale,
|
||||
recommendationMarket: null == recommendationMarket
|
||||
? _value.recommendationMarket
|
||||
: recommendationMarket // ignore: cast_nullable_to_non_nullable
|
||||
as Market,
|
||||
searchMode: null == searchMode
|
||||
? _value.searchMode
|
||||
: searchMode // ignore: cast_nullable_to_non_nullable
|
||||
as SearchMode,
|
||||
downloadLocation: null == downloadLocation
|
||||
? _value.downloadLocation
|
||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
pipedInstance: null == pipedInstance
|
||||
? _value.pipedInstance
|
||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
themeMode: null == themeMode
|
||||
? _value.themeMode
|
||||
: themeMode // ignore: cast_nullable_to_non_nullable
|
||||
as ThemeMode,
|
||||
audioSource: null == audioSource
|
||||
? _value.audioSource
|
||||
: audioSource // ignore: cast_nullable_to_non_nullable
|
||||
as AudioSource,
|
||||
streamMusicCodec: null == streamMusicCodec
|
||||
? _value.streamMusicCodec
|
||||
: streamMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
downloadMusicCodec: null == downloadMusicCodec
|
||||
? _value.downloadMusicCodec
|
||||
: downloadMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
discordPresence: null == discordPresence
|
||||
? _value.discordPresence
|
||||
: discordPresence // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
endlessPlayback: null == endlessPlayback
|
||||
? _value.endlessPlayback
|
||||
: endlessPlayback // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$UserPreferencesImpl implements _UserPreferences {
|
||||
const _$UserPreferencesImpl(
|
||||
{this.audioQuality = SourceQualities.high,
|
||||
this.albumColorSync = true,
|
||||
this.amoledDarkTheme = false,
|
||||
this.checkUpdate = true,
|
||||
this.normalizeAudio = false,
|
||||
this.showSystemTrayIcon = true,
|
||||
this.skipNonMusic = false,
|
||||
this.systemTitleBar = false,
|
||||
this.closeBehavior = CloseBehavior.minimizeToTray,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
this.accentColorScheme = const SpotubeColor(0xFF2196F3, name: "Blue"),
|
||||
this.layoutMode = LayoutMode.adaptive,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
this.locale = const Locale("system", "system"),
|
||||
this.recommendationMarket = Market.US,
|
||||
this.searchMode = SearchMode.youtube,
|
||||
this.downloadLocation = "",
|
||||
this.pipedInstance = "https://pipedapi.kavin.rocks",
|
||||
this.themeMode = ThemeMode.system,
|
||||
this.audioSource = AudioSource.youtube,
|
||||
this.streamMusicCodec = SourceCodecs.weba,
|
||||
this.downloadMusicCodec = SourceCodecs.m4a,
|
||||
this.discordPresence = true,
|
||||
this.endlessPlayback = true});
|
||||
|
||||
factory _$UserPreferencesImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$UserPreferencesImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final SourceQualities audioQuality;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool albumColorSync;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool amoledDarkTheme;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool checkUpdate;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool normalizeAudio;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool showSystemTrayIcon;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool skipNonMusic;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool systemTitleBar;
|
||||
@override
|
||||
@JsonKey()
|
||||
final CloseBehavior closeBehavior;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
final SpotubeColor accentColorScheme;
|
||||
@override
|
||||
@JsonKey()
|
||||
final LayoutMode layoutMode;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
final Locale locale;
|
||||
@override
|
||||
@JsonKey()
|
||||
final Market recommendationMarket;
|
||||
@override
|
||||
@JsonKey()
|
||||
final SearchMode searchMode;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String downloadLocation;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String pipedInstance;
|
||||
@override
|
||||
@JsonKey()
|
||||
final ThemeMode themeMode;
|
||||
@override
|
||||
@JsonKey()
|
||||
final AudioSource audioSource;
|
||||
@override
|
||||
@JsonKey()
|
||||
final SourceCodecs streamMusicCodec;
|
||||
@override
|
||||
@JsonKey()
|
||||
final SourceCodecs downloadMusicCodec;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool discordPresence;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool endlessPlayback;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$UserPreferencesImpl &&
|
||||
(identical(other.audioQuality, audioQuality) ||
|
||||
other.audioQuality == audioQuality) &&
|
||||
(identical(other.albumColorSync, albumColorSync) ||
|
||||
other.albumColorSync == albumColorSync) &&
|
||||
(identical(other.amoledDarkTheme, amoledDarkTheme) ||
|
||||
other.amoledDarkTheme == amoledDarkTheme) &&
|
||||
(identical(other.checkUpdate, checkUpdate) ||
|
||||
other.checkUpdate == checkUpdate) &&
|
||||
(identical(other.normalizeAudio, normalizeAudio) ||
|
||||
other.normalizeAudio == normalizeAudio) &&
|
||||
(identical(other.showSystemTrayIcon, showSystemTrayIcon) ||
|
||||
other.showSystemTrayIcon == showSystemTrayIcon) &&
|
||||
(identical(other.skipNonMusic, skipNonMusic) ||
|
||||
other.skipNonMusic == skipNonMusic) &&
|
||||
(identical(other.systemTitleBar, systemTitleBar) ||
|
||||
other.systemTitleBar == systemTitleBar) &&
|
||||
(identical(other.closeBehavior, closeBehavior) ||
|
||||
other.closeBehavior == closeBehavior) &&
|
||||
(identical(other.accentColorScheme, accentColorScheme) ||
|
||||
other.accentColorScheme == accentColorScheme) &&
|
||||
(identical(other.layoutMode, layoutMode) ||
|
||||
other.layoutMode == layoutMode) &&
|
||||
(identical(other.locale, locale) || other.locale == locale) &&
|
||||
(identical(other.recommendationMarket, recommendationMarket) ||
|
||||
other.recommendationMarket == recommendationMarket) &&
|
||||
(identical(other.searchMode, searchMode) ||
|
||||
other.searchMode == searchMode) &&
|
||||
(identical(other.downloadLocation, downloadLocation) ||
|
||||
other.downloadLocation == downloadLocation) &&
|
||||
(identical(other.pipedInstance, pipedInstance) ||
|
||||
other.pipedInstance == pipedInstance) &&
|
||||
(identical(other.themeMode, themeMode) ||
|
||||
other.themeMode == themeMode) &&
|
||||
(identical(other.audioSource, audioSource) ||
|
||||
other.audioSource == audioSource) &&
|
||||
(identical(other.streamMusicCodec, streamMusicCodec) ||
|
||||
other.streamMusicCodec == streamMusicCodec) &&
|
||||
(identical(other.downloadMusicCodec, downloadMusicCodec) ||
|
||||
other.downloadMusicCodec == downloadMusicCodec) &&
|
||||
(identical(other.discordPresence, discordPresence) ||
|
||||
other.discordPresence == discordPresence) &&
|
||||
(identical(other.endlessPlayback, endlessPlayback) ||
|
||||
other.endlessPlayback == endlessPlayback));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
audioQuality,
|
||||
albumColorSync,
|
||||
amoledDarkTheme,
|
||||
checkUpdate,
|
||||
normalizeAudio,
|
||||
showSystemTrayIcon,
|
||||
skipNonMusic,
|
||||
systemTitleBar,
|
||||
closeBehavior,
|
||||
accentColorScheme,
|
||||
layoutMode,
|
||||
locale,
|
||||
recommendationMarket,
|
||||
searchMode,
|
||||
downloadLocation,
|
||||
pipedInstance,
|
||||
themeMode,
|
||||
audioSource,
|
||||
streamMusicCodec,
|
||||
downloadMusicCodec,
|
||||
discordPresence,
|
||||
endlessPlayback
|
||||
]);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
|
||||
__$$UserPreferencesImplCopyWithImpl<_$UserPreferencesImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$UserPreferencesImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _UserPreferences implements UserPreferences {
|
||||
const factory _UserPreferences(
|
||||
{final SourceQualities audioQuality,
|
||||
final bool albumColorSync,
|
||||
final bool amoledDarkTheme,
|
||||
final bool checkUpdate,
|
||||
final bool normalizeAudio,
|
||||
final bool showSystemTrayIcon,
|
||||
final bool skipNonMusic,
|
||||
final bool systemTitleBar,
|
||||
final CloseBehavior closeBehavior,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
final SpotubeColor accentColorScheme,
|
||||
final LayoutMode layoutMode,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
final Locale locale,
|
||||
final Market recommendationMarket,
|
||||
final SearchMode searchMode,
|
||||
final String downloadLocation,
|
||||
final String pipedInstance,
|
||||
final ThemeMode themeMode,
|
||||
final AudioSource audioSource,
|
||||
final SourceCodecs streamMusicCodec,
|
||||
final SourceCodecs downloadMusicCodec,
|
||||
final bool discordPresence,
|
||||
final bool endlessPlayback}) = _$UserPreferencesImpl;
|
||||
|
||||
factory _UserPreferences.fromJson(Map<String, dynamic> json) =
|
||||
_$UserPreferencesImpl.fromJson;
|
||||
|
||||
@override
|
||||
SourceQualities get audioQuality;
|
||||
@override
|
||||
bool get albumColorSync;
|
||||
@override
|
||||
bool get amoledDarkTheme;
|
||||
@override
|
||||
bool get checkUpdate;
|
||||
@override
|
||||
bool get normalizeAudio;
|
||||
@override
|
||||
bool get showSystemTrayIcon;
|
||||
@override
|
||||
bool get skipNonMusic;
|
||||
@override
|
||||
bool get systemTitleBar;
|
||||
@override
|
||||
CloseBehavior get closeBehavior;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor get accentColorScheme;
|
||||
@override
|
||||
LayoutMode get layoutMode;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale get locale;
|
||||
@override
|
||||
Market get recommendationMarket;
|
||||
@override
|
||||
SearchMode get searchMode;
|
||||
@override
|
||||
String get downloadLocation;
|
||||
@override
|
||||
String get pipedInstance;
|
||||
@override
|
||||
ThemeMode get themeMode;
|
||||
@override
|
||||
AudioSource get audioSource;
|
||||
@override
|
||||
SourceCodecs get streamMusicCodec;
|
||||
@override
|
||||
SourceCodecs get downloadMusicCodec;
|
||||
@override
|
||||
bool get discordPresence;
|
||||
@override
|
||||
bool get endlessPlayback;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -6,67 +6,63 @@ part of 'user_preferences_state.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
UserPreferences _$UserPreferencesFromJson(Map<String, dynamic> json) =>
|
||||
UserPreferences(
|
||||
audioQuality: $enumDecodeNullable(
|
||||
_$SourceQualitiesEnumMap, json['audioQuality'],
|
||||
unknownValue: SourceQualities.high) ??
|
||||
SourceQualities.high,
|
||||
_$UserPreferencesImpl _$$UserPreferencesImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$UserPreferencesImpl(
|
||||
audioQuality:
|
||||
$enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ??
|
||||
SourceQualities.high,
|
||||
albumColorSync: json['albumColorSync'] as bool? ?? true,
|
||||
amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false,
|
||||
checkUpdate: json['checkUpdate'] as bool? ?? true,
|
||||
normalizeAudio: json['normalizeAudio'] as bool? ?? false,
|
||||
showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? true,
|
||||
skipNonMusic: json['skipNonMusic'] as bool? ?? true,
|
||||
skipNonMusic: json['skipNonMusic'] as bool? ?? false,
|
||||
systemTitleBar: json['systemTitleBar'] as bool? ?? false,
|
||||
closeBehavior: $enumDecodeNullable(
|
||||
_$CloseBehaviorEnumMap, json['closeBehavior'],
|
||||
unknownValue: CloseBehavior.minimizeToTray) ??
|
||||
CloseBehavior.minimizeToTray,
|
||||
closeBehavior:
|
||||
$enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ??
|
||||
CloseBehavior.minimizeToTray,
|
||||
accentColorScheme: UserPreferences._accentColorSchemeReadValue(
|
||||
json, 'accentColorScheme') ==
|
||||
null
|
||||
? UserPreferences._defaultAccentColorScheme()
|
||||
? const SpotubeColor(0xFF2196F3, name: "Blue")
|
||||
: UserPreferences._accentColorSchemeFromJson(
|
||||
UserPreferences._accentColorSchemeReadValue(
|
||||
json, 'accentColorScheme') as Map<String, dynamic>),
|
||||
layoutMode: $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode'],
|
||||
unknownValue: LayoutMode.adaptive) ??
|
||||
LayoutMode.adaptive,
|
||||
layoutMode:
|
||||
$enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode']) ??
|
||||
LayoutMode.adaptive,
|
||||
locale: UserPreferences._localeReadValue(json, 'locale') == null
|
||||
? UserPreferences._defaultLocaleValue()
|
||||
? const Locale("system", "system")
|
||||
: UserPreferences._localeFromJson(
|
||||
UserPreferences._localeReadValue(json, 'locale')
|
||||
as Map<String, dynamic>),
|
||||
recommendationMarket: $enumDecodeNullable(
|
||||
_$MarketEnumMap, json['recommendationMarket'],
|
||||
unknownValue: Market.US) ??
|
||||
Market.US,
|
||||
searchMode: $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode'],
|
||||
unknownValue: SearchMode.youtube) ??
|
||||
SearchMode.youtube,
|
||||
downloadLocation: json['downloadLocation'] as String? ?? '',
|
||||
recommendationMarket:
|
||||
$enumDecodeNullable(_$MarketEnumMap, json['recommendationMarket']) ??
|
||||
Market.US,
|
||||
searchMode:
|
||||
$enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ??
|
||||
SearchMode.youtube,
|
||||
downloadLocation: json['downloadLocation'] as String? ?? "",
|
||||
pipedInstance:
|
||||
json['pipedInstance'] as String? ?? 'https://pipedapi.kavin.rocks',
|
||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'],
|
||||
unknownValue: ThemeMode.system) ??
|
||||
json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks",
|
||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||
ThemeMode.system,
|
||||
audioSource: $enumDecodeNullable(
|
||||
_$AudioSourceEnumMap, json['audioSource'],
|
||||
unknownValue: AudioSource.youtube) ??
|
||||
AudioSource.youtube,
|
||||
audioSource:
|
||||
$enumDecodeNullable(_$AudioSourceEnumMap, json['audioSource']) ??
|
||||
AudioSource.youtube,
|
||||
streamMusicCodec: $enumDecodeNullable(
|
||||
_$SourceCodecsEnumMap, json['streamMusicCodec'],
|
||||
unknownValue: SourceCodecs.weba) ??
|
||||
_$SourceCodecsEnumMap, json['streamMusicCodec']) ??
|
||||
SourceCodecs.weba,
|
||||
downloadMusicCodec: $enumDecodeNullable(
|
||||
_$SourceCodecsEnumMap, json['downloadMusicCodec'],
|
||||
unknownValue: SourceCodecs.m4a) ??
|
||||
_$SourceCodecsEnumMap, json['downloadMusicCodec']) ??
|
||||
SourceCodecs.m4a,
|
||||
discordPresence: json['discordPresence'] as bool? ?? true,
|
||||
endlessPlayback: json['endlessPlayback'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserPreferencesToJson(UserPreferences instance) =>
|
||||
Map<String, dynamic> _$$UserPreferencesImplToJson(
|
||||
_$UserPreferencesImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!,
|
||||
'albumColorSync': instance.albumColorSync,
|
||||
@ -90,6 +86,7 @@ Map<String, dynamic> _$UserPreferencesToJson(UserPreferences instance) =>
|
||||
'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!,
|
||||
'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!,
|
||||
'discordPresence': instance.discordPresence,
|
||||
'endlessPlayback': instance.endlessPlayback,
|
||||
};
|
||||
|
||||
const _$SourceQualitiesEnumMap = {
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:flutter_broadcasts/flutter_broadcasts.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||
|
||||
@ -14,6 +17,13 @@ class MkPlayerWithState extends Player {
|
||||
final StreamController<bool> _shuffleStream;
|
||||
final StreamController<PlaylistMode> _loopModeStream;
|
||||
|
||||
static const String EXTRA_PACKAGE_NAME = "android.media.extra.PACKAGE_NAME";
|
||||
static const String EXTRA_AUDIO_SESSION = "android.media.extra.AUDIO_SESSION";
|
||||
static const String ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION =
|
||||
"android.media.action.OPEN_AUDIO_EFFECT_CONTROL_SESSION";
|
||||
static const String ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION =
|
||||
"android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION";
|
||||
|
||||
late final List<StreamSubscription> _subscriptions;
|
||||
|
||||
bool _shuffled;
|
||||
@ -21,6 +31,9 @@ class MkPlayerWithState extends Player {
|
||||
|
||||
Playlist? _playlist;
|
||||
List<Media>? _tempMedias;
|
||||
int _androidAudioSessionId = 0;
|
||||
String _packageName = "";
|
||||
AndroidAudioManager? _androidAudioManager;
|
||||
|
||||
MkPlayerWithState({super.configuration})
|
||||
: _playerStateStream = StreamController.broadcast(),
|
||||
@ -64,6 +77,34 @@ class MkPlayerWithState extends Player {
|
||||
Catcher2.reportCheckedError('[MediaKitError] \n$event', null);
|
||||
}),
|
||||
];
|
||||
PackageInfo.fromPlatform().then((packageInfo) {
|
||||
_packageName = packageInfo.packageName;
|
||||
});
|
||||
if (DesktopTools.platform.isAndroid) {
|
||||
_androidAudioManager = AndroidAudioManager();
|
||||
AudioSession.instance.then((s) async {
|
||||
_androidAudioSessionId =
|
||||
await _androidAudioManager!.generateAudioSessionId();
|
||||
notifyAudioSessionUpdate(true);
|
||||
|
||||
nativePlayer.setProperty(
|
||||
"audiotrack-session-id", _androidAudioSessionId.toString());
|
||||
nativePlayer.setProperty("ao", "audiotrack,opensles,");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> notifyAudioSessionUpdate(bool active) async {
|
||||
if (DesktopTools.platform.isAndroid) {
|
||||
sendBroadcast(BroadcastMessage(
|
||||
name: active
|
||||
? ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION
|
||||
: ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION,
|
||||
data: {
|
||||
EXTRA_AUDIO_SESSION: _androidAudioSessionId,
|
||||
EXTRA_PACKAGE_NAME: _packageName
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
bool get shuffled => _shuffled;
|
||||
@ -140,10 +181,11 @@ class MkPlayerWithState extends Player {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() {
|
||||
Future<void> dispose() async {
|
||||
for (var element in _subscriptions) {
|
||||
element.cancel();
|
||||
}
|
||||
await notifyAudioSessionUpdate(false);
|
||||
return super.dispose();
|
||||
}
|
||||
|
||||
|
||||
15
lib/services/kv_store/kv_store.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class KVStoreService {
|
||||
static SharedPreferences? _sharedPreferences;
|
||||
static SharedPreferences get sharedPreferences => _sharedPreferences!;
|
||||
|
||||
static Future<void> initialize() async {
|
||||
_sharedPreferences = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
static bool get doneGettingStarted =>
|
||||
sharedPreferences.getBool('doneGettingStarted') ?? false;
|
||||
static set doneGettingStarted(bool value) =>
|
||||
sharedPreferences.setBool('doneGettingStarted', value);
|
||||
}
|
||||
@ -1,36 +1,60 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
|
||||
typedef SearchParams = ({
|
||||
SpotifyApi spotify,
|
||||
SearchType searchType,
|
||||
String query
|
||||
});
|
||||
|
||||
class SearchQueries {
|
||||
const SearchQueries();
|
||||
|
||||
static final queryJob =
|
||||
InfiniteQueryJob.withVariableKey<List<Page>, dynamic, int, SearchParams>(
|
||||
baseQueryKey: "search-query",
|
||||
task: (variableKey, page, args) => args!.spotify.search.get(
|
||||
args.query,
|
||||
types: [args.searchType],
|
||||
).getPage(10, page),
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isEmpty) return null;
|
||||
if ((lastPageData.first.isLast ||
|
||||
(lastPageData.first.items ?? []).length < 10)) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.first.nextOffset;
|
||||
},
|
||||
enabled: false,
|
||||
);
|
||||
|
||||
InfiniteQuery<List<Page>, dynamic, int> query(
|
||||
WidgetRef ref,
|
||||
String query,
|
||||
String queryStr,
|
||||
SearchType searchType,
|
||||
) {
|
||||
return useSpotifyInfiniteQuery<List<Page>, dynamic, int>(
|
||||
"search-query/${searchType.name}",
|
||||
(page, spotify) {
|
||||
if (query.trim().isEmpty) return [];
|
||||
final queryString = query;
|
||||
return spotify.search.get(
|
||||
queryString,
|
||||
types: [searchType],
|
||||
).getPage(10, page);
|
||||
},
|
||||
enabled: false,
|
||||
ref: ref,
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isEmpty) return null;
|
||||
if ((lastPageData.first.isLast ||
|
||||
(lastPageData.first.items ?? []).length < 10)) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.first.nextOffset;
|
||||
},
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final query = useInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
|
||||
job: queryJob(searchType.name),
|
||||
args: (spotify: spotify, searchType: searchType, query: queryStr),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
spotifyProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refreshAll();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
19
lib/services/song_link/model.dart
Normal file
@ -0,0 +1,19 @@
|
||||
part of './song_link.dart';
|
||||
|
||||
@freezed
|
||||
class SongLink with _$SongLink {
|
||||
const factory SongLink({
|
||||
required String displayName,
|
||||
required String linkId,
|
||||
required String platform,
|
||||
required bool show,
|
||||
required String? uniqueId,
|
||||
required String? country,
|
||||
required String? url,
|
||||
required String? nativeAppUriMobile,
|
||||
required String? nativeAppUriDesktop,
|
||||
}) = _SongLink;
|
||||
|
||||
factory SongLink.fromJson(Map<String, dynamic> json) =>
|
||||
_$SongLinkFromJson(json);
|
||||
}
|
||||
54
lib/services/song_link/song_link.dart
Normal file
@ -0,0 +1,54 @@
|
||||
library song_link;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:html/parser.dart';
|
||||
|
||||
part 'model.dart';
|
||||
|
||||
part 'song_link.freezed.dart';
|
||||
part 'song_link.g.dart';
|
||||
|
||||
abstract class SongLinkService {
|
||||
static final dio = Dio();
|
||||
static Future<List<SongLink>> links(String spotifyId) async {
|
||||
try {
|
||||
final res = await dio.get(
|
||||
"https://song.link/s/$spotifyId",
|
||||
options: Options(
|
||||
headers: {
|
||||
"Accept":
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||||
},
|
||||
responseType: ResponseType.plain,
|
||||
),
|
||||
);
|
||||
|
||||
final document = parse(res.data);
|
||||
|
||||
final script = document.getElementById("__NEXT_DATA__")?.text;
|
||||
|
||||
if (script == null) {
|
||||
return <SongLink>[];
|
||||
}
|
||||
|
||||
final pageProps = jsonDecode(script) as Map<String, dynamic>;
|
||||
final songLinks = pageProps["props"]?["pageProps"]?["pageData"]
|
||||
?["sections"]
|
||||
?.firstWhere(
|
||||
(section) => section?["sectionId"] == "section|auto|links|listen",
|
||||
)?["links"] as List?;
|
||||
|
||||
return songLinks?.map((link) => SongLink.fromJson(link)).toList() ??
|
||||
<SongLink>[];
|
||||
} catch (e, stackTrace) {
|
||||
Catcher2.reportCheckedError(e, stackTrace);
|
||||
return <SongLink>[];
|
||||
}
|
||||
}
|
||||
}
|
||||
320
lib/services/song_link/song_link.freezed.dart
Normal file
@ -0,0 +1,320 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'song_link.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
SongLink _$SongLinkFromJson(Map<String, dynamic> json) {
|
||||
return _SongLink.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SongLink {
|
||||
String get displayName => throw _privateConstructorUsedError;
|
||||
String get linkId => throw _privateConstructorUsedError;
|
||||
String get platform => throw _privateConstructorUsedError;
|
||||
bool get show => throw _privateConstructorUsedError;
|
||||
String? get uniqueId => throw _privateConstructorUsedError;
|
||||
String? get country => throw _privateConstructorUsedError;
|
||||
String? get url => throw _privateConstructorUsedError;
|
||||
String? get nativeAppUriMobile => throw _privateConstructorUsedError;
|
||||
String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$SongLinkCopyWith<SongLink> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SongLinkCopyWith<$Res> {
|
||||
factory $SongLinkCopyWith(SongLink value, $Res Function(SongLink) then) =
|
||||
_$SongLinkCopyWithImpl<$Res, SongLink>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String displayName,
|
||||
String linkId,
|
||||
String platform,
|
||||
bool show,
|
||||
String? uniqueId,
|
||||
String? country,
|
||||
String? url,
|
||||
String? nativeAppUriMobile,
|
||||
String? nativeAppUriDesktop});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink>
|
||||
implements $SongLinkCopyWith<$Res> {
|
||||
_$SongLinkCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? displayName = null,
|
||||
Object? linkId = null,
|
||||
Object? platform = null,
|
||||
Object? show = null,
|
||||
Object? uniqueId = freezed,
|
||||
Object? country = freezed,
|
||||
Object? url = freezed,
|
||||
Object? nativeAppUriMobile = freezed,
|
||||
Object? nativeAppUriDesktop = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
displayName: null == displayName
|
||||
? _value.displayName
|
||||
: displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
linkId: null == linkId
|
||||
? _value.linkId
|
||||
: linkId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
platform: null == platform
|
||||
? _value.platform
|
||||
: platform // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
show: null == show
|
||||
? _value.show
|
||||
: show // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
uniqueId: freezed == uniqueId
|
||||
? _value.uniqueId
|
||||
: uniqueId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
country: freezed == country
|
||||
? _value.country
|
||||
: country // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: freezed == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriMobile: freezed == nativeAppUriMobile
|
||||
? _value.nativeAppUriMobile
|
||||
: nativeAppUriMobile // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriDesktop: freezed == nativeAppUriDesktop
|
||||
? _value.nativeAppUriDesktop
|
||||
: nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SongLinkImplCopyWith<$Res>
|
||||
implements $SongLinkCopyWith<$Res> {
|
||||
factory _$$SongLinkImplCopyWith(
|
||||
_$SongLinkImpl value, $Res Function(_$SongLinkImpl) then) =
|
||||
__$$SongLinkImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String displayName,
|
||||
String linkId,
|
||||
String platform,
|
||||
bool show,
|
||||
String? uniqueId,
|
||||
String? country,
|
||||
String? url,
|
||||
String? nativeAppUriMobile,
|
||||
String? nativeAppUriDesktop});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SongLinkImplCopyWithImpl<$Res>
|
||||
extends _$SongLinkCopyWithImpl<$Res, _$SongLinkImpl>
|
||||
implements _$$SongLinkImplCopyWith<$Res> {
|
||||
__$$SongLinkImplCopyWithImpl(
|
||||
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? displayName = null,
|
||||
Object? linkId = null,
|
||||
Object? platform = null,
|
||||
Object? show = null,
|
||||
Object? uniqueId = freezed,
|
||||
Object? country = freezed,
|
||||
Object? url = freezed,
|
||||
Object? nativeAppUriMobile = freezed,
|
||||
Object? nativeAppUriDesktop = freezed,
|
||||
}) {
|
||||
return _then(_$SongLinkImpl(
|
||||
displayName: null == displayName
|
||||
? _value.displayName
|
||||
: displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
linkId: null == linkId
|
||||
? _value.linkId
|
||||
: linkId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
platform: null == platform
|
||||
? _value.platform
|
||||
: platform // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
show: null == show
|
||||
? _value.show
|
||||
: show // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
uniqueId: freezed == uniqueId
|
||||
? _value.uniqueId
|
||||
: uniqueId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
country: freezed == country
|
||||
? _value.country
|
||||
: country // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: freezed == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriMobile: freezed == nativeAppUriMobile
|
||||
? _value.nativeAppUriMobile
|
||||
: nativeAppUriMobile // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriDesktop: freezed == nativeAppUriDesktop
|
||||
? _value.nativeAppUriDesktop
|
||||
: nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SongLinkImpl implements _SongLink {
|
||||
const _$SongLinkImpl(
|
||||
{required this.displayName,
|
||||
required this.linkId,
|
||||
required this.platform,
|
||||
required this.show,
|
||||
required this.uniqueId,
|
||||
required this.country,
|
||||
required this.url,
|
||||
required this.nativeAppUriMobile,
|
||||
required this.nativeAppUriDesktop});
|
||||
|
||||
factory _$SongLinkImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SongLinkImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String displayName;
|
||||
@override
|
||||
final String linkId;
|
||||
@override
|
||||
final String platform;
|
||||
@override
|
||||
final bool show;
|
||||
@override
|
||||
final String? uniqueId;
|
||||
@override
|
||||
final String? country;
|
||||
@override
|
||||
final String? url;
|
||||
@override
|
||||
final String? nativeAppUriMobile;
|
||||
@override
|
||||
final String? nativeAppUriDesktop;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SongLink(displayName: $displayName, linkId: $linkId, platform: $platform, show: $show, uniqueId: $uniqueId, country: $country, url: $url, nativeAppUriMobile: $nativeAppUriMobile, nativeAppUriDesktop: $nativeAppUriDesktop)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SongLinkImpl &&
|
||||
(identical(other.displayName, displayName) ||
|
||||
other.displayName == displayName) &&
|
||||
(identical(other.linkId, linkId) || other.linkId == linkId) &&
|
||||
(identical(other.platform, platform) ||
|
||||
other.platform == platform) &&
|
||||
(identical(other.show, show) || other.show == show) &&
|
||||
(identical(other.uniqueId, uniqueId) ||
|
||||
other.uniqueId == uniqueId) &&
|
||||
(identical(other.country, country) || other.country == country) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.nativeAppUriMobile, nativeAppUriMobile) ||
|
||||
other.nativeAppUriMobile == nativeAppUriMobile) &&
|
||||
(identical(other.nativeAppUriDesktop, nativeAppUriDesktop) ||
|
||||
other.nativeAppUriDesktop == nativeAppUriDesktop));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
|
||||
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||
__$$SongLinkImplCopyWithImpl<_$SongLinkImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SongLinkImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SongLink implements SongLink {
|
||||
const factory _SongLink(
|
||||
{required final String displayName,
|
||||
required final String linkId,
|
||||
required final String platform,
|
||||
required final bool show,
|
||||
required final String? uniqueId,
|
||||
required final String? country,
|
||||
required final String? url,
|
||||
required final String? nativeAppUriMobile,
|
||||
required final String? nativeAppUriDesktop}) = _$SongLinkImpl;
|
||||
|
||||
factory _SongLink.fromJson(Map<String, dynamic> json) =
|
||||
_$SongLinkImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get displayName;
|
||||
@override
|
||||
String get linkId;
|
||||
@override
|
||||
String get platform;
|
||||
@override
|
||||
bool get show;
|
||||
@override
|
||||
String? get uniqueId;
|
||||
@override
|
||||
String? get country;
|
||||
@override
|
||||
String? get url;
|
||||
@override
|
||||
String? get nativeAppUriMobile;
|
||||
@override
|
||||
String? get nativeAppUriDesktop;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
33
lib/services/song_link/song_link.g.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'song_link.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SongLinkImpl _$$SongLinkImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SongLinkImpl(
|
||||
displayName: json['displayName'] as String,
|
||||
linkId: json['linkId'] as String,
|
||||
platform: json['platform'] as String,
|
||||
show: json['show'] as bool,
|
||||
uniqueId: json['uniqueId'] as String?,
|
||||
country: json['country'] as String?,
|
||||
url: json['url'] as String?,
|
||||
nativeAppUriMobile: json['nativeAppUriMobile'] as String?,
|
||||
nativeAppUriDesktop: json['nativeAppUriDesktop'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SongLinkImplToJson(_$SongLinkImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'displayName': instance.displayName,
|
||||
'linkId': instance.linkId,
|
||||
'platform': instance.platform,
|
||||
'show': instance.show,
|
||||
'uniqueId': instance.uniqueId,
|
||||
'country': instance.country,
|
||||
'url': instance.url,
|
||||
'nativeAppUriMobile': instance.nativeAppUriMobile,
|
||||
'nativeAppUriDesktop': instance.nativeAppUriDesktop,
|
||||
};
|
||||
@ -1,7 +1,12 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
class TrackNotFoundException implements Exception {
|
||||
factory TrackNotFoundException(Track track) {
|
||||
throw Exception("Failed to find any results for ${track.name}");
|
||||
class TrackNotFoundError extends Error {
|
||||
final Track track;
|
||||
|
||||
TrackNotFoundError(this.track);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '[TrackNotFoundError] ${track.name} - ${track.artists?.map((e) => e.name).join(", ")}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
abstract class SourcedTrack extends Track {
|
||||
final SourceMap source;
|
||||
@ -101,9 +107,8 @@ abstract class SourcedTrack extends Track {
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
try {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
@ -112,8 +117,35 @@ abstract class SourcedTrack extends Track {
|
||||
AudioSource.jiosaavn =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on TrackNotFoundError catch (_) {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.youtube =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
),
|
||||
AudioSource.jiosaavn =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on HttpClientClosedException catch (_) {
|
||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
} catch (e) {
|
||||
return YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
if (e is DioException || e is ClientException || e is SocketException) {
|
||||
if (preferences.audioSource == AudioSource.jiosaavn) {
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
);
|
||||
}
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,15 +37,17 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
static Future<SourcedTrack> fetchFromTrack({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
bool weakMatch = false,
|
||||
}) async {
|
||||
final cachedSource = await SourceMatch.box.get(track.id);
|
||||
|
||||
if (cachedSource == null ||
|
||||
cachedSource.sourceType != SourceType.jiosaavn) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
final siblings =
|
||||
await fetchSiblings(ref: ref, track: track, weakMatch: weakMatch);
|
||||
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -119,6 +121,7 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
static Future<List<SiblingType>> fetchSiblings({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
bool weakMatch = false,
|
||||
}) async {
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
@ -126,9 +129,12 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
await jiosaavnClient.search.songs(query, limit: 20);
|
||||
|
||||
final trackArtistNames = track.artists?.map((ar) => ar.name).toList();
|
||||
return results
|
||||
|
||||
final matchedResults = results
|
||||
.where(
|
||||
(s) {
|
||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
||||
|
||||
final sameName = s.name?.unescapeHtml() == track.name;
|
||||
final artistNames = [
|
||||
s.primaryArtists,
|
||||
@ -139,12 +145,27 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
(artist) =>
|
||||
trackArtistNames?.any((ar) => artist == ar) ?? false,
|
||||
);
|
||||
if (weakMatch) {
|
||||
final containsName =
|
||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
||||
final containsPrimaryArtist = s.primaryArtists
|
||||
.unescapeHtml()
|
||||
.contains(trackArtistNames?.first ?? "");
|
||||
|
||||
return containsName && containsPrimaryArtist;
|
||||
}
|
||||
|
||||
return sameName && sameArtists;
|
||||
},
|
||||
)
|
||||
.map(toSiblingType)
|
||||
.toList();
|
||||
|
||||
if (weakMatch && matchedResults.isEmpty) {
|
||||
return results.map(toSiblingType).toList();
|
||||
}
|
||||
|
||||
return matchedResults;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -55,7 +55,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
if (cachedSource == null) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -157,16 +157,20 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
}) async {
|
||||
final pipedClient = ref.read(pipedProvider);
|
||||
final preference = ref.read(userPreferencesProvider);
|
||||
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
||||
"$query - Topic",
|
||||
query,
|
||||
preference.searchMode == SearchMode.youtube
|
||||
? PipedFilter.video
|
||||
? PipedFilter.videos
|
||||
: PipedFilter.musicSongs,
|
||||
);
|
||||
|
||||
final isYouTubeMusic = preference.searchMode == SearchMode.youtubeMusic;
|
||||
// when falling back to piped API make sure to use the YouTube mode
|
||||
final isYouTubeMusic = preference.audioSource != AudioSource.piped
|
||||
? false
|
||||
: preference.searchMode == SearchMode.youtubeMusic;
|
||||
|
||||
if (isYouTubeMusic) {
|
||||
final artists = (track.artists ?? [])
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/source_match.dart';
|
||||
import 'package:spotube/services/song_link/song_link.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
@ -48,7 +50,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -70,9 +72,14 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
);
|
||||
}
|
||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||
final manifest = await youtubeClient.videos.streamsClient.getManifest(
|
||||
cachedSource.sourceId,
|
||||
);
|
||||
final manifest = await youtubeClient.videos.streamsClient
|
||||
.getManifest(
|
||||
cachedSource.sourceId,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
return YoutubeSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: [],
|
||||
@ -125,7 +132,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
SourceMap? sourceMap;
|
||||
if (index == 0) {
|
||||
final manifest =
|
||||
await youtubeClient.videos.streamsClient.getManifest(item.id);
|
||||
await youtubeClient.videos.streamsClient.getManifest(item.id).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
sourceMap = toSourceMap(manifest);
|
||||
}
|
||||
|
||||
@ -207,6 +217,20 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final links = await SongLinkService.links(track.id!);
|
||||
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
||||
|
||||
if (ytLink?.url != null) {
|
||||
return [
|
||||
await toSiblingType(
|
||||
0,
|
||||
YoutubeVideoInfo.fromVideo(
|
||||
await youtubeClient.videos.get(ytLink!.url!),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
final searchResults = await youtubeClient.search.search(
|
||||
@ -243,8 +267,12 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||
..insert(0, sourceInfo);
|
||||
|
||||
final manifest =
|
||||
await youtubeClient.videos.streamsClient.getManifest(newSourceInfo.id);
|
||||
final manifest = await youtubeClient.videos.streamsClient
|
||||
.getManifest(newSourceInfo.id)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
|
||||
await SourceMatch.box.put(
|
||||
id!,
|
||||
|
||||
@ -119,7 +119,9 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||
Future<void> _load() async {
|
||||
final json = await box.get(cacheKey);
|
||||
|
||||
if (json != null) {
|
||||
if (json != null ||
|
||||
(json is Map && json.entries.isNotEmpty) ||
|
||||
(json is List && json.isNotEmpty)) {
|
||||
state = await fromJson(castNestedJson(json));
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,9 +53,9 @@ abstract class ServiceUtils {
|
||||
|
||||
return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}"
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(" *\\[[^\\]]*]"), '')
|
||||
.replaceAll(RegExp("feat.|ft."), '')
|
||||
.replaceAll(RegExp("\\s+"), ' ')
|
||||
.replaceAll(RegExp(r"\s*\[[^\]]*]"), ' ')
|
||||
.replaceAll(RegExp(r"\sfeat\.|\sft\."), ' ')
|
||||
.replaceAll(RegExp(r"\s+"), ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
@ -292,24 +292,24 @@ abstract class ServiceUtils {
|
||||
return List<T>.from(tracks)
|
||||
..sort((a, b) {
|
||||
switch (sortBy) {
|
||||
case SortBy.album:
|
||||
return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0;
|
||||
case SortBy.artist:
|
||||
return a.artists?.first.name
|
||||
?.compareTo(b.artists?.first.name ?? "") ??
|
||||
0;
|
||||
case SortBy.ascending:
|
||||
return a.name?.compareTo(b.name ?? "") ?? 0;
|
||||
case SortBy.oldest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return aDate.compareTo(bDate);
|
||||
case SortBy.descending:
|
||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||
case SortBy.newest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return bDate.compareTo(aDate);
|
||||
case SortBy.descending:
|
||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||
case SortBy.oldest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return aDate.compareTo(bDate);
|
||||
case SortBy.duration:
|
||||
return a.durationMs?.compareTo(b.durationMs ?? 0) ?? 0;
|
||||
case SortBy.artist:
|
||||
return a.artists?.first.name?.compareTo(b.artists?.first.name ?? "") ?? 0;
|
||||
case SortBy.album:
|
||||
return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -143,4 +143,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
41
pubspec.lock
@ -478,10 +478,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
||||
sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.3"
|
||||
version: "5.4.1"
|
||||
disable_battery_optimization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -863,10 +863,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: e667e406a74d67715f1fa0bd941d9ded49aff72f3a9f4440a36aece4e8d457a7
|
||||
sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.10"
|
||||
flutter_rust_bridge:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -965,8 +965,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.6"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
|
||||
@ -1014,10 +1022,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "3b40e751eaaa855179b416974d59d29669e750d2e50fcdb2b37f1cb0ca8c803a"
|
||||
sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.1"
|
||||
version: "12.1.3"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1078,10 +1086,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hooks_riverpod
|
||||
sha256: "69dcb88acbc68c81fc27ec15a89a4e24b7812c83c13a6307a1a9366ada758541"
|
||||
sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.10"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1594,11 +1602,12 @@ packages:
|
||||
piped_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: piped_client
|
||||
sha256: "8b96e1f9d8533c1da7eff7fbbd4bf188256fc76a20900d378b52be09418ea771"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "64631732eefe3d93889756dc2e4ff5c8523ed763"
|
||||
url: "https://github.com/KRTirtho/piped_client.git"
|
||||
source: git
|
||||
version: "0.1.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1707,10 +1716,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: "494bf2cfb4df30000273d3052bdb1cc1de738574c6b678f0beb146ea56f5e208"
|
||||
sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.5.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
16
pubspec.yaml
@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El
|
||||
|
||||
publish_to: "none"
|
||||
|
||||
version: 3.5.0+28
|
||||
version: 3.4.1+28
|
||||
|
||||
homepage: https://spotube.krtirtho.dev
|
||||
repository: https://github.com/KRTirtho/spotube
|
||||
@ -27,7 +27,7 @@ dependencies:
|
||||
dbus: ^0.7.8
|
||||
device_info_plus: ^9.0.3
|
||||
device_preview: ^1.1.0
|
||||
dio: ^5.3.2
|
||||
dio: ^5.4.1
|
||||
disable_battery_optimization: ^1.1.0+1
|
||||
duration: ^3.0.12
|
||||
envied: ^0.3.0
|
||||
@ -50,12 +50,12 @@ dependencies:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
flutter_native_splash: ^2.3.10
|
||||
flutter_riverpod: ^2.4.3
|
||||
flutter_riverpod: ^2.4.10
|
||||
flutter_secure_storage: ^9.0.0
|
||||
flutter_svg: ^1.1.6
|
||||
form_validator: ^2.1.1
|
||||
fuzzywuzzy: ^1.1.6
|
||||
go_router: ^13.0.1
|
||||
go_router: 12.1.3 # Stuck on this https://github.com/flutter/flutter/issues/140869
|
||||
google_fonts: ^6.1.0
|
||||
hive: ^2.2.3
|
||||
hive_flutter: ^1.1.0
|
||||
@ -76,7 +76,9 @@ dependencies:
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.8
|
||||
permission_handler: ^11.0.1
|
||||
piped_client: ^0.1.0
|
||||
piped_client:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/piped_client.git
|
||||
popover: ^0.2.6+3
|
||||
scrobblenaut:
|
||||
git:
|
||||
@ -122,6 +124,8 @@ dependencies:
|
||||
app_links: ^3.5.0
|
||||
win32_registry: ^1.1.2
|
||||
flutter_sharing_intent: ^1.1.0
|
||||
flutter_broadcasts: ^0.4.0
|
||||
freezed_annotation: ^2.4.1
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
@ -138,6 +142,7 @@ dev_dependencies:
|
||||
json_serializable: ^6.6.2
|
||||
pub_api_client: ^2.4.0
|
||||
pubspec_parse: ^1.2.2
|
||||
freezed: ^2.4.6
|
||||
|
||||
dependency_overrides:
|
||||
http: ^1.1.0
|
||||
@ -149,6 +154,7 @@ flutter:
|
||||
assets:
|
||||
- assets/
|
||||
- assets/tutorial/
|
||||
- assets/logos/
|
||||
- LICENSE
|
||||
|
||||
flutter_launcher_icons:
|
||||
|
||||
@ -1 +1,542 @@
|
||||
{}
|
||||
{
|
||||
"ar": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"sort_duration",
|
||||
"audio_source",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
]
|
||||
}
|
||||
|
||||
@ -29,19 +29,87 @@
|
||||
|
||||
<title>spotube</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="stylesheet" type="text/css" href="splash/style.css">
|
||||
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<script src="splash/splash.js"></script>
|
||||
|
||||
<style id="splash-screen-style">
|
||||
html {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background-color: #ffffff;
|
||||
background-image: url("splash/img/light-background.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.contain {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stretch {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
}
|
||||
|
||||
.cover {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, 0);
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.bottomLeft {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.bottomRight {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
<script id="splash-screen-script">
|
||||
function removeSplashFromWeb() {
|
||||
document.getElementById("splash")?.remove();
|
||||
document.getElementById("splash-branding")?.remove();
|
||||
document.body.style.background = "transparent";
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body> <picture id="splash-branding">
|
||||
<body>
|
||||
<picture id="splash-branding">
|
||||
<source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x" media="(prefers-color-scheme: light)">
|
||||
<source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
||||
<img class="bottom" aria-hidden="true" src="splash/img/branding-1x.png" alt="">
|
||||
</picture> <picture id="splash">
|
||||
</picture>
|
||||
<picture id="splash">
|
||||
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
|
||||
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
||||
</picture>
|
||||
</picture>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |