Merge branch 'dev' into website
@ -1,4 +1,4 @@
|
||||
{
|
||||
"flutterSdkVersion": "3.16.0",
|
||||
"flutterSdkVersion": "3.19.1",
|
||||
"flavors": {}
|
||||
}
|
18
.github/workflows/spotube-release-binary.yml
vendored
@ -26,7 +26,7 @@ on:
|
||||
default: true
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: '3.16.3'
|
||||
FLUTTER_VERSION: '3.19.1'
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
@ -181,6 +181,7 @@ jobs:
|
||||
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ inputs.channel == 'release' }}
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: Spotube-Release-Binaries
|
||||
@ -189,6 +190,16 @@ jobs:
|
||||
dist/Spotube-linux-x86_64.rpm
|
||||
dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ inputs.channel == 'nightly' }}
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: Spotube-Release-Binaries
|
||||
path: |
|
||||
dist/Spotube-linux-x86_64.deb
|
||||
dist/Spotube-linux-x86_64.rpm
|
||||
dist/spotube-linux-nightly-x86_64.tar.xz
|
||||
|
||||
- name: Debug With SSH When fails
|
||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
@ -424,6 +435,11 @@ jobs:
|
||||
RELEASE.md5sum
|
||||
RELEASE.sha256sum
|
||||
|
||||
- name: Debug With SSH
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
|
||||
- name: Upload Release Binaries (stable)
|
||||
if: ${{ !inputs.dry_run && inputs.channel == 'stable' }}
|
||||
uses: ncipollo/release-action@v1
|
||||
|
1
Makefile
@ -28,6 +28,7 @@ publishaur:
|
||||
|
||||
innoinstall:
|
||||
powershell curl -o build\installer.exe http://files.jrsoftware.org/is/6/innosetup-${INNO_VERSION}.exe
|
||||
powershell git clone https://github.com/DomGries/InnoDependencyInstaller.git build\inno-depend
|
||||
powershell build\installer.exe /verysilent /allusers /dir=build\iscc
|
||||
|
||||
inno:
|
||||
|
11
README.md
@ -13,6 +13,8 @@ Btw it's not just another Electron app 😉
|
||||
<a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a>
|
||||
<a href="https://www.buymeacoffee.com/krtirtho"><img alt="Buy me a Coffee" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/buymeacoffee-singular_vector.svg"></a>
|
||||
|
||||
[](https://news.ycombinator.com/item?id=39066136)
|
||||
|
||||
<a href="https://opencollective.com/spotube"><img src="https://opencollective.com/spotube/donate/button.png?color=blue" alt="Donate to our Open Collective" height="45"></a>
|
||||
|
||||
---
|
||||
@ -136,6 +138,15 @@ This handy table lists all the methods you can use to install Spotube:
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Macos - <a href="https://brew.sh">Homebrew</a></td>
|
||||
<td>
|
||||
<pre lang="bash">
|
||||
brew tap krtirtho/apps
|
||||
brew install --cask spotube
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows - <a href="https://chocolatey.org">Chocolatey</a></td>
|
||||
<td>
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
BIN
assets/logos/songlink-transparent.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
assets/logos/songlink.png
Normal file
After Width: | Height: | Size: 86 KiB |
@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
@ -196,7 +196,7 @@ SPEC CHECKSUMS:
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
||||
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
@ -221,6 +221,6 @@ SPEC CHECKSUMS:
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
|
||||
PODFILE CHECKSUM: e36c7ad9836dfd8d22934c7680185432a658e28f
|
||||
PODFILE CHECKSUM: 5129d2e80ab0dfc533f262cedf032011b1dfe4fd
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
@ -406,7 +406,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@ -2,6 +2,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>
|
||||
@ -24,10 +26,31 @@
|
||||
<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>
|
||||
@ -43,24 +66,5 @@
|
||||
</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>
|
@ -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>
|
||||
|
@ -2,6 +2,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>
|
||||
@ -24,10 +26,31 @@
|
||||
<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>
|
||||
@ -43,24 +66,5 @@
|
||||
</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>
|
||||
|
@ -2,6 +2,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>
|
||||
@ -24,10 +26,31 @@
|
||||
<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>
|
||||
@ -43,24 +66,5 @@
|
||||
</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>
|
||||
|
@ -9,6 +9,21 @@
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class $AssetsLogosGen {
|
||||
const $AssetsLogosGen();
|
||||
|
||||
/// File path: assets/logos/songlink-transparent.png
|
||||
AssetGenImage get songlinkTransparent =>
|
||||
const AssetGenImage('assets/logos/songlink-transparent.png');
|
||||
|
||||
/// File path: assets/logos/songlink.png
|
||||
AssetGenImage get songlink =>
|
||||
const AssetGenImage('assets/logos/songlink.png');
|
||||
|
||||
/// List of all assets
|
||||
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
||||
}
|
||||
|
||||
class $AssetsTutorialGen {
|
||||
const $AssetsTutorialGen();
|
||||
|
||||
@ -37,6 +52,7 @@ class Assets {
|
||||
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
|
||||
static const AssetGenImage likedTracks =
|
||||
AssetGenImage('assets/liked-tracks.jpg');
|
||||
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
||||
static const AssetGenImage placeholder =
|
||||
AssetGenImage('assets/placeholder.png');
|
||||
static const AssetGenImage spotubeHeroBanner =
|
||||
|
@ -4,8 +4,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:spotube/components/player/player_controls.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/components/player/player_controls.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
@ -64,6 +64,7 @@ class HomeTabIntent extends Intent {
|
||||
class HomeTabAction extends Action<HomeTabIntent> {
|
||||
@override
|
||||
invoke(intent) {
|
||||
final router = intent.ref.read(routerProvider);
|
||||
switch (intent.tab) {
|
||||
case HomeTabs.browse:
|
||||
router.go("/");
|
||||
|
@ -6,6 +6,11 @@ class ISOLanguageName {
|
||||
required this.name,
|
||||
required this.nativeName,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$name ($nativeName)";
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment the languages as we add support for them
|
||||
@ -348,10 +353,10 @@ abstract class LanguageLocals {
|
||||
// name: "Kongo",
|
||||
// nativeName: "KiKongo",
|
||||
// ),
|
||||
// "ko": const ISOLanguageName(
|
||||
// name: "Korean",
|
||||
// nativeName: "한국어 (韓國語), 조선말 (朝鮮語)",
|
||||
// ),
|
||||
"ko": const ISOLanguageName(
|
||||
name: "Korean",
|
||||
nativeName: "한국어 (韓國語), 조선말 (朝鮮語)",
|
||||
),
|
||||
// "ku": const ISOLanguageName(
|
||||
// name: "Kurdish",
|
||||
// nativeName: "Kurdî, كوردی",
|
||||
@ -700,10 +705,10 @@ abstract class LanguageLocals {
|
||||
// name: "Venda",
|
||||
// nativeName: "Tshivenḓa",
|
||||
// ),
|
||||
// "vi": const ISOLanguageName(
|
||||
// name: "Vietnamese",
|
||||
// nativeName: "Tiếng Việt",
|
||||
// ),
|
||||
"vi": const ISOLanguageName(
|
||||
name: "Vietnamese",
|
||||
nativeName: "Tiếng Việt",
|
||||
),
|
||||
// "vo": const ISOLanguageName(
|
||||
// name: "Volapük",
|
||||
// nativeName: "Volapük",
|
||||
|
@ -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,7 +35,8 @@ import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
||||
|
||||
final rootNavigatorKey = Catcher2.navigatorKey;
|
||||
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final router = GoRouter(
|
||||
final routerProvider = Provider((ref) {
|
||||
return GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: [
|
||||
ShellRoute(
|
||||
@ -40,7 +45,20 @@ final router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
pageBuilder: (context, state) => const SpotubePage(child: HomePage()),
|
||||
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: HomePage()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "genres",
|
||||
@ -131,7 +149,8 @@ final router = GoRouter(
|
||||
path: "/artist/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.pathParameters["id"] != null);
|
||||
return SpotubePage(child: ArtistPage(state.pathParameters["id"]!));
|
||||
return SpotubePage(
|
||||
child: ArtistPage(state.pathParameters["id"]!));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@ -163,6 +182,13 @@ final router = GoRouter(
|
||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/getting-started",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: GettingStarting(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
@ -185,3 +211,4 @@ final router = GoRouter(
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
@ -112,4 +112,7 @@ abstract class SpotubeIcons {
|
||||
static const discord = SimpleIcons.discord;
|
||||
static const youtube = SimpleIcons.youtube;
|
||||
static const radio = FeatherIcons.radio;
|
||||
static const github = SimpleIcons.github;
|
||||
static const openCollective = SimpleIcons.opencollective;
|
||||
static const anonymous = FeatherIcons.user;
|
||||
}
|
||||
|
31
lib/components/getting_started/blur_card.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class BlurCard extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
const BlurCard({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
@ -69,6 +72,10 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: PointerDeviceKind.values.toSet(),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Column(
|
||||
@ -88,6 +95,7 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -50,10 +50,11 @@ enum SortBy {
|
||||
none,
|
||||
ascending,
|
||||
descending,
|
||||
artist,
|
||||
album,
|
||||
newest,
|
||||
oldest,
|
||||
duration,
|
||||
artist,
|
||||
album,
|
||||
}
|
||||
|
||||
final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
||||
|
@ -25,6 +25,7 @@ import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PlayerView extends HookConsumerWidget {
|
||||
final PanelController panelController;
|
||||
@ -94,10 +95,10 @@ class PlayerView extends HookConsumerWidget {
|
||||
|
||||
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) async {
|
||||
panelController.close();
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await panelController.close();
|
||||
return false;
|
||||
},
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme.copyWith(color: bodyTextColor),
|
||||
@ -137,11 +138,32 @@ class PlayerView extends HookConsumerWidget {
|
||||
onPressed: panelController.close,
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
icon: Assets.logos.songlinkTransparent.image(
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: bodyTextColor,
|
||||
),
|
||||
label: Text(context.l10n.song_link),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: bodyTextColor,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
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,
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: bodyTextColor),
|
||||
foregroundColor: bodyTextColor,
|
||||
),
|
||||
onPressed: currentTrack == null
|
||||
? null
|
||||
: () {
|
||||
|
@ -15,8 +15,6 @@ class InterScrollbar extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (DesktopTools.platform.isDesktop) return child;
|
||||
|
||||
return DraggableScrollbar.semicircle(
|
||||
|
@ -48,6 +48,11 @@ class SortTracksDropdown extends StatelessWidget {
|
||||
enabled: value != SortBy.oldest,
|
||||
title: Text(context.l10n.sort_oldest),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: SortBy.duration,
|
||||
enabled: value != SortBy.duration,
|
||||
title: Text(context.l10n.sort_duration),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: SortBy.artist,
|
||||
enabled: value != SortBy.artist,
|
||||
|
@ -7,6 +7,7 @@ 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';
|
||||
@ -26,10 +27,12 @@ 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,
|
||||
@ -165,6 +168,7 @@ class TrackOptions extends HookConsumerWidget {
|
||||
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);
|
||||
@ -276,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,
|
||||
@ -418,6 +426,15 @@ class TrackOptions extends HookConsumerWidget {
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.songlink,
|
||||
leading: Assets.logos.songlinkTransparent.image(
|
||||
width: 22,
|
||||
height: 22,
|
||||
color: colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
title: Text(context.l10n.song_link),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.details,
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
|
@ -70,9 +70,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
||||
tooltip: props.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
onPressed: () {
|
||||
props.onHeart?.call();
|
||||
if (isUserPlaylist) {
|
||||
onPressed: () async {
|
||||
final shouldPop = await props.onHeart?.call();
|
||||
if (isUserPlaylist && shouldPop == true && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart';
|
||||
@ -13,6 +15,7 @@ class TrackView extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final props = InheritedTrackView.of(context);
|
||||
final controller = useScrollController();
|
||||
|
||||
return Scaffold(
|
||||
appBar: DesktopTools.platform.isDesktop
|
||||
@ -29,8 +32,11 @@ class TrackView extends HookConsumerWidget {
|
||||
extendBodyBehindAppBar: true,
|
||||
body: RefreshIndicator(
|
||||
onRefresh: props.pagination.onRefresh,
|
||||
child: const CustomScrollView(
|
||||
slivers: [
|
||||
child: InterScrollbar(
|
||||
controller: controller,
|
||||
child: CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: const [
|
||||
TrackViewFlexHeader(),
|
||||
SliverAnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 500),
|
||||
@ -39,6 +45,7 @@ class TrackView extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -62,7 +64,7 @@ class InheritedTrackView extends InheritedWidget {
|
||||
final String shareUrl;
|
||||
|
||||
// events
|
||||
final VoidCallback? onHeart; // if null heart button will hidden
|
||||
final FutureOr<bool?> Function()? onHeart; // if null heart button will hidden
|
||||
|
||||
const InheritedTrackView({
|
||||
super.key,
|
||||
|
@ -19,6 +19,8 @@ void useDeepLinking(WidgetRef ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
useEffect(() {
|
||||
void uriListener(List<SharedFile> files) async {
|
||||
for (final file in files) {
|
||||
|
@ -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",
|
||||
@ -290,5 +291,27 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
291
lib/l10n/app_ko.arb
Normal file
@ -0,0 +1,291 @@
|
||||
{
|
||||
"guest": "게스트",
|
||||
"browse": "찾아보기",
|
||||
"search": "검색",
|
||||
"library": "라이브러리",
|
||||
"lyrics": "가사",
|
||||
"settings": "설정",
|
||||
"genre_categories_filter": "카테고리 혹은 장르별로 불러오기",
|
||||
"genre": "장르",
|
||||
"personalized": "맞춤 추천",
|
||||
"featured": "인기",
|
||||
"new_releases": "신곡",
|
||||
"songs": "노래",
|
||||
"playing_track": "{track} 을 재생",
|
||||
"queue_clear_alert": "현재 재생 대기열을 없앱니다。{track_length} 곡이 제거됩니다。\n계속 진행할까요?",
|
||||
"load_more": "더 불러오기",
|
||||
"playlists": "플레이리스트",
|
||||
"artists": "아티스트",
|
||||
"albums": "앨범",
|
||||
"tracks": "곡",
|
||||
"downloads": "다운로드한 곡",
|
||||
"filter_playlists": "플레이리스트를 필터링",
|
||||
"liked_tracks": "좋아하는 곡",
|
||||
"liked_tracks_description": "좋아요를 남긴 곡들",
|
||||
"create_playlist": "플레이리스트 생성",
|
||||
"create_a_playlist": "플레이리스트를 생성",
|
||||
"create": "생성",
|
||||
"cancel": "취소",
|
||||
"playlist_name": "플레이리스트명",
|
||||
"name_of_playlist": "플레이리스트의 이름",
|
||||
"description": "설명",
|
||||
"public": "공개",
|
||||
"collaborative": "공유 플레이리스트",
|
||||
"search_local_tracks": "기기에 저장된 곡을 검색하기",
|
||||
"play": "재생",
|
||||
"delete": "삭제",
|
||||
"none": "없음",
|
||||
"sort_a_z": "A-Z 순으로 정렬",
|
||||
"sort_z_a": "Z-A 순으로 정렬",
|
||||
"sort_artist": "아티스트 순으로 정렬",
|
||||
"sort_album": "앨범 순으로 정렬",
|
||||
"sort_tracks": "곡명 순으로 정렬",
|
||||
"currently_downloading": "현재 ({tracks_length}) 곡 다운로드 중",
|
||||
"cancel_all": "모두 취소",
|
||||
"filter_artist": "아티스트 필터링",
|
||||
"followers": "{followers} 팔로워",
|
||||
"add_artist_to_blacklist": "이 아티스트를 블랙리스트에 추가",
|
||||
"top_tracks": "인기곡",
|
||||
"fans_also_like": "애청자들이 좋아하는 곡",
|
||||
"loading": "불러오는 중...",
|
||||
"artist": "아티스트",
|
||||
"blacklisted": "블랙리스트",
|
||||
"following": "팔로우 중",
|
||||
"follow": "팔로우하기",
|
||||
"artist_url_copied": "아티스트의 URL 주소를 클립보드에 복사함",
|
||||
"added_to_queue": "{tracks} 곡을 대기열에 추가함",
|
||||
"filter_albums": "앨범 필터링",
|
||||
"synced": "동기화됨",
|
||||
"plain": "그대로",
|
||||
"shuffle": "셔플",
|
||||
"search_tracks": "곡 검색하기",
|
||||
"released": "공개일",
|
||||
"error": "에러",
|
||||
"title": "타이틀",
|
||||
"time": "길이",
|
||||
"more_actions": "다른 작업",
|
||||
"download_count": "({count}) 곡 다운로드",
|
||||
"add_count_to_playlist": "플레이리스트에 ({count}) 곡을 추가",
|
||||
"add_count_to_queue": "대기열에 ({count}) 곡을 추가",
|
||||
"play_count_next": "이 다음에 ({count}) 곡을 재생",
|
||||
"album": "앨범",
|
||||
"copied_to_clipboard": "{data} 를 클립보드에 복사함",
|
||||
"add_to_following_playlists": "{track} 을 이 플레이리스트에 추가",
|
||||
"add": "추가",
|
||||
"added_track_to_queue": "대기열에 {track} 을 추가함",
|
||||
"add_to_queue": "대기열에 추가",
|
||||
"track_will_play_next": "{track} 을 이 다음에 재생",
|
||||
"play_next": "이 다음에 재생",
|
||||
"removed_track_from_queue": "대기열에서 {track} 를 제거함",
|
||||
"remove_from_queue": "대기열에서 제거",
|
||||
"remove_from_favorites": "즐겨찾기에서 제거",
|
||||
"save_as_favorite": "즐겨찾기에 추가",
|
||||
"add_to_playlist": "플레이리스트에 추가",
|
||||
"remove_from_playlist": "플레이리스트에서 제거",
|
||||
"add_to_blacklist": "블랙리스트에 추가",
|
||||
"remove_from_blacklist": "블랙리스트에서 제거",
|
||||
"share": "공유",
|
||||
"mini_player": "미니 플레이어",
|
||||
"slide_to_seek": "앞뒤로 슬라이드하여 탐색",
|
||||
"shuffle_playlist": "플레이리스트를 섞기",
|
||||
"unshuffle_playlist": "플레이리스트를 섞지 않기",
|
||||
"previous_track": "이전 곡",
|
||||
"next_track": "다음 곡",
|
||||
"pause_playback": "일시정지",
|
||||
"resume_playback": "재개",
|
||||
"loop_track": "반복 재생",
|
||||
"repeat_playlist": "플레이리스트 반복",
|
||||
"queue": "재생 대기열",
|
||||
"alternative_track_sources": "대체가능한 음악 서버",
|
||||
"download_track": "곡 다운로드",
|
||||
"tracks_in_queue": "대기열에 {tracks} 곡이 있음",
|
||||
"clear_all": "모두 제거",
|
||||
"show_hide_ui_on_hover": "마우스를 올리면 UI를 표시/숨김",
|
||||
"always_on_top": "항상 위에 표시",
|
||||
"exit_mini_player": "미니 플레이어 닫기",
|
||||
"download_location": "다운로드 경로",
|
||||
"account": "계정",
|
||||
"login_with_spotify": "Spotify 계정으로 로그인",
|
||||
"connect_with_spotify": "Spotify에 연결",
|
||||
"logout": "로그아웃",
|
||||
"logout_of_this_account": "이 계정에서 로그아웃",
|
||||
"language_region": "언어 & 지역",
|
||||
"language": "언어",
|
||||
"system_default": "시스템 기본설정",
|
||||
"market_place_region": "마켓플레이스 지역",
|
||||
"recommendation_country": "추천 국가",
|
||||
"appearance": "디자인",
|
||||
"layout_mode": "레이아웃 모드",
|
||||
"override_layout_settings": "반응형 레이아웃 모드 설정 덮어씌우기",
|
||||
"adaptive": "적응형",
|
||||
"compact": "컴팩트",
|
||||
"extended": "확장",
|
||||
"theme": "테마",
|
||||
"dark": "다크",
|
||||
"light": "라이트",
|
||||
"system": "시스템과 동일",
|
||||
"accent_color": "보조색",
|
||||
"sync_album_color": "앨범 색상",
|
||||
"sync_album_color_description": "앨범아트의 주요 색상을 보조색으로 사용",
|
||||
"playback": "재생",
|
||||
"audio_quality": "음질",
|
||||
"high": "높음",
|
||||
"low": "낮음",
|
||||
"pre_download_play": "재생할 곡을 미리 다운로드",
|
||||
"pre_download_play_description": "스트리밍 방식을 쓰는 대신 파일 단위로 다운로드 받고 재생 (인터넷 대역폭이 높은 환경에서 추천)",
|
||||
"skip_non_music": "음악이 아닌 부분을 스킵 (SponsorBlock)",
|
||||
"blacklist_description": "블랙리스트에 추가된 곡과 아티스트",
|
||||
"wait_for_download_to_finish": "현재 진행중인 다운로드가 끝날 때까지 기다려주세요",
|
||||
"desktop": "데스크톱",
|
||||
"close_behavior": "닫을 때의 동작",
|
||||
"close": "닫기",
|
||||
"minimize_to_tray": "트레이로 최소화",
|
||||
"show_tray_icon": "시스템 트레이 아이콘 표시",
|
||||
"about": "앱 정보",
|
||||
"u_love_spotube": "Spotube... 사랑하시죠?",
|
||||
"check_for_updates": "업데이트 확인",
|
||||
"about_spotube": "Spotube에 관해",
|
||||
"blacklist": "블랙리스트",
|
||||
"please_sponsor": "후원해주시면 감사하겠습니다.",
|
||||
"spotube_description": "Spotube는, 경량에 크로스플랫폼인데다 무료이기까지한 스포티파이 클라이언트입니다",
|
||||
"version": "버전",
|
||||
"build_number": "빌드 번호",
|
||||
"founder": "창시자",
|
||||
"repository": "리포지토리",
|
||||
"bug_issues": "버그 및 이슈",
|
||||
"made_with": "❤️을 담아 방글라데시에서 만듦",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "라이선스",
|
||||
"add_spotify_credentials": "먼저 Spotify의 로그인정보를 추가하기",
|
||||
"credentials_will_not_be_shared_disclaimer": "걱정마세요. 개인정보를 수집하거나 공유하지 않습니다.",
|
||||
"know_how_to_login": "어떻게 하는건지 모르겠나요?",
|
||||
"follow_step_by_step_guide": "사용법 확인하기",
|
||||
"spotify_cookie": "Spotify {name} Cookies",
|
||||
"cookie_name_cookie": "{name} Cookies",
|
||||
"fill_in_all_fields": "모든 필드에 정보를 입력해주세요",
|
||||
"submit": "제출",
|
||||
"exit": "종료",
|
||||
"previous": "이전으로",
|
||||
"next": "다음으로",
|
||||
"done": "완료",
|
||||
"step_1": "1단계",
|
||||
"first_go_to": "가장 먼저 먼저 들어갈 곳은 ",
|
||||
"login_if_not_logged_in": "그리고 로그인을 하지 않았다면 로그인해주세요",
|
||||
"step_2": "2단계",
|
||||
"step_2_steps": "1. 로그인에 성공하면、F12나 마우스 우클릭 > 검사(Inspect)을 눌러 브라우저의 개발자 도구(devtools)를 열어주세요.\n2. 애플리케이션 (Application) 탭 (Chrome, Edge, Brave 등) 또는 스토리지 탭 (Firefox, Palemoon 등)을 열어주세요.\n3. 쿠키 (Cookies) 섹션으로 들어가서, https://accounts.spotify.com 서브섹션으로 들어가주세요.",
|
||||
"step_3": "3단계",
|
||||
"success_emoji": "성공🥳",
|
||||
"success_message": "성공적으로 스포티파이 게정으로 로그인했습니다. 잘했어요!",
|
||||
"step_4": "4단계",
|
||||
"something_went_wrong": "알 수 없는 이유로 동작에 실패했습니다.",
|
||||
"piped_instance": "Piped 서버의 인스턴스",
|
||||
"piped_description": "곡 탐색에 사용할 Piped 서버 인스턴스",
|
||||
"piped_warning": "몇몇 서버는 제대로 동작하지 않을 수 있습니다. 본인 책임 하에 이용해주세요.",
|
||||
"generate_playlist": "플레이리스트 생성",
|
||||
"track_exists": "곡 {track} 은 이미 리스트에 있습니다",
|
||||
"replace_downloaded_tracks": "다운로드한 모든 곡을 교체",
|
||||
"skip_download_tracks": "다운로드가 끝난 곡을 모두 건너뛰기",
|
||||
"do_you_want_to_replace": "현재 곡을 교체하시겠습니까?",
|
||||
"replace": "교체",
|
||||
"skip": "건너뛰기",
|
||||
"select_up_to_count_type": "{type}을 {count}개까지 선택",
|
||||
"select_genres": "장르 선택",
|
||||
"add_genres": "장르 추가",
|
||||
"country": "국가",
|
||||
"number_of_tracks_generate": "생성할 곡 수",
|
||||
"acousticness": "반주 구간 (Acousticness)",
|
||||
"danceability": "흥겨운 정도 (Danceability)",
|
||||
"energy": "에너지 (Energy)",
|
||||
"instrumentalness": "기악성 (Instrumentalness)",
|
||||
"liveness": "생동감 (Liveness)",
|
||||
"loudness": "라우드니스 (Loudness)",
|
||||
"speechiness": "회화성 (Speechniss)",
|
||||
"valence": "감정가 (Valence)",
|
||||
"popularity": "인기도 (Popularity)",
|
||||
"key": "조성 (키)",
|
||||
"duration": "길이 (초)",
|
||||
"tempo": "템포 (BPM)",
|
||||
"mode": "장조",
|
||||
"time_signature": "박자",
|
||||
"short": "짧음",
|
||||
"medium": "중간",
|
||||
"long": "긺",
|
||||
"min": "최소",
|
||||
"max": "최대",
|
||||
"target": "목표",
|
||||
"moderate": "보통",
|
||||
"deselect_all": "모두 선택해제",
|
||||
"select_all": "모두 선택",
|
||||
"are_you_sure": "괜찮겠습니까?",
|
||||
"generating_playlist": "커스텀 플레이리스트를 생성하는 중...",
|
||||
"selected_count_tracks": "{count} 곡이 선택되었습니다.",
|
||||
"download_warning": "모든 트랙을 대량으로 다운로드하는 것은 명백한 불법 복제이며 음악 창작 사회에 피해를 입히는 행위입니다. 이 점을 알아주셨으면 합니다. 항상 아티스트의 노력을 존중하고 응원해 주세요.",
|
||||
"download_ip_ban_warning": "참고로, 평소보다 과도한 다운로드 요청으로 인해 YouTube에서 IP가 차단될 수 있습니다. IP 차단은 해당 IP 기기에서 최소 2~3개월 동안 (로그인한 상태에서도) YouTube를 사용할 수 없음을 의미합니다. 그리고 이런 일이 발생하더라도 스포튜브는 어떠한 책임도 지지 않습니다.",
|
||||
"by_clicking_accept_terms": "'동의'를 클릭하면 다음 약관에 동의하는 것입니다:",
|
||||
"download_agreement_1": "알고 있습니다. 전 나쁜 사람입니다.",
|
||||
"download_agreement_2": "제가 할 수 있는 모든 곳에서 아티스트를 지원할 것이며, 저는 그들의 작품을 살 돈이 없기 때문에 이렇게 하는 것뿐입니다.",
|
||||
"download_agreement_3": "본인은 YouTube에서 내 IP가 차단될 수 있음을 완전히 알고 있으며, 현재 내 행동으로 인해 발생하는 사고에 대해 Spotube 또는 그 소유자/기여자에게 책임을 묻지 않습니다.",
|
||||
"decline": "거절",
|
||||
"accept": "동의",
|
||||
"details": "상세",
|
||||
"youtube": "YouTube",
|
||||
"channel": "채널",
|
||||
"likes": "좋아요",
|
||||
"dislikes": "싫어요",
|
||||
"views": "조회수",
|
||||
"streamUrl": "스트림 URL",
|
||||
"stop": "중지",
|
||||
"sort_newest": "최근에 추가된 순으로 정렬",
|
||||
"sort_oldest": "예전에 추가된 순으로 정렬",
|
||||
"sleep_timer": "취침 타이머",
|
||||
"mins": "{minutes} 분",
|
||||
"hours": "{hours} 시간",
|
||||
"hour": "{hours} 시간",
|
||||
"custom_hours": "시간 설정",
|
||||
"logs": "로그",
|
||||
"developers": "개발",
|
||||
"not_logged_in": "로그인하지 않았습니다",
|
||||
"search_mode": "검색 모드",
|
||||
"audio_source": "오디오 출처",
|
||||
"ok": "알겠습니다",
|
||||
"failed_to_encrypt": "암호화에 실패했습니다",
|
||||
"encryption_failed_warning": "Spotube는 암호화를 사용하여 데이터를 안전하게 저장합니다. 하지만 그렇게 하지 못했습니다. 따라서 안전하지 않은 저장소로 대체됩니다.\n리눅스를 사용하는 경우, 비밀 서비스(gnome-keyring, kde-wallet, keepassxc 등)가 설치되어 있는지 확인하세요.",
|
||||
"querying_info": "정보를 얻는 중...",
|
||||
"piped_api_down": "Piped API가 응답하지 않습니다",
|
||||
"piped_down_error_instructions": "Piped 인스턴스 {pipedInstance}가 현재 다운되었습니다.\n\n인스턴스를 변경하거나 'API 유형'을 공식 YouTube API로 변경하세요.\n\n변경 후 앱을 다시 시작해야 합니다.",
|
||||
"you_are_offline": "현재 오프라인입니다",
|
||||
"connection_restored": "인터넷에 다시 연결되었습니다",
|
||||
"use_system_title_bar": "시스템 타이틀바를 사용",
|
||||
"update_playlist": "플레이리스트를 업데이트",
|
||||
"update": "업데이트",
|
||||
"crunching_results": "결과를 처리하는 중...",
|
||||
"search_to_get_results": "결과를 얻으려면 검색해주세요",
|
||||
"use_amoled_mode": "AMOLED모드를 사용",
|
||||
"pitch_dark_theme": "검정색 기반의 어두운 테마",
|
||||
"normalize_audio": "오디오 노멀라이즈",
|
||||
"change_cover": "커버 변경",
|
||||
"add_cover": "커버 추가",
|
||||
"restore_defaults": "기본값으로 복원",
|
||||
"download_music_codec": "다운로드 음악 코덱",
|
||||
"streaming_music_codec": "스트리밍 음악 코덱",
|
||||
"login_with_lastfm": "Last.fm에 로그인",
|
||||
"connect": "연결",
|
||||
"disconnect_lastfm": "Last.fm에서 연결 해제",
|
||||
"disconnect": "연결 해제",
|
||||
"username": "사용자명",
|
||||
"password": "비밀번호",
|
||||
"login": "로그인",
|
||||
"login_with_your_lastfm": "내 Last.fm 계정으로로그인",
|
||||
"scrobble_to_lastfm": "Scrobble to Last.fm",
|
||||
"go_to_album": "앨범으로 이동",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "모두 탐색",
|
||||
"genres": "장르",
|
||||
"explore_genres": "장르 탐색",
|
||||
"step_3_steps": "\"sp_dc\" 쿠키의 값을 복사",
|
||||
"step_4_steps": "복사한 \"sp_dc\"값을 붙여넣기",
|
||||
"friends": "친구",
|
||||
"no_lyrics_available": "죄송하지만 이 곡의 가사를 찾지 못했습니다",
|
||||
"@@locale": "ko"
|
||||
}
|
288
lib/l10n/app_vi.arb
Normal file
@ -0,0 +1,288 @@
|
||||
{
|
||||
"guest": "Khách",
|
||||
"browse": "Khám phá",
|
||||
"search": "Tìm kiếm",
|
||||
"library": "Thư viên",
|
||||
"lyrics": "Lời bài hát",
|
||||
"settings": "Cài đặt",
|
||||
"genre_categories_filter": "Lọc theo thể loại nhạc...",
|
||||
"genre": "Thể loại nhạc",
|
||||
"personalized": "Cá nhân hóa",
|
||||
"featured": "Nổi bật",
|
||||
"new_releases": "Bản phát hành mới",
|
||||
"songs": "Bài hát",
|
||||
"playing_track": "Đang phát {track}",
|
||||
"queue_clear_alert": "Điều này sẽ xóa hàng đợi hiện tại. {track_length} bài hát sẽ bị xóa\nBạn có muốn tiếp tục không?",
|
||||
"load_more": "Tải thêm",
|
||||
"playlists": "Danh sách phát",
|
||||
"artists": "Nghệ sĩ",
|
||||
"albums": "Album",
|
||||
"tracks": "Bài hát",
|
||||
"downloads": "Tải về",
|
||||
"filter_playlists": "Lọc danh sách phát...",
|
||||
"liked_tracks": "Bài hát được thích",
|
||||
"liked_tracks_description": "Tất cả bài hát bạn đã thích",
|
||||
"create_playlist": "Tạo danh sách phát",
|
||||
"create_a_playlist": "Tạo danh sách phát",
|
||||
"update_playlist": "Cập nhật danh sách phát",
|
||||
"create": "Tạo",
|
||||
"cancel": "Hủy",
|
||||
"update": "Cập nhật",
|
||||
"playlist_name": "Tên danh sách phát",
|
||||
"name_of_playlist": "Tên của danh sách phát",
|
||||
"description": "Mô tả",
|
||||
"public": "Công khai",
|
||||
"collaborative": "Hợp tác",
|
||||
"search_local_tracks": "Tìm kiếm bài hát trong máy...",
|
||||
"play": "Phát",
|
||||
"delete": "Xóa",
|
||||
"none": "Không có",
|
||||
"sort_a_z": "Sắp xếp theo A-Z",
|
||||
"sort_z_a": "Sắp xếp theo Z-A",
|
||||
"sort_artist": "Sắp xếp theo Nghệ sĩ",
|
||||
"sort_album": "Sắp xếp theo Album",
|
||||
"sort_tracks": "Sắp xếp các bài hát",
|
||||
"currently_downloading": "Đang tải về ({tracks_length} bài hát)",
|
||||
"cancel_all": "Hủy tất cả",
|
||||
"filter_artist": "Lọc nghệ sĩ...",
|
||||
"followers": "{followers} Người theo dõi",
|
||||
"add_artist_to_blacklist": "Thêm nghệ sĩ vào blacklist",
|
||||
"top_tracks": "Bài hát nổi bật",
|
||||
"fans_also_like": "Người hâm mộ cũng thích",
|
||||
"loading": "Đang tải...",
|
||||
"artist": "Nghệ sĩ",
|
||||
"blacklisted": "Đã đưa vào blacklist",
|
||||
"following": "Đang theo dõi",
|
||||
"follow": "Theo dõi",
|
||||
"artist_url_copied": "Đã sao chép URL nghệ sĩ",
|
||||
"added_to_queue": "Đã thêm {tracks} bài hát vào hàng đợi",
|
||||
"filter_albums": "Lọc album...",
|
||||
"synced": "Đồng bộ",
|
||||
"plain": "Bình thường",
|
||||
"shuffle": "Trộn",
|
||||
"search_tracks": "Tìm kiếm bài hát...",
|
||||
"released": "Phát hành",
|
||||
"error": "Lỗi {error}",
|
||||
"title": "Đề mục",
|
||||
"time": "Thời gian",
|
||||
"more_actions": "Thao tác khác",
|
||||
"download_count": "Tải xuống ({count})",
|
||||
"add_count_to_playlist": "Thêm ({count}) vào danh sách phát",
|
||||
"add_count_to_queue": "Thêm ({count}) vào hàng đợi",
|
||||
"play_count_next": "Phát ({count}) tiếp theo",
|
||||
"album": "Album",
|
||||
"copied_to_clipboard": "Đã sao chép {data} vào clipboard",
|
||||
"add_to_following_playlists": "Thêm {track} vào danh sách phát đang theo dõi",
|
||||
"add": "Thêm",
|
||||
"added_track_to_queue": "Đã thêm {track} vào hàng đợi",
|
||||
"add_to_queue": "Thêm vào hàng đợi",
|
||||
"track_will_play_next": "{track} sẽ được phát tiếp theo",
|
||||
"play_next": "Phát tiếp theo",
|
||||
"removed_track_from_queue": "Đã xóa {track} khỏi hàng đợi",
|
||||
"remove_from_queue": "Xóa khỏi hàng đợi",
|
||||
"remove_from_favorites": "Xóa khỏi bài hát yêu thích",
|
||||
"save_as_favorite": "Thêm vào bài hát yêu thích",
|
||||
"add_to_playlist": "Thêm vào danh sách phát",
|
||||
"remove_from_playlist": "Xóa khỏi danh sách phát",
|
||||
"add_to_blacklist": "Thêm vào blacklist",
|
||||
"remove_from_blacklist": "Xóa khỏi blacklist",
|
||||
"share": "Chia sẻ",
|
||||
"mini_player": "Trình phát thu nhỏ",
|
||||
"slide_to_seek": "Trượt để tìm kiếm tiến hoặc lùi",
|
||||
"shuffle_playlist": "Xáo trộn bài hát",
|
||||
"unshuffle_playlist": "Hủy xáo trộn bài hát",
|
||||
"previous_track": "Bài hát trước",
|
||||
"next_track": "Bài hát tiếp theo",
|
||||
"pause_playback": "Tạm dừng phát",
|
||||
"resume_playback": "Tiếp tục phát",
|
||||
"loop_track": "Lặp lại bài hát",
|
||||
"repeat_playlist": "Lặp lại danh sách phát",
|
||||
"queue": "Hàng đợi",
|
||||
"alternative_track_sources": "Đổi nguồn bài hát",
|
||||
"download_track": "Tải xuống",
|
||||
"tracks_in_queue": "{tracks} bài hát trong hàng đợi",
|
||||
"clear_all": "Xóa tất cả",
|
||||
"show_hide_ui_on_hover": "Hiển thị/Ẩn giao diện người dùng khi di chuột qua",
|
||||
"always_on_top": "Luôn ở trên cùng",
|
||||
"exit_mini_player": "Thoát khỏi trình phát thu nhỏ",
|
||||
"download_location": "Vị trí tải xuống",
|
||||
"account": "Tài khoản",
|
||||
"login_with_spotify": "Đăng nhập bằng tài khoản Spotify của bạn",
|
||||
"connect_with_spotify": "Liên kết với Spotify",
|
||||
"logout": "Đăng xuất",
|
||||
"logout_of_this_account": "Đăng xuất khỏi tài khoản này",
|
||||
"language_region": "Ngôn ngữ và Khu vực",
|
||||
"language": "Ngôn ngữ",
|
||||
"system_default": "Mặc định hệ thống",
|
||||
"market_place_region": "Khu vực Marketplace",
|
||||
"recommendation_country": "Quốc gia gợi ý",
|
||||
"appearance": "Giao diện",
|
||||
"layout_mode": "Chế độ layout",
|
||||
"override_layout_settings": "Ghi đè cài đặt layout",
|
||||
"adaptive": "Tương thích",
|
||||
"compact": "Nhỏ gọn",
|
||||
"extended": "Mở rộng",
|
||||
"theme": "Chủ đề",
|
||||
"dark": "Tối",
|
||||
"light": "Sáng",
|
||||
"system": "Hệ thống",
|
||||
"accent_color": "Màu nhấn",
|
||||
"sync_album_color": "Đồng bộ màu album",
|
||||
"sync_album_color_description": "Sử dụng màu chủ đạo của hình ảnh album làm màu nhấn",
|
||||
"playback": "Phát",
|
||||
"audio_quality": "Chất lượng âm thanh",
|
||||
"high": "Cao",
|
||||
"low": "Thấp",
|
||||
"pre_download_play": "Tải xuống và phát",
|
||||
"pre_download_play_description": "Thay vì stream âm thanh, tải xuống trước và phát (Khuyến nghị cho người dùng có băng thông cao)",
|
||||
"skip_non_music": "Bỏ qua các đoạn không phải nhạc (SponsorBlock)",
|
||||
"blacklist_description": "Các bài hát và nghệ sĩ trong blacklist",
|
||||
"wait_for_download_to_finish": "Vui lòng đợi quá trình tải xuống hiện tại hoàn thành",
|
||||
"desktop": "Máy tính",
|
||||
"close_behavior": "Thao tác đóng",
|
||||
"close": "Đóng",
|
||||
"minimize_to_tray": "Thu nhỏ vào khay hệ thống",
|
||||
"show_tray_icon": "Hiển thị biểu tượng trên khay hệ thống",
|
||||
"about": "Về chúng tôi",
|
||||
"u_love_spotube": "Chúng tôi biết bạn yêu Spotube",
|
||||
"check_for_updates": "Kiểm tra cập nhật",
|
||||
"about_spotube": "Về Spotube",
|
||||
"blacklist": "blacklist",
|
||||
"please_sponsor": "Vui lòng tài trợ/ủng hộ",
|
||||
"spotube_description": "Spotube, một ứng dụng Spotify nhẹ, đa nền tảng và miễn phí",
|
||||
"version": "Phiên bản",
|
||||
"build_number": "Số phiên bản",
|
||||
"founder": "Người sáng lập",
|
||||
"repository": "Mã nguồn",
|
||||
"bug_issues": "Báo cáo lỗi",
|
||||
"made_with": "Được làm bằng ❤️ ở Băng-la-đét",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Giấy phép",
|
||||
"add_spotify_credentials": "Điền thông tin đăng nhập Spotify của bạn",
|
||||
"credentials_will_not_be_shared_disclaimer": "Đừng lo, thông tin đăng nhập của bạn sẽ không được thu thập hoặc chia sẻ với bất kỳ ai",
|
||||
"know_how_to_login": "Không biết cách lấy thông tin đăng nhập?",
|
||||
"follow_step_by_step_guide": "Các bước lấy thông tin đăng nhập",
|
||||
"spotify_cookie": "Cookie Spotify {name}",
|
||||
"cookie_name_cookie": "Cookie {name}",
|
||||
"fill_in_all_fields": "Vui lòng điền đầy đủ thông tin",
|
||||
"submit": "Gửi",
|
||||
"exit": "Thoát",
|
||||
"previous": "Trước",
|
||||
"next": "Tiếp",
|
||||
"done": "Hoàn tất",
|
||||
"step_1": "Bước 1",
|
||||
"first_go_to": "Đầu tiên, truy cập",
|
||||
"login_if_not_logged_in": "và Đăng nhập/Đăng ký nếu chưa có tài khoản",
|
||||
"step_2": "Bước 2",
|
||||
"step_2_steps": "1. Sau khi đăng nhập, nhấn F12 hoặc Chuột phải > Mở devtools của trình duyệt.\n2. Sau đó, chuyển đến Tab \"Ứng dụng/Application\" (Chrome, Edge, Brave, v.v.) hoặc Tab \"Lưu trữ/Storage\" (Firefox, Palemoon, v.v.)\n3. Chuyển đến phần \"Cookie\" sau đó phần con \"https://accounts.spotify.com\"",
|
||||
"step_3": "Bước 3",
|
||||
"step_3_steps": "Sao chép giá trị của Cookie \"sp_dc\" và \"sp_key\" (hoặc sp_gaid)",
|
||||
"success_emoji": "Thành công🥳",
|
||||
"success_message": "Bây giờ bạn đã đăng nhập thành công bằng tài khoản Spotify của mình. Làm tốt lắm!",
|
||||
"step_4": "Bước 4",
|
||||
"step_4_steps": "Dán giá trị đã sao chép của Cookie \"sp_dc\" và \"sp_key\" (hoặc sp_gaid) vào các trường tương ứng",
|
||||
"something_went_wrong": "Đã xảy ra lỗi",
|
||||
"piped_instance": "Phiên bản Server Piped",
|
||||
"piped_description": "Phiên bản Piped để sử dụng cho Track matching",
|
||||
"piped_warning": "Một số phiên bản Piped có thể không hoạt động tốt",
|
||||
"generate_playlist": "Tạo danh sách phát",
|
||||
"track_exists": "Bài hát {track} đã tồn tại",
|
||||
"replace_downloaded_tracks": "Thay thế tất cả các bài hát đã tải",
|
||||
"skip_download_tracks": "Bỏ qua tải xuống tất cả các bài hát đã tải",
|
||||
"do_you_want_to_replace": "Bạn có muốn thay thế bài hát hiện có không?",
|
||||
"replace": "Thay thế",
|
||||
"skip": "Bỏ qua",
|
||||
"select_up_to_count_type": "Chọn tối đa {count} {type}",
|
||||
"select_genres": "Chọn Thể loại",
|
||||
"add_genres": "Thêm Thể loại",
|
||||
"country": "Quốc gia",
|
||||
"number_of_tracks_generate": "Số lượng bài hát để tạo",
|
||||
"acousticness": "Độ âm thanh",
|
||||
"danceability": "Khả năng nhảy",
|
||||
"energy": "Năng lượng",
|
||||
"instrumentalness": "Độ nhạc cụ",
|
||||
"liveness": "Sống động",
|
||||
"loudness": "Độ ồn",
|
||||
"speechiness": "Độ nói",
|
||||
"valence": "Tính tích cực",
|
||||
"popularity": "Độ phổ biến",
|
||||
"key": "Tông",
|
||||
"duration": "Thời lượng (giây)",
|
||||
"tempo": "Nhịp độ (BPM)",
|
||||
"mode": "Chế độ",
|
||||
"time_signature": "Chữ ký thời gian",
|
||||
"short": "Ngắn",
|
||||
"medium": "Trung bình",
|
||||
"long": "Dài",
|
||||
"min": "Tối thiểu",
|
||||
"max": "Tối đa",
|
||||
"target": "Mục tiêu",
|
||||
"moderate": "Trung bình",
|
||||
"deselect_all": "Bỏ chọn tất cả",
|
||||
"select_all": "Chọn tất cả",
|
||||
"are_you_sure": "Bạn có chắc chắn?",
|
||||
"generating_playlist": "Đang tạo danh sách phát tùy chỉnh của bạn...",
|
||||
"selected_count_tracks": "Đã chọn {count} bài hát",
|
||||
"download_warning": "Tải xuống tất cả các bài hát một lần, sẽ vi phạm bản quyền âm nhạc và gây thiệt hại cho xã hội sáng tạo âm nhạc. Hy vọng bạn nhận thức được điều này. Hãy luôn tôn trọng và ủng hộ công sức của nghệ sĩ",
|
||||
"download_ip_ban_warning": "Địa chỉ IP của bạn có thể bị chặn trên YouTube do yêu cầu tải xuống quá mức so với bình thường. Chặn IP có nghĩa là bạn không thể sử dụng YouTube (ngay cả khi bạn đã đăng nhập) ít nhất 2-3 tháng từ thiết bị IP đó. Và Spotube không chịu trách nhiệm nếu điều này xảy ra",
|
||||
"by_clicking_accept_terms": "Bằng cách nhấp vào 'Chấp nhận', bạn đồng ý với các điều khoản sau:",
|
||||
"download_agreement_1": "Tôi biết mình đang vi phạm bản quyền âm nhạc. Đó là không tốt.",
|
||||
"download_agreement_2": "Tôi sẽ ủng hộ nghệ sĩ bất cứ nơi nào tôi có thể và tôi chỉ làm điều này vì tôi không có tiền để mua tác phẩm của họ",
|
||||
"download_agreement_3": "Tôi hoàn toàn nhận thức được rằng địa chỉ IP của tôi có thể bị chặn trên YouTube và tôi không đổ lỗi cho Spotube hoặc chủ sở hữu/người đóng góp của nó về bất kỳ tai nạn nào do hành động này của tôi",
|
||||
"decline": "Từ chối",
|
||||
"accept": "Chấp nhận",
|
||||
"details": "Chi tiết",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Kênh",
|
||||
"likes": "Thích",
|
||||
"dislikes": "Không thích",
|
||||
"views": "Lượt xem",
|
||||
"streamUrl": "URL phát trực tiếp",
|
||||
"stop": "Dừng",
|
||||
"sort_newest": "Sắp xếp theo mới nhất",
|
||||
"sort_oldest": "Sắp xếp theo cũ nhất",
|
||||
"sleep_timer": "Hẹn giờ tắt",
|
||||
"mins": "{minutes} Phút",
|
||||
"hours": "{hours} Giờ",
|
||||
"hour": "{hours} Giờ",
|
||||
"custom_hours": "Giờ Tùy chỉnh",
|
||||
"logs": "Nhật ký",
|
||||
"developers": "Nhà phát triển",
|
||||
"not_logged_in": "Bạn chưa đăng nhập",
|
||||
"search_mode": "Chế độ tìm kiếm",
|
||||
"audio_source": "Nguồn âm thanh",
|
||||
"ok": "Ok",
|
||||
"failed_to_encrypt": "Mã hóa không thành công",
|
||||
"encryption_failed_warning": "Spotube không thành công trong việc mã hóa nhằm lưu trữ dữ liêu an toàn. vậy nên sẽ chuyển về lưu trữ không an toàn\nNếu bạn đang sử dụng Linux, đảm bảo rằng bạn có sử dụng dịch vụ bảo mật (gnome-keyring, kde-wallet, keepassxc, v.v.)",
|
||||
"querying_info": "Đang truy vấn thông tin...",
|
||||
"piped_api_down": "API Piped đang gặp sự cố",
|
||||
"piped_down_error_instructions": "Phiên bản Piped {pipedInstance} hiện đang gặp sự cố\n\nThay đổi phiên bản hoặc thay đổi 'Loại API' thành API YouTube official\n\nKhởi động lai ứng dụng sau khi thay đổi.",
|
||||
"you_are_offline": "Bạn đang ngoại tuyến",
|
||||
"connection_restored": "Kết nối internet của bạn đã được khôi phục",
|
||||
"use_system_title_bar": "Sử dụng thanh tiêu đề hệ thống",
|
||||
"crunching_results": "Đang tìm kiếm...",
|
||||
"search_to_get_results": "Chưa tìm kiếm",
|
||||
"use_amoled_mode": "Chủ đề tối hoàn toàn",
|
||||
"pitch_dark_theme": "Chế độ AMOLED",
|
||||
"normalize_audio": "Bình thường hóa âm thanh",
|
||||
"change_cover": "Thay đổi ảnh bìa",
|
||||
"add_cover": "Thêm ảnh bìa",
|
||||
"restore_defaults": "Khôi phục mặc định",
|
||||
"download_music_codec": "Định dạng tải xuống",
|
||||
"streaming_music_codec": "Định dạng nghe",
|
||||
"login_with_lastfm": "Đăng nhập bằng tài khoản Last.fm",
|
||||
"connect": "Liên kết",
|
||||
"disconnect_lastfm": "Dừng liên kết Last.fm",
|
||||
"disconnect": "Ngắt kết nối",
|
||||
"username": "Tên người dùng",
|
||||
"password": "Mật khẩu",
|
||||
"login": "Đăng nhập",
|
||||
"login_with_your_lastfm": "Đăng nhập bằng tài khoản Last.fm của bạn",
|
||||
"scrobble_to_lastfm": "Scrobble đến Last.fm",
|
||||
"go_to_album": "Đi đến Album",
|
||||
"discord_rich_presence": "Hiển thị trạng thái Discord",
|
||||
"browse_all": "Duyệt tất cả",
|
||||
"genres": "Thể loại",
|
||||
"explore_genres": "Khám phá Thể loại"
|
||||
}
|
@ -9,6 +9,8 @@
|
||||
/// energywave@github, ncvescera@github, OpenCode@github => Italian
|
||||
/// mdksec@github => Turkish
|
||||
/// Stephan-P@github, SecularSteve@github => Dutch
|
||||
/// doannc2212@github => Vietnamese
|
||||
/// sappho192@github => Korean
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class L10n {
|
||||
@ -25,6 +27,7 @@ class L10n {
|
||||
const Locale('hi', 'IN'),
|
||||
const Locale('it', 'IT'),
|
||||
const Locale('ja', 'JP'),
|
||||
const Locale('ko', 'KR'),
|
||||
const Locale('nl', 'NL'),
|
||||
const Locale('pl', 'PL'),
|
||||
const Locale('pt', 'PT'),
|
||||
@ -32,5 +35,6 @@ class L10n {
|
||||
const Locale('uk', 'UA'),
|
||||
const Locale('tr', 'TR'),
|
||||
const Locale('zh', 'CN'),
|
||||
const Locale('vi', 'VN'),
|
||||
];
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -15,9 +15,9 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
class AlbumPage extends HookConsumerWidget {
|
||||
final AlbumSimple album;
|
||||
const AlbumPage({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.album,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -47,6 +47,7 @@ class AlbumPage extends HookConsumerWidget {
|
||||
image: TypeConversionUtils.image_X_UrlString(
|
||||
album.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
index: 0,
|
||||
),
|
||||
title: album.name!,
|
||||
description:
|
||||
@ -69,8 +70,9 @@ class AlbumPage extends HookConsumerWidget {
|
||||
shareUrl: album.externalUrls!.spotify!,
|
||||
isLiked: isLiked,
|
||||
onHeart: albumIsSaved.hasData
|
||||
? () {
|
||||
toggleAlbumLike.mutate(isLiked);
|
||||
? () async {
|
||||
await toggleAlbumLike.mutate(isLiked);
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
child: const TrackView(),
|
||||
|
94
lib/pages/getting_started/getting_started.dart
Normal file
@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/greeting.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/playback.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/region.dart';
|
||||
import 'package:spotube/pages/getting_started/sections/support.dart';
|
||||
|
||||
class GettingStarting extends HookConsumerWidget {
|
||||
const GettingStarting({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
final pageController = usePageController();
|
||||
|
||||
final onNext = useCallback(() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}, [pageController]);
|
||||
|
||||
final onPrevious = useCallback(() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}, [pageController]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
actions: [
|
||||
ListenableBuilder(
|
||||
listenable: pageController,
|
||||
builder: (context, _) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: pageController.hasClients &&
|
||||
(pageController.page == 0 || pageController.page == 3)
|
||||
? const SizedBox()
|
||||
: TextButton(
|
||||
onPressed: () {
|
||||
pageController.animateToPage(
|
||||
3,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.skip_this_nonsense,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: Assets.bengaliPatternsBg.provider(),
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
colorScheme.background.withOpacity(0.2),
|
||||
BlendMode.srcOver,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
children: [
|
||||
GettingStartedPageGreetingSection(onNext: onNext),
|
||||
GettingStartedPageLanguageRegionSection(onNext: onNext),
|
||||
GettingStartedPagePlaybackSection(
|
||||
onNext: onNext,
|
||||
onPrevious: onPrevious,
|
||||
),
|
||||
const GettingStartedScreenSupportSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
55
lib/pages/getting_started/sections/greeting.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class GettingStartedPageGreetingSection extends HookConsumerWidget {
|
||||
final VoidCallback onNext;
|
||||
const GettingStartedPageGreetingSection({super.key, required this.onNext});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme) = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Assets.spotubeLogoPng.image(height: 200),
|
||||
const Gap(24),
|
||||
Text(
|
||||
"Spotube",
|
||||
style:
|
||||
textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
kIsMobile
|
||||
? context.l10n.freedom_of_music_palm
|
||||
: context.l10n.freedom_of_music,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const Gap(84),
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onNext,
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(context.l10n.get_started),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
161
lib/pages/getting_started/sections/playback.dart
Normal file
@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
|
||||
final audioSourceToIconMap = {
|
||||
AudioSource.youtube: const Icon(
|
||||
SpotubeIcons.youtube,
|
||||
color: Colors.red,
|
||||
size: 30,
|
||||
),
|
||||
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 30),
|
||||
AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48),
|
||||
};
|
||||
|
||||
class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
||||
final VoidCallback onNext;
|
||||
final VoidCallback onPrevious;
|
||||
|
||||
const GettingStartedPagePlaybackSection({
|
||||
super.key,
|
||||
required this.onNext,
|
||||
required this.onPrevious,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :colorScheme, :dividerColor) =
|
||||
Theme.of(context);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
final preferencesNotifier = ref.read(userPreferencesProvider.notifier);
|
||||
|
||||
final audioSourceToDescription = useMemoized(
|
||||
() => {
|
||||
AudioSource.youtube: "${context.l10n.youtube_source_description}\n"
|
||||
"${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}",
|
||||
AudioSource.piped: context.l10n.piped_source_description,
|
||||
AudioSource.jiosaavn:
|
||||
"${context.l10n.jiosaavn_source_description}\n"
|
||||
"${context.l10n.highest_quality("320kbps mp")}",
|
||||
},
|
||||
[]);
|
||||
|
||||
return Center(
|
||||
child: BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(SpotubeIcons.album, size: 16),
|
||||
const Gap(8),
|
||||
Text(context.l10n.playback, style: textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.l10n.select_audio_source,
|
||||
style: textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
ToggleButtons(
|
||||
isSelected: [
|
||||
for (final source in AudioSource.values)
|
||||
preferences.audioSource == source,
|
||||
],
|
||||
onPressed: (index) {
|
||||
preferencesNotifier.setAudioSource(AudioSource.values[index]);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
children: [
|
||||
for (final source in AudioSource.values)
|
||||
SizedBox.square(
|
||||
dimension: 84,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
audioSourceToIconMap[source]!,
|
||||
const Gap(8),
|
||||
Text(
|
||||
source.name,
|
||||
style: textTheme.bodySmall!.copyWith(
|
||||
color: preferences.audioSource == source
|
||||
? colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
title: Align(
|
||||
alignment: switch (preferences.audioSource) {
|
||||
AudioSource.youtube => Alignment.centerLeft,
|
||||
AudioSource.piped => Alignment.center,
|
||||
AudioSource.jiosaavn => Alignment.centerRight,
|
||||
},
|
||||
child: Text(
|
||||
audioSourceToDescription[preferences.audioSource]!,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
ListTile(
|
||||
title: Text(context.l10n.endless_playback),
|
||||
subtitle: Text(
|
||||
context.l10n.endless_playback_description,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: dividerColor,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
preferencesNotifier
|
||||
.setEndlessPlayback(!preferences.endlessPlayback);
|
||||
},
|
||||
trailing: Switch(
|
||||
value: preferences.endlessPlayback,
|
||||
onChanged: (value) {
|
||||
preferencesNotifier.setEndlessPlayback(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(34),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.angleLeft),
|
||||
label: Text(context.l10n.previous),
|
||||
onPressed: onPrevious,
|
||||
),
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(context.l10n.next),
|
||||
onPressed: onNext,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
129
lib/pages/getting_started/sections/region.dart
Normal file
@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/language_codes.dart';
|
||||
import 'package:spotube/collections/spotify_markets.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/l10n/l10n.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
|
||||
final void Function() onNext;
|
||||
const GettingStartedPageLanguageRegionSection(
|
||||
{super.key, required this.onNext});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :dividerColor) = Theme.of(context);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
|
||||
return SafeArea(
|
||||
child: Center(
|
||||
child: BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
SpotubeIcons.language,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.l10n.language_region,
|
||||
style: textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(48),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.choose_your_region,
|
||||
style: textTheme.titleSmall,
|
||||
),
|
||||
Text(
|
||||
context.l10n.choose_your_region_description,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: dividerColor,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownMenu(
|
||||
initialSelection: preferences.recommendationMarket,
|
||||
onSelected: (value) {
|
||||
if (value == null) return;
|
||||
ref
|
||||
.read(userPreferencesProvider.notifier)
|
||||
.setRecommendationMarket(value);
|
||||
},
|
||||
hintText: preferences.recommendationMarket.name,
|
||||
label: Text(context.l10n.market_place_region),
|
||||
inputDecorationTheme:
|
||||
const InputDecorationTheme(isDense: true),
|
||||
dropdownMenuEntries: [
|
||||
for (final market in spotifyMarkets)
|
||||
DropdownMenuEntry(
|
||||
value: market.$1,
|
||||
label: market.$2,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(36),
|
||||
Text(
|
||||
context.l10n.choose_your_language,
|
||||
style: textTheme.titleSmall,
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownMenu(
|
||||
initialSelection: preferences.locale,
|
||||
onSelected: (locale) {
|
||||
if (locale == null) return;
|
||||
ref
|
||||
.read(userPreferencesProvider.notifier)
|
||||
.setLocale(locale);
|
||||
},
|
||||
hintText: context.l10n.system_default,
|
||||
label: Text(context.l10n.language),
|
||||
inputDecorationTheme:
|
||||
const InputDecorationTheme(isDense: true),
|
||||
dropdownMenuEntries: [
|
||||
DropdownMenuEntry(
|
||||
value: const Locale("system", "system"),
|
||||
label: context.l10n.system_default,
|
||||
),
|
||||
for (final locale in L10n.all)
|
||||
DropdownMenuEntry(
|
||||
value: locale,
|
||||
label: LanguageLocals.getDisplayLanguage(
|
||||
locale.languageCode)
|
||||
.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(48),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(context.l10n.next),
|
||||
onPressed: onNext,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
130
lib/pages/getting_started/sections/support.dart
Normal file
@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
||||
const GettingStartedScreenSupportSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BlurCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(SpotubeIcons.heartFilled, color: Colors.pink),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.l10n.help_project_grow,
|
||||
style:
|
||||
textTheme.titleMedium?.copyWith(color: Colors.pink),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
Text(context.l10n.help_project_grow_description),
|
||||
const Gap(16),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.github),
|
||||
label: Text(context.l10n.contribute_on_github),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.black,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
await launchUrlString(
|
||||
"https://github.com/KRTirtho/spotube",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.openCollective),
|
||||
label: Text(context.l10n.donate_on_open_collective),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xff4cb7f6),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
await launchUrlString(
|
||||
"https://opencollective.com/spotube",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(48),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
colorScheme.primary,
|
||||
colorScheme.secondary,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(SpotubeIcons.anonymous),
|
||||
label: Text(context.l10n.browse_anonymously),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
KVStoreService.doneGettingStarted = true;
|
||||
context.go("/");
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(SpotubeIcons.spotify),
|
||||
label: Text(context.l10n.connect_with_spotify),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xff1db954),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
KVStoreService.doneGettingStarted = true;
|
||||
context.push("/login");
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -18,8 +18,7 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
|
||||
class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
final Category category;
|
||||
const GenrePlaylistsPage({Key? key, required this.category})
|
||||
: super(key: key);
|
||||
const GenrePlaylistsPage({super.key, required this.category});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -51,35 +50,29 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
)
|
||||
: null,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: CustomScrollView(
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(category.icons!.first.url!),
|
||||
alignment: Alignment.topCenter,
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(0.5),
|
||||
BlendMode.darken,
|
||||
),
|
||||
repeat: ImageRepeat.noRepeat,
|
||||
matchTextDirection: true,
|
||||
),
|
||||
),
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
automaticallyImplyLeading: DesktopTools.platform.isMobile,
|
||||
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
||||
pinned: true,
|
||||
floating: false,
|
||||
title: const Text(""),
|
||||
backgroundColor: Colors.brown.withOpacity(0.7),
|
||||
backgroundColor: Colors.transparent,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
stretchModes: const [
|
||||
StretchMode.zoomBackground,
|
||||
StretchMode.blurBackground,
|
||||
],
|
||||
background: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(
|
||||
category.icons!.first.url!,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: const ColoredBox(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
centerTitle: DesktopTools.platform.isDesktop,
|
||||
title: Text(
|
||||
category.name!,
|
||||
@ -169,6 +162,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
const SliverGap(20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
class GenrePage extends HookConsumerWidget {
|
||||
const GenrePage({Key? key}) : super(key: key);
|
||||
const GenrePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
|
@ -1,6 +1,4 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
@ -27,7 +27,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||
leading: ThemedButtonsTabBar(
|
||||
tabs: [
|
||||
Tab(text: " ${context.l10n.playlists} "),
|
||||
Tab(text: " ${context.l10n.tracks} "),
|
||||
Tab(text: " ${context.l10n.local_tracks} "),
|
||||
Tab(
|
||||
child: Badge(
|
||||
isLabelVisible: downloadingCount > 0,
|
||||
|
@ -2,8 +2,11 @@ import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/infinite_query.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
@ -45,6 +48,8 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
],
|
||||
);
|
||||
|
||||
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
|
||||
|
||||
return InheritedTrackView(
|
||||
collectionId: playlist.id!,
|
||||
image: TypeConversionUtils.image_X_UrlString(
|
||||
@ -72,9 +77,20 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
shareUrl: playlist.externalUrls?.spotify ?? "",
|
||||
onHeart: () async {
|
||||
if (!isLikedQuery.hasData || togglePlaylistLike.isMutating) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
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(),
|
||||
);
|
||||
|
@ -162,7 +162,15 @@ class RootApp extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (rootPaths[location] != 0) {
|
||||
onSelectIndexChanged(0);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Sidebar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
@ -195,6 +203,7 @@ class RootApp extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,9 +29,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return SectionCardWithHeading(
|
||||
heading: context.l10n.appearance,
|
||||
children: [
|
||||
final children = [
|
||||
AdaptiveSelectTile<LayoutMode>(
|
||||
secondary: const Icon(SpotubeIcons.dashboard),
|
||||
title: Text(context.l10n.layout_mode),
|
||||
@ -104,7 +107,23 @@ class SettingsAppearanceSection extends HookConsumerWidget {
|
||||
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: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import 'package:spotube/l10n/l10n.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
class SettingsLanguageRegionSection extends HookConsumerWidget {
|
||||
const SettingsLanguageRegionSection({Key? key}) : super(key: key);
|
||||
const SettingsLanguageRegionSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
|
@ -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!);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -175,59 +175,4 @@ class CustomSpotifyEndpoints {
|
||||
);
|
||||
return SpotifyFriends.fromJson(jsonDecode(res.body));
|
||||
}
|
||||
|
||||
Future<Artist> artist({required String id}) async {
|
||||
final pathQuery = "$_baseUrl/artists/$id";
|
||||
|
||||
final res = await _client.get(
|
||||
Uri.parse(pathQuery),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken",
|
||||
"accept": "application/json",
|
||||
},
|
||||
);
|
||||
final data = jsonDecode(res.body);
|
||||
|
||||
return Artist.fromJson(_purifyArtistResponse(data));
|
||||
}
|
||||
|
||||
Future<List<Artist>> relatedArtists({required String id}) async {
|
||||
final pathQuery = "$_baseUrl/artists/$id/related-artists";
|
||||
|
||||
final res = await _client.get(
|
||||
Uri.parse(pathQuery),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken",
|
||||
"accept": "application/json",
|
||||
},
|
||||
);
|
||||
|
||||
final data = jsonDecode(res.body);
|
||||
|
||||
return List.castFrom<dynamic, Artist>(
|
||||
data["artists"]
|
||||
.map((artist) => Artist.fromJson(_purifyArtistResponse(artist)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _purifyArtistResponse(Map<String, dynamic> data) {
|
||||
if (data["popularity"] != null) {
|
||||
data["popularity"] = data["popularity"].toInt();
|
||||
}
|
||||
if (data["followers"]?["total"] != null) {
|
||||
data["followers"]["total"] = data["followers"]["total"].toInt();
|
||||
}
|
||||
if (data["images"] != null) {
|
||||
data["images"] = data["images"].map((e) {
|
||||
e["height"] = e["height"].toInt();
|
||||
e["width"] = e["width"].toInt();
|
||||
return e;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
15
lib/services/kv_store/kv_store.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class KVStoreService {
|
||||
static SharedPreferences? _sharedPreferences;
|
||||
static SharedPreferences get sharedPreferences => _sharedPreferences!;
|
||||
|
||||
static Future<void> initialize() async {
|
||||
_sharedPreferences = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
static bool get doneGettingStarted =>
|
||||
sharedPreferences.getBool('doneGettingStarted') ?? false;
|
||||
static set doneGettingStarted(bool value) =>
|
||||
sharedPreferences.setBool('doneGettingStarted', value);
|
||||
}
|
@ -4,7 +4,6 @@ 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/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/wikipedia/wikipedia.dart';
|
||||
import 'package:wikipedia_api/wikipedia_api.dart';
|
||||
@ -16,10 +15,9 @@ class ArtistQueries {
|
||||
WidgetRef ref,
|
||||
String artist,
|
||||
) {
|
||||
final customSpotify = ref.watch(customSpotifyEndpointProvider);
|
||||
return useSpotifyQuery<Artist, dynamic>(
|
||||
"artist-profile/$artist",
|
||||
(spotify) => customSpotify.artist(id: artist),
|
||||
(spotify) => spotify.artists.get(artist),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
@ -127,11 +125,10 @@ class ArtistQueries {
|
||||
WidgetRef ref,
|
||||
String artist,
|
||||
) {
|
||||
final customSpotify = ref.watch(customSpotifyEndpointProvider);
|
||||
return useSpotifyQuery<Iterable<Artist>, dynamic>(
|
||||
"artist-related-artist-query/$artist",
|
||||
(spotify) {
|
||||
return customSpotify.relatedArtists(id: artist);
|
||||
return spotify.artists.relatedArtists(artist);
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
|
19
lib/services/song_link/model.dart
Normal file
@ -0,0 +1,19 @@
|
||||
part of './song_link.dart';
|
||||
|
||||
@freezed
|
||||
class SongLink with _$SongLink {
|
||||
const factory SongLink({
|
||||
required String displayName,
|
||||
required String linkId,
|
||||
required String platform,
|
||||
required bool show,
|
||||
required String? uniqueId,
|
||||
required String? country,
|
||||
required String? url,
|
||||
required String? nativeAppUriMobile,
|
||||
required String? nativeAppUriDesktop,
|
||||
}) = _SongLink;
|
||||
|
||||
factory SongLink.fromJson(Map<String, dynamic> json) =>
|
||||
_$SongLinkFromJson(json);
|
||||
}
|
54
lib/services/song_link/song_link.dart
Normal file
@ -0,0 +1,54 @@
|
||||
library song_link;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:html/parser.dart';
|
||||
|
||||
part 'model.dart';
|
||||
|
||||
part 'song_link.freezed.dart';
|
||||
part 'song_link.g.dart';
|
||||
|
||||
abstract class SongLinkService {
|
||||
static final dio = Dio();
|
||||
static Future<List<SongLink>> links(String spotifyId) async {
|
||||
try {
|
||||
final res = await dio.get(
|
||||
"https://song.link/s/$spotifyId",
|
||||
options: Options(
|
||||
headers: {
|
||||
"Accept":
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||||
},
|
||||
responseType: ResponseType.plain,
|
||||
),
|
||||
);
|
||||
|
||||
final document = parse(res.data);
|
||||
|
||||
final script = document.getElementById("__NEXT_DATA__")?.text;
|
||||
|
||||
if (script == null) {
|
||||
return <SongLink>[];
|
||||
}
|
||||
|
||||
final pageProps = jsonDecode(script) as Map<String, dynamic>;
|
||||
final songLinks = pageProps["props"]?["pageProps"]?["pageData"]
|
||||
?["sections"]
|
||||
?.firstWhere(
|
||||
(section) => section?["sectionId"] == "section|auto|links|listen",
|
||||
)?["links"] as List?;
|
||||
|
||||
return songLinks?.map((link) => SongLink.fromJson(link)).toList() ??
|
||||
<SongLink>[];
|
||||
} catch (e, stackTrace) {
|
||||
Catcher2.reportCheckedError(e, stackTrace);
|
||||
return <SongLink>[];
|
||||
}
|
||||
}
|
||||
}
|
320
lib/services/song_link/song_link.freezed.dart
Normal file
@ -0,0 +1,320 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'song_link.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
SongLink _$SongLinkFromJson(Map<String, dynamic> json) {
|
||||
return _SongLink.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SongLink {
|
||||
String get displayName => throw _privateConstructorUsedError;
|
||||
String get linkId => throw _privateConstructorUsedError;
|
||||
String get platform => throw _privateConstructorUsedError;
|
||||
bool get show => throw _privateConstructorUsedError;
|
||||
String? get uniqueId => throw _privateConstructorUsedError;
|
||||
String? get country => throw _privateConstructorUsedError;
|
||||
String? get url => throw _privateConstructorUsedError;
|
||||
String? get nativeAppUriMobile => throw _privateConstructorUsedError;
|
||||
String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$SongLinkCopyWith<SongLink> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SongLinkCopyWith<$Res> {
|
||||
factory $SongLinkCopyWith(SongLink value, $Res Function(SongLink) then) =
|
||||
_$SongLinkCopyWithImpl<$Res, SongLink>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String displayName,
|
||||
String linkId,
|
||||
String platform,
|
||||
bool show,
|
||||
String? uniqueId,
|
||||
String? country,
|
||||
String? url,
|
||||
String? nativeAppUriMobile,
|
||||
String? nativeAppUriDesktop});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink>
|
||||
implements $SongLinkCopyWith<$Res> {
|
||||
_$SongLinkCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? displayName = null,
|
||||
Object? linkId = null,
|
||||
Object? platform = null,
|
||||
Object? show = null,
|
||||
Object? uniqueId = freezed,
|
||||
Object? country = freezed,
|
||||
Object? url = freezed,
|
||||
Object? nativeAppUriMobile = freezed,
|
||||
Object? nativeAppUriDesktop = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
displayName: null == displayName
|
||||
? _value.displayName
|
||||
: displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
linkId: null == linkId
|
||||
? _value.linkId
|
||||
: linkId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
platform: null == platform
|
||||
? _value.platform
|
||||
: platform // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
show: null == show
|
||||
? _value.show
|
||||
: show // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
uniqueId: freezed == uniqueId
|
||||
? _value.uniqueId
|
||||
: uniqueId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
country: freezed == country
|
||||
? _value.country
|
||||
: country // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: freezed == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriMobile: freezed == nativeAppUriMobile
|
||||
? _value.nativeAppUriMobile
|
||||
: nativeAppUriMobile // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriDesktop: freezed == nativeAppUriDesktop
|
||||
? _value.nativeAppUriDesktop
|
||||
: nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SongLinkImplCopyWith<$Res>
|
||||
implements $SongLinkCopyWith<$Res> {
|
||||
factory _$$SongLinkImplCopyWith(
|
||||
_$SongLinkImpl value, $Res Function(_$SongLinkImpl) then) =
|
||||
__$$SongLinkImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String displayName,
|
||||
String linkId,
|
||||
String platform,
|
||||
bool show,
|
||||
String? uniqueId,
|
||||
String? country,
|
||||
String? url,
|
||||
String? nativeAppUriMobile,
|
||||
String? nativeAppUriDesktop});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SongLinkImplCopyWithImpl<$Res>
|
||||
extends _$SongLinkCopyWithImpl<$Res, _$SongLinkImpl>
|
||||
implements _$$SongLinkImplCopyWith<$Res> {
|
||||
__$$SongLinkImplCopyWithImpl(
|
||||
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? displayName = null,
|
||||
Object? linkId = null,
|
||||
Object? platform = null,
|
||||
Object? show = null,
|
||||
Object? uniqueId = freezed,
|
||||
Object? country = freezed,
|
||||
Object? url = freezed,
|
||||
Object? nativeAppUriMobile = freezed,
|
||||
Object? nativeAppUriDesktop = freezed,
|
||||
}) {
|
||||
return _then(_$SongLinkImpl(
|
||||
displayName: null == displayName
|
||||
? _value.displayName
|
||||
: displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
linkId: null == linkId
|
||||
? _value.linkId
|
||||
: linkId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
platform: null == platform
|
||||
? _value.platform
|
||||
: platform // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
show: null == show
|
||||
? _value.show
|
||||
: show // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
uniqueId: freezed == uniqueId
|
||||
? _value.uniqueId
|
||||
: uniqueId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
country: freezed == country
|
||||
? _value.country
|
||||
: country // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
url: freezed == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriMobile: freezed == nativeAppUriMobile
|
||||
? _value.nativeAppUriMobile
|
||||
: nativeAppUriMobile // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
nativeAppUriDesktop: freezed == nativeAppUriDesktop
|
||||
? _value.nativeAppUriDesktop
|
||||
: nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SongLinkImpl implements _SongLink {
|
||||
const _$SongLinkImpl(
|
||||
{required this.displayName,
|
||||
required this.linkId,
|
||||
required this.platform,
|
||||
required this.show,
|
||||
required this.uniqueId,
|
||||
required this.country,
|
||||
required this.url,
|
||||
required this.nativeAppUriMobile,
|
||||
required this.nativeAppUriDesktop});
|
||||
|
||||
factory _$SongLinkImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SongLinkImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String displayName;
|
||||
@override
|
||||
final String linkId;
|
||||
@override
|
||||
final String platform;
|
||||
@override
|
||||
final bool show;
|
||||
@override
|
||||
final String? uniqueId;
|
||||
@override
|
||||
final String? country;
|
||||
@override
|
||||
final String? url;
|
||||
@override
|
||||
final String? nativeAppUriMobile;
|
||||
@override
|
||||
final String? nativeAppUriDesktop;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SongLink(displayName: $displayName, linkId: $linkId, platform: $platform, show: $show, uniqueId: $uniqueId, country: $country, url: $url, nativeAppUriMobile: $nativeAppUriMobile, nativeAppUriDesktop: $nativeAppUriDesktop)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SongLinkImpl &&
|
||||
(identical(other.displayName, displayName) ||
|
||||
other.displayName == displayName) &&
|
||||
(identical(other.linkId, linkId) || other.linkId == linkId) &&
|
||||
(identical(other.platform, platform) ||
|
||||
other.platform == platform) &&
|
||||
(identical(other.show, show) || other.show == show) &&
|
||||
(identical(other.uniqueId, uniqueId) ||
|
||||
other.uniqueId == uniqueId) &&
|
||||
(identical(other.country, country) || other.country == country) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.nativeAppUriMobile, nativeAppUriMobile) ||
|
||||
other.nativeAppUriMobile == nativeAppUriMobile) &&
|
||||
(identical(other.nativeAppUriDesktop, nativeAppUriDesktop) ||
|
||||
other.nativeAppUriDesktop == nativeAppUriDesktop));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
|
||||
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||
__$$SongLinkImplCopyWithImpl<_$SongLinkImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SongLinkImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SongLink implements SongLink {
|
||||
const factory _SongLink(
|
||||
{required final String displayName,
|
||||
required final String linkId,
|
||||
required final String platform,
|
||||
required final bool show,
|
||||
required final String? uniqueId,
|
||||
required final String? country,
|
||||
required final String? url,
|
||||
required final String? nativeAppUriMobile,
|
||||
required final String? nativeAppUriDesktop}) = _$SongLinkImpl;
|
||||
|
||||
factory _SongLink.fromJson(Map<String, dynamic> json) =
|
||||
_$SongLinkImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get displayName;
|
||||
@override
|
||||
String get linkId;
|
||||
@override
|
||||
String get platform;
|
||||
@override
|
||||
bool get show;
|
||||
@override
|
||||
String? get uniqueId;
|
||||
@override
|
||||
String? get country;
|
||||
@override
|
||||
String? get url;
|
||||
@override
|
||||
String? get nativeAppUriMobile;
|
||||
@override
|
||||
String? get nativeAppUriDesktop;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
33
lib/services/song_link/song_link.g.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'song_link.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SongLinkImpl _$$SongLinkImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SongLinkImpl(
|
||||
displayName: json['displayName'] as String,
|
||||
linkId: json['linkId'] as String,
|
||||
platform: json['platform'] as String,
|
||||
show: json['show'] as bool,
|
||||
uniqueId: json['uniqueId'] as String?,
|
||||
country: json['country'] as String?,
|
||||
url: json['url'] as String?,
|
||||
nativeAppUriMobile: json['nativeAppUriMobile'] as String?,
|
||||
nativeAppUriDesktop: json['nativeAppUriDesktop'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SongLinkImplToJson(_$SongLinkImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'displayName': instance.displayName,
|
||||
'linkId': instance.linkId,
|
||||
'platform': instance.platform,
|
||||
'show': instance.show,
|
||||
'uniqueId': instance.uniqueId,
|
||||
'country': instance.country,
|
||||
'url': instance.url,
|
||||
'nativeAppUriMobile': instance.nativeAppUriMobile,
|
||||
'nativeAppUriDesktop': instance.nativeAppUriDesktop,
|
||||
};
|
@ -1,7 +1,12 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
class TrackNotFoundException implements Exception {
|
||||
factory TrackNotFoundException(Track track) {
|
||||
throw Exception("Failed to find any results for ${track.name}");
|
||||
class TrackNotFoundError extends Error {
|
||||
final Track track;
|
||||
|
||||
TrackNotFoundError(this.track);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '[TrackNotFoundError] ${track.name} - ${track.artists?.map((e) => e.name).join(", ")}';
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
abstract class SourcedTrack extends Track {
|
||||
final SourceMap source;
|
||||
@ -101,9 +107,8 @@ abstract class SourcedTrack extends Track {
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
try {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
|
||||
try {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
@ -112,8 +117,35 @@ abstract class SourcedTrack extends Track {
|
||||
AudioSource.jiosaavn =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on TrackNotFoundError catch (_) {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.youtube =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
),
|
||||
AudioSource.jiosaavn =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on HttpClientClosedException catch (_) {
|
||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
} catch (e) {
|
||||
return YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
if (e is DioException || e is ClientException || e is SocketException) {
|
||||
if (preferences.audioSource == AudioSource.jiosaavn) {
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
);
|
||||
}
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,15 +37,17 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
static Future<SourcedTrack> fetchFromTrack({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
bool weakMatch = false,
|
||||
}) async {
|
||||
final cachedSource = await SourceMatch.box.get(track.id);
|
||||
|
||||
if (cachedSource == null ||
|
||||
cachedSource.sourceType != SourceType.jiosaavn) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
final siblings =
|
||||
await fetchSiblings(ref: ref, track: track, weakMatch: weakMatch);
|
||||
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -119,6 +121,7 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
static Future<List<SiblingType>> fetchSiblings({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
bool weakMatch = false,
|
||||
}) async {
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
@ -126,9 +129,12 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
await jiosaavnClient.search.songs(query, limit: 20);
|
||||
|
||||
final trackArtistNames = track.artists?.map((ar) => ar.name).toList();
|
||||
return results
|
||||
|
||||
final matchedResults = results
|
||||
.where(
|
||||
(s) {
|
||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
||||
|
||||
final sameName = s.name?.unescapeHtml() == track.name;
|
||||
final artistNames = [
|
||||
s.primaryArtists,
|
||||
@ -139,12 +145,27 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
(artist) =>
|
||||
trackArtistNames?.any((ar) => artist == ar) ?? false,
|
||||
);
|
||||
if (weakMatch) {
|
||||
final containsName =
|
||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
||||
final containsPrimaryArtist = s.primaryArtists
|
||||
.unescapeHtml()
|
||||
.contains(trackArtistNames?.first ?? "");
|
||||
|
||||
return containsName && containsPrimaryArtist;
|
||||
}
|
||||
|
||||
return sameName && sameArtists;
|
||||
},
|
||||
)
|
||||
.map(toSiblingType)
|
||||
.toList();
|
||||
|
||||
if (weakMatch && matchedResults.isEmpty) {
|
||||
return results.map(toSiblingType).toList();
|
||||
}
|
||||
|
||||
return matchedResults;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -55,7 +55,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
if (cachedSource == null) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -157,16 +157,20 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
}) async {
|
||||
final pipedClient = ref.read(pipedProvider);
|
||||
final preference = ref.read(userPreferencesProvider);
|
||||
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
||||
"$query - Topic",
|
||||
query,
|
||||
preference.searchMode == SearchMode.youtube
|
||||
? PipedFilter.video
|
||||
? PipedFilter.videos
|
||||
: PipedFilter.musicSongs,
|
||||
);
|
||||
|
||||
final isYouTubeMusic = preference.searchMode == SearchMode.youtubeMusic;
|
||||
// when falling back to piped API make sure to use the YouTube mode
|
||||
final isYouTubeMusic = preference.audioSource != AudioSource.piped
|
||||
? false
|
||||
: preference.searchMode == SearchMode.youtubeMusic;
|
||||
|
||||
if (isYouTubeMusic) {
|
||||
final artists = (track.artists ?? [])
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/source_match.dart';
|
||||
import 'package:spotube/services/song_link/song_link.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
@ -48,7 +50,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -70,8 +72,13 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
);
|
||||
}
|
||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||
final manifest = await youtubeClient.videos.streamsClient.getManifest(
|
||||
final manifest = await youtubeClient.videos.streamsClient
|
||||
.getManifest(
|
||||
cachedSource.sourceId,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
return YoutubeSourcedTrack(
|
||||
ref: ref,
|
||||
@ -125,7 +132,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
SourceMap? sourceMap;
|
||||
if (index == 0) {
|
||||
final manifest =
|
||||
await youtubeClient.videos.streamsClient.getManifest(item.id);
|
||||
await youtubeClient.videos.streamsClient.getManifest(item.id).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
sourceMap = toSourceMap(manifest);
|
||||
}
|
||||
|
||||
@ -207,6 +217,20 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final links = await SongLinkService.links(track.id!);
|
||||
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
||||
|
||||
if (ytLink?.url != null) {
|
||||
return [
|
||||
await toSiblingType(
|
||||
0,
|
||||
YoutubeVideoInfo.fromVideo(
|
||||
await youtubeClient.videos.get(ytLink!.url!),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
final searchResults = await youtubeClient.search.search(
|
||||
@ -243,8 +267,12 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||
..insert(0, sourceInfo);
|
||||
|
||||
final manifest =
|
||||
await youtubeClient.videos.streamsClient.getManifest(newSourceInfo.id);
|
||||
final manifest = await youtubeClient.videos.streamsClient
|
||||
.getManifest(newSourceInfo.id)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
|
||||
await SourceMatch.box.put(
|
||||
id!,
|
||||
|
@ -119,7 +119,9 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||
Future<void> _load() async {
|
||||
final json = await box.get(cacheKey);
|
||||
|
||||
if (json != null) {
|
||||
if (json != null ||
|
||||
(json is Map && json.entries.isNotEmpty) ||
|
||||
(json is List && json.isNotEmpty)) {
|
||||
state = await fromJson(castNestedJson(json));
|
||||
}
|
||||
}
|
||||
|
@ -53,9 +53,9 @@ abstract class ServiceUtils {
|
||||
|
||||
return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}"
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(" *\\[[^\\]]*]"), '')
|
||||
.replaceAll(RegExp("feat.|ft."), '')
|
||||
.replaceAll(RegExp("\\s+"), ' ')
|
||||
.replaceAll(RegExp(r"\s*\[[^\]]*]"), ' ')
|
||||
.replaceAll(RegExp(r"\sfeat\.|\sft\."), ' ')
|
||||
.replaceAll(RegExp(r"\s+"), ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
@ -292,24 +292,24 @@ abstract class ServiceUtils {
|
||||
return List<T>.from(tracks)
|
||||
..sort((a, b) {
|
||||
switch (sortBy) {
|
||||
case SortBy.album:
|
||||
return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0;
|
||||
case SortBy.artist:
|
||||
return a.artists?.first.name
|
||||
?.compareTo(b.artists?.first.name ?? "") ??
|
||||
0;
|
||||
case SortBy.ascending:
|
||||
return a.name?.compareTo(b.name ?? "") ?? 0;
|
||||
case SortBy.oldest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return aDate.compareTo(bDate);
|
||||
case SortBy.descending:
|
||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||
case SortBy.newest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return bDate.compareTo(aDate);
|
||||
case SortBy.descending:
|
||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||
case SortBy.oldest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return aDate.compareTo(bDate);
|
||||
case SortBy.duration:
|
||||
return a.durationMs?.compareTo(b.durationMs ?? 0) ?? 0;
|
||||
case SortBy.artist:
|
||||
return a.artists?.first.name?.compareTo(b.artists?.first.name ?? "") ?? 0;
|
||||
case SortBy.album:
|
||||
return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
@ -6,3 +6,4 @@ Icon=/usr/share/icons/spotube/spotube-logo.png
|
||||
Comment=A music streaming app combining the power of Spotify & YouTube
|
||||
Terminal=false
|
||||
Categories=Audio;Music;Player;AudioVideo;
|
||||
MimeType=x-scheme-handler/spotify;
|
@ -143,4 +143,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
@ -582,6 +582,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Spotube;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music";
|
||||
|
93
pubspec.lock
@ -17,6 +17,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.0"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -149,10 +157,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_session
|
||||
sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad"
|
||||
sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.16"
|
||||
version: "0.1.18"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -470,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:
|
||||
@ -680,6 +688,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.9"
|
||||
flutter_broadcasts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_broadcasts
|
||||
sha256: "9e76eeeda4a9faef63e3b08af5664c79219a2eabffc8ce95296858ea70423b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -818,10 +834,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "3.0.1"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -839,10 +855,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: "91004565166dbbc7a85e7e99b84124a287839830ca957cfe45004793fe6fe69f"
|
||||
sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.3.10"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -855,10 +871,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:
|
||||
@ -1014,10 +1030,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 +1094,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:
|
||||
@ -1102,10 +1118,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1291,10 +1307,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
local_notifier:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1611,18 +1627,19 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
version: "6.0.2"
|
||||
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:
|
||||
@ -1715,10 +1732,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: puppeteer
|
||||
sha256: "59e723cc5b69537159a7c34efd645dc08a6a1ac4647d7d7823606802c0f93cdb"
|
||||
sha256: eedeaae6ec5d2e54f9ae22ab4d6b3dda2e8791c356cc783046d06c287ffe11d8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.6.0"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1731,10 +1748,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:
|
||||
@ -1945,10 +1962,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: spotify
|
||||
sha256: e967c5e295792e9d38f4c5e9e60d7c2868ed9cb2a8fac2a67c75303f8395e374
|
||||
sha256: "2308a84511c18ec1e72515a57e28abb1467389549d571c460732b4538c2e34de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
version: "0.13.3"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2261,6 +2278,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2330,10 +2355,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2351,5 +2376,5 @@ packages:
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
sdks:
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
23
pubspec.yaml
@ -16,7 +16,7 @@ dependencies:
|
||||
args: ^2.3.2
|
||||
async: ^2.9.0
|
||||
audio_service: ^0.18.9
|
||||
audio_session: ^0.1.13
|
||||
audio_session: ^0.1.18
|
||||
auto_size_text: ^3.0.0
|
||||
buttons_tabbar: ^1.3.6
|
||||
cached_network_image: ^3.3.0
|
||||
@ -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
|
||||
@ -49,19 +49,19 @@ dependencies:
|
||||
flutter_inappwebview: ^5.7.2+3
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
flutter_native_splash: ^2.3.3
|
||||
flutter_riverpod: ^2.4.3
|
||||
flutter_native_splash: ^2.3.10
|
||||
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
|
||||
hooks_riverpod: ^2.4.3
|
||||
html: ^0.15.1
|
||||
http: ^1.1.0
|
||||
http: ^1.2.0
|
||||
image_picker: ^1.0.4
|
||||
intl: ^0.18.0
|
||||
introduction_screen: ^3.0.2
|
||||
@ -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:
|
||||
@ -87,7 +89,6 @@ dependencies:
|
||||
shared_preferences: ^2.2.2
|
||||
skeleton_text: ^3.0.1
|
||||
smtc_windows: ^0.1.1
|
||||
spotify: ^0.12.0
|
||||
stroke_text: ^0.0.2
|
||||
system_theme: ^2.1.0
|
||||
titlebar_buttons: ^1.0.0
|
||||
@ -122,7 +123,9 @@ 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
|
||||
spotify: ^0.13.3
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
@ -130,7 +133,7 @@ dev_dependencies:
|
||||
flutter_distributor: ^0.0.2
|
||||
flutter_gen_runner: ^5.1.0+1
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_lints: ^2.0.1
|
||||
flutter_lints: ^3.0.1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
@ -142,7 +145,6 @@ dev_dependencies:
|
||||
freezed: ^2.4.6
|
||||
|
||||
dependency_overrides:
|
||||
http: ^1.1.0
|
||||
system_tray: 2.0.2
|
||||
|
||||
flutter:
|
||||
@ -151,6 +153,7 @@ flutter:
|
||||
assets:
|
||||
- assets/
|
||||
- assets/tutorial/
|
||||
- assets/logos/
|
||||
- LICENSE
|
||||
|
||||
flutter_launcher_icons:
|
||||
|
@ -1,127 +1,574 @@
|
||||
{
|
||||
"ar": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
"sort_duration",
|
||||
"friends",
|
||||
"no_lyrics_available",
|
||||
"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"
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks",
|
||||
"song_link",
|
||||
"skip_this_nonsense",
|
||||
"freedom_of_music",
|
||||
"freedom_of_music_palm",
|
||||
"get_started",
|
||||
"youtube_source_description",
|
||||
"piped_source_description",
|
||||
"jiosaavn_source_description",
|
||||
"highest_quality",
|
||||
"select_audio_source",
|
||||
"endless_playback_description",
|
||||
"choose_your_region",
|
||||
"choose_your_region_description",
|
||||
"choose_your_language",
|
||||
"help_project_grow",
|
||||
"help_project_grow_description",
|
||||
"contribute_on_github",
|
||||
"donate_on_open_collective",
|
||||
"browse_anonymously"
|
||||
]
|
||||
}
|
||||
|
@ -29,15 +29,83 @@
|
||||
|
||||
<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="">
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |