Merge branch 'dev' into dependabot/pub/dev/flutter_native_splash-2.3.10

This commit is contained in:
Kingkor Roy Tirtho 2024-02-25 22:51:24 +06:00 committed by GitHub
commit 0ef8bd847e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
198 changed files with 11704 additions and 7783 deletions

View File

@ -1,4 +1,4 @@
{
"flutterSdkVersion": "3.16.0",
"flutterSdkVersion": "3.19.1",
"flavors": {}
}

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -31,4 +31,6 @@ linter:
analyzer:
enable-experiment:
- records
- patterns
- patterns
errors:
invalid_annotation_target: ignore

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/logos/songlink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>

View File

@ -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'

View File

@ -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

View File

@ -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 = (

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 =

View File

@ -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("/");

View File

@ -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

View File

@ -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()),
),
],
);
});

View File

@ -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;
}

View 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,
),
),
),
);
}
}

View File

@ -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),
),
],
),
],
),
),
),
),

View File

@ -50,10 +50,11 @@ enum SortBy {
none,
ascending,
descending,
artist,
album,
newest,
oldest,
duration,
artist,
album,
}
final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {

View File

@ -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,

View File

@ -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(

View File

@ -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,

View File

@ -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),

View File

@ -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();
}
},

View File

@ -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(),
),
],
),
),
),
);

View File

@ -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,

View File

@ -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) {

View 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,
],
);
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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 {

View File

@ -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);

View 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(),
],
),
),
);
}
}

View 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),
),
),
],
),
),
);
}
}

View 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,
),
),
],
),
],
),
),
);
}
}

View 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,
),
),
),
],
),
),
),
);
}
}

View 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");
},
),
],
),
),
],
),
);
}
}

View File

@ -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';

View File

@ -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,

View File

@ -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(),
);

View File

@ -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,
),
],
),
),
);
}

View File

@ -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,
);
}
}

View File

@ -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,
),
],
);
}

View File

@ -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!);

View File

@ -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";

View File

@ -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,
);
}
}

View File

@ -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;
}

View File

@ -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 = {

View File

@ -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();
}

View 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);
}

View File

@ -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;
}
}

View 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);
}

View 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>[];
}
}
}

View 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;
}

View 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,
};

View File

@ -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(", ")}';
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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 ?? [])

View File

@ -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!,

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -143,4 +143,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

View File

@ -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:

View File

@ -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:

View File

@ -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"
]
}

View File

@ -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 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

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