Compare commits

...

35 Commits

Author SHA1 Message Date
Tobse
37e60fb4a6
Merge 24f186e9fd into 4fae9013a7 2025-11-11 03:11:06 -04:00
Kingkor Roy Tirtho
4fae9013a7 fix: download not working in different devices and slow 2025-11-11 10:39:13 +06:00
Kingkor Roy Tirtho
834445eda3 chore: remove jsf as arm doesn't build 2025-11-10 13:07:58 +06:00
Rahul Sahani
f10a3d4976
feat(queue): add multi-select and bulk actions to queue (#2839)
* feat(queue): add multi-select and bulk actions to queue

- Add selection mode to PlayerQueue with long-press to select
- Disable inner navigation (title/artist) when selecting via TrackTile
- Show checkboxes only in selection mode
- Add selection AppBar behavior and bottom-sheet menu with: Select all, Add to playlist, Remove selected, Cancel
- Reuse existing PlaylistAddTrackDialog for bulk add
- Hide drag handle while in selection mode

Closes: # (implement multi-select queue feature)

* chore: update .gitignore to include .vscode and modify signing configurations back to default in build.gradle

* chore: add VS Code configuration files

* chore: update dependencies in pubspec.lock

* chore: update pubspec.lock to reflect dependency changes and version updates

* chore: fix replace material widgets with shadcn widgets

---------

Co-authored-by: Kingkor Roy Tirtho <krtirtho@gmail.com>
2025-11-10 10:36:10 +06:00
Kingkor Roy Tirtho
3209c75144 fix: downloaded tracks are not tagged with metadata 2025-11-08 15:49:37 +06:00
Kingkor Roy Tirtho
700a69fcd1
Merge pull request #2840 from KRTirtho/audio-source-extension
feat: add audio source plugin support
2025-11-08 14:14:06 +06:00
Kingkor Roy Tirtho
d2dd60aa5c chore: update YoutubeExplode to v3 2025-11-08 13:48:50 +06:00
Kingkor Roy Tirtho
fda2257119 feat: add default plugin loading capability 2025-11-07 22:51:48 +06:00
Kingkor Roy Tirtho
7c632c8f06 cd: remove unnecessary stuff for android build 2025-11-07 21:56:03 +06:00
Kingkor Roy Tirtho
a012a8f3af chore: fix unique index on source_match_table causing failure on insert 2025-11-07 20:28:09 +06:00
Kingkor Roy Tirtho
64f937bd14 chore: remove useless appbundle build 2025-11-07 18:59:55 +06:00
Kingkor Roy Tirtho
d1b73dbb1c feat: add NewPipe support for desktop platforms 2025-11-07 18:48:18 +06:00
Kingkor Roy Tirtho
e1fa9efa14 fix: selection preset quality returning null 2025-11-04 13:45:23 +06:00
Kingkor Roy Tirtho
6272f376ea fix: quality preset initialization fails and audio source auth 2025-11-04 12:02:10 +06:00
Kingkor Roy Tirtho
4b5108e54e fix: streaming not working 2025-11-03 21:27:06 +06:00
Kingkor Roy Tirtho
6311831902 feat: move away from track source query and preferences audio quality and codec 2025-11-03 19:33:47 +06:00
Kingkor Roy Tirtho
99a84aa6dc chore: create sourced track from active audio source plugin 2025-11-03 13:32:48 +06:00
Kingkor Roy Tirtho
3bc296cf22 feat: add setting default audio source support 2025-10-25 23:23:27 +06:00
Kingkor Roy Tirtho
f6d9d64b7d feat(plugins): filter plugins by abilities in plugins page and show abilities as badge 2025-10-23 08:57:45 +06:00
Kingkor Roy Tirtho
439de5d7f7 feat: add plugin audio source models and api service 2025-10-19 13:48:53 +06:00
Kingkor Roy Tirtho
88699e9a3b fix: jiosaavn not working due to json signature change 2025-10-17 11:26:58 +06:00
Kingkor Roy Tirtho
348c2e931b fix: upgrade NewPipeExtractor to avoid unplayable streams 2025-10-17 10:27:00 +06:00
Kingkor Roy Tirtho
973ca20c8e fix(playback): play next not working 2025-09-20 20:25:51 +06:00
Kingkor Roy Tirtho
7d849b1430 fix: change plugin download directory to application support 2025-09-20 18:01:42 +06:00
Kingkor Roy Tirtho
e5150515f3 chore: cache dab music match source 2025-09-20 17:54:08 +06:00
Kingkor Roy Tirtho
66848c78c7 chore: add dab music option on getting started page and audio source quality label 2025-09-19 23:55:38 +06:00
Kingkor Roy Tirtho
3e34bc4be6 chore: streaming issue for mp3 2025-09-19 21:40:26 +06:00
Kingkor Roy Tirtho
cecb687592 feat(playback): add uncompressed flac playback support 2025-09-19 11:53:36 +06:00
Kingkor Roy Tirtho
e8a54d3209 feat(playback): add dab music source 2025-09-19 10:31:49 +06:00
Kingkor Roy Tirtho
ca6924f5a9 feat: show plugin source and set the only plugin as default if no plugin is there 2025-09-18 23:28:56 +06:00
Kingkor Roy Tirtho
0e48b7a337 website: update logo 2025-09-12 00:38:35 +06:00
Kingkor Roy Tirtho
60fbf66639 website: use locked version of astro-pagefine 2025-09-12 00:26:56 +06:00
Kingkor Roy Tirtho
97370712bc website: add back download buttons 2025-09-12 00:19:00 +06:00
Kingkor Roy Tirtho
c36e819ba3 Merge branch 'dev' into website 2025-09-12 00:11:03 +06:00
Kingkor Roy Tirtho
61d34963fa website: ads not showing up 2025-08-15 21:39:05 +06:00
184 changed files with 14809 additions and 7478 deletions

View File

@ -12,10 +12,10 @@ on:
type: boolean type: boolean
default: true default: true
jobs: jobs:
description: Jobs to run (flathub,aur,winget,chocolatey,playstore) description: Jobs to run (flathub,aur,winget,chocolatey)
required: true required: true
type: string type: string
default: "flathub,aur,winget,chocolatey,playstore" default: "flathub,aur,winget,chocolatey"
jobs: jobs:
flathub: flathub:
@ -112,26 +112,26 @@ jobs:
- name: Tagname (workflow dispatch) - name: Tagname (workflow dispatch)
run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV
- uses: robinraju/release-downloader@main # - uses: robinraju/release-downloader@main
with: # with:
repository: KRTirtho/spotube # repository: KRTirtho/spotube
tag: v${{ env.TAG_NAME }} # tag: v${{ env.TAG_NAME }}
tarBall: false # tarBall: false
zipBall: false # zipBall: false
out-file-path: dist # out-file-path: dist
fileName: "Spotube-playstore-all-arch.aab" # fileName: "Spotube-playstore-all-arch.aab"
- name: Create service-account.json # - name: Create service-account.json
run: | # run: |
echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json # echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
- name: Upload Android Release to Play Store # - name: Upload Android Release to Play Store
if: ${{!inputs.dry_run}} # if: ${{!inputs.dry_run}}
uses: r0adkll/upload-google-play@v1 # uses: r0adkll/upload-google-play@v1
with: # with:
serviceAccountJson: ./service-account.json # serviceAccountJson: ./service-account.json
releaseFiles: ./dist/Spotube-playstore-all-arch.aab # releaseFiles: ./dist/Spotube-playstore-all-arch.aab
packageName: oss.krtirtho.spotube # packageName: oss.krtirtho.spotube
track: production # track: production
status: draft # status: draft
releaseName: ${{ env.TAG_NAME }} # releaseName: ${{ env.TAG_NAME }}

View File

@ -49,7 +49,6 @@ jobs:
arch: all arch: all
files: | files: |
build/Spotube-android-all-arch.apk build/Spotube-android-all-arch.apk
build/Spotube-playstore-all-arch.aab
- os: windows-latest - os: windows-latest
platform: windows platform: windows
arch: x86 arch: x86
@ -77,6 +76,14 @@ jobs:
cache: true cache: true
git-source: https://github.com/flutter/flutter.git git-source: https://github.com/flutter/flutter.git
- name: free disk space
if: ${{ matrix.platform == 'android' }}
run: |
sudo swapoff -a
sudo rm -f /swapfile
sudo apt clean
docker rmi $(docker image ls -aq)
df -h
- name: Setup Java - name: Setup Java
if: ${{matrix.platform == 'android'}} if: ${{matrix.platform == 'android'}}
uses: actions/setup-java@v4 uses: actions/setup-java@v4

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
.history .history
.svn/ .svn/
# IntelliJ related # IntelliJ related
*.iml *.iml
*.ipr *.ipr

View File

@ -202,7 +202,6 @@ If you are curious, you can [read the reason of choosing this license](https://d
1. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube. 1. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube.
1. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader. 1. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader.
1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites. 1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites.
1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content
1. [LRCLib](https://lrclib.net/) - A public synced lyric API. 1. [LRCLib](https://lrclib.net/) - A public synced lyric API.
1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution 1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution
1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users 1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users

1
android/.gitignore vendored
View File

@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java
key.properties key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks
.kotlin

View File

@ -2,6 +2,7 @@ plugins {
id "com.android.application" id "com.android.application"
id "kotlin-android" id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin" id "dev.flutter.flutter-gradle-plugin"
id "org.jetbrains.kotlin.plugin.compose"
} }
def localProperties = new Properties() def localProperties = new Properties()

View File

@ -19,7 +19,8 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.7.0' apply false id "com.android.application" version '8.7.0' apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false id "org.jetbrains.kotlin.android" version "2.1.0" apply false
id "org.jetbrains.kotlin.plugin.compose" version "2.1.0" apply false
} }
include ':app' include ':app'

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -2,9 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:xml/xml.dart';
import '../../core/env.dart'; import '../../core/env.dart';
import 'common.dart'; import 'common.dart';
@ -24,39 +22,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
"flutter build apk --flavor ${CliEnv.channel.name}", "flutter build apk --flavor ${CliEnv.channel.name}",
); );
await dotEnvFile.writeAsString(
"\nENABLE_UPDATE_CHECK=0"
"\nHIDE_DONATIONS=1",
mode: FileMode.append,
);
final androidManifestFile = File(
join(cwd.path, "android", "app", "src", "main", "AndroidManifest.xml"));
final androidManifestXml =
XmlDocument.parse(await androidManifestFile.readAsString());
final deletingElement =
androidManifestXml.findAllElements("meta-data").firstWhereOrNull(
(el) =>
el.getAttribute("android:name") ==
"com.google.android.gms.car.application",
);
deletingElement?.parent?.children.remove(deletingElement);
await androidManifestFile.writeAsString(
androidManifestXml.toXmlString(pretty: true),
);
await shell.run(
"""
dart run build_runner clean
dart run build_runner build --delete-conflicting-outputs
flutter build appbundle --flavor ${CliEnv.channel.name}
""",
);
final ogApkFile = File( final ogApkFile = File(
join( join(
"build", "build",
@ -71,22 +36,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
join(cwd.path, "build", "Spotube-android-all-arch.apk"), join(cwd.path, "build", "Spotube-android-all-arch.apk"),
); );
final ogAppbundleFile = File(
join(
cwd.path,
"build",
"app",
"outputs",
"bundle",
"${CliEnv.channel.name}Release",
"app-${CliEnv.channel.name}-release.aab",
),
);
await ogAppbundleFile.copy(
join(cwd.path, "build", "Spotube-playstore-all-arch.aab"),
);
stdout.writeln("✅ Built Android Apk and Appbundle"); stdout.writeln("✅ Built Android Apk and Appbundle");
} }
} }

View File

@ -39,6 +39,11 @@ class InstallDependenciesCommand extends Command {
switch (argResults!.option("platform")) { switch (argResults!.option("platform")) {
case "windows": case "windows":
await shell.run(
"""
choco install innosetup -y
""",
);
break; break;
case "linux": case "linux":
await shell.run( await shell.run(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,5 @@
// dart format width=80
/// GENERATED CODE - DO NOT MODIFY BY HAND /// GENERATED CODE - DO NOT MODIFY BY HAND
/// ***************************************************** /// *****************************************************
/// FlutterGen /// FlutterGen
@ -5,7 +7,7 @@
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use // ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -64,9 +66,26 @@ class $AssetsImagesGen {
]; ];
} }
class $AssetsPluginsGen {
const $AssetsPluginsGen();
/// Directory path: assets/plugins/spotube-plugin-musicbrainz-listenbrainz
$AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen
get spotubePluginMusicbrainzListenbrainz =>
const $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen();
/// Directory path: assets/plugins/spotube-plugin-youtube-audio
$AssetsPluginsSpotubePluginYoutubeAudioGen get spotubePluginYoutubeAudio =>
const $AssetsPluginsSpotubePluginYoutubeAudioGen();
}
class $AssetsImagesLogosGen { class $AssetsImagesLogosGen {
const $AssetsImagesLogosGen(); const $AssetsImagesLogosGen();
/// File path: assets/images/logos/dab-music.png
AssetGenImage get dabMusic =>
const AssetGenImage('assets/images/logos/dab-music.png');
/// File path: assets/images/logos/invidious.jpg /// File path: assets/images/logos/invidious.jpg
AssetGenImage get invidious => AssetGenImage get invidious =>
const AssetGenImage('assets/images/logos/invidious.jpg'); const AssetGenImage('assets/images/logos/invidious.jpg');
@ -75,20 +94,39 @@ class $AssetsImagesLogosGen {
AssetGenImage get jiosaavn => AssetGenImage get jiosaavn =>
const AssetGenImage('assets/images/logos/jiosaavn.png'); const AssetGenImage('assets/images/logos/jiosaavn.png');
/// File path: assets/images/logos/songlink-transparent.png /// List of all assets
AssetGenImage get songlinkTransparent => List<AssetGenImage> get values => [dabMusic, invidious, jiosaavn];
const AssetGenImage('assets/images/logos/songlink-transparent.png'); }
class $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen {
const $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen();
/// File path: assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug
String get plugin =>
'assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug';
/// List of all assets /// List of all assets
List<AssetGenImage> get values => [invidious, jiosaavn, songlinkTransparent]; List<String> get values => [plugin];
}
class $AssetsPluginsSpotubePluginYoutubeAudioGen {
const $AssetsPluginsSpotubePluginYoutubeAudioGen();
/// File path: assets/plugins/spotube-plugin-youtube-audio/plugin.smplug
String get plugin =>
'assets/plugins/spotube-plugin-youtube-audio/plugin.smplug';
/// List of all assets
List<String> get values => [plugin];
} }
class Assets { class Assets {
Assets._(); const Assets._();
static const String license = 'LICENSE'; static const String license = 'LICENSE';
static const $AssetsBrandingGen branding = $AssetsBrandingGen(); static const $AssetsBrandingGen branding = $AssetsBrandingGen();
static const $AssetsImagesGen images = $AssetsImagesGen(); static const $AssetsImagesGen images = $AssetsImagesGen();
static const $AssetsPluginsGen plugins = $AssetsPluginsGen();
/// List of all assets /// List of all assets
static List<String> get values => [license]; static List<String> get values => [license];
@ -99,12 +137,14 @@ class AssetGenImage {
this._assetName, { this._assetName, {
this.size, this.size,
this.flavors = const {}, this.flavors = const {},
this.animation,
}); });
final String _assetName; final String _assetName;
final Size? size; final Size? size;
final Set<String> flavors; final Set<String> flavors;
final AssetGenImageAnimation? animation;
Image image({ Image image({
Key? key, Key? key,
@ -127,7 +167,7 @@ class AssetGenImage {
bool gaplessPlayback = true, bool gaplessPlayback = true,
bool isAntiAlias = false, bool isAntiAlias = false,
String? package, String? package,
FilterQuality filterQuality = FilterQuality.low, FilterQuality filterQuality = FilterQuality.medium,
int? cacheWidth, int? cacheWidth,
int? cacheHeight, int? cacheHeight,
}) { }) {
@ -174,3 +214,15 @@ class AssetGenImage {
String get keyName => _assetName; String get keyName => _assetName;
} }
class AssetGenImageAnimation {
const AssetGenImageAnimation({
required this.isAnimation,
required this.duration,
required this.frames,
});
final bool isAnimation;
final Duration duration;
final int frames;
}

View File

@ -1,3 +1,4 @@
// dart format width=80
/// GENERATED CODE - DO NOT MODIFY BY HAND /// GENERATED CODE - DO NOT MODIFY BY HAND
/// ***************************************************** /// *****************************************************
/// FlutterGen /// FlutterGen
@ -5,7 +6,7 @@
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use // ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import
class FontFamily { class FontFamily {
FontFamily._(); FontFamily._();

View File

@ -1,3 +1,4 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// ************************************************************************** // **************************************************************************
@ -59,10 +60,7 @@ import 'package:spotube/pages/track/track.dart' as _i35;
/// [_i1.AboutSpotubePage] /// [_i1.AboutSpotubePage]
class AboutSpotubeRoute extends _i41.PageRouteInfo<void> { class AboutSpotubeRoute extends _i41.PageRouteInfo<void> {
const AboutSpotubeRoute({List<_i41.PageRouteInfo>? children}) const AboutSpotubeRoute({List<_i41.PageRouteInfo>? children})
: super( : super(AboutSpotubeRoute.name, initialChildren: children);
AboutSpotubeRoute.name,
initialChildren: children,
);
static const String name = 'AboutSpotubeRoute'; static const String name = 'AboutSpotubeRoute';
@ -84,11 +82,7 @@ class AlbumRoute extends _i41.PageRouteInfo<AlbumRouteArgs> {
List<_i41.PageRouteInfo>? children, List<_i41.PageRouteInfo>? children,
}) : super( }) : super(
AlbumRoute.name, AlbumRoute.name,
args: AlbumRouteArgs( args: AlbumRouteArgs(key: key, id: id, album: album),
key: key,
id: id,
album: album,
),
rawPathParams: {'id': id}, rawPathParams: {'id': id},
initialChildren: children, initialChildren: children,
); );
@ -99,21 +93,13 @@ class AlbumRoute extends _i41.PageRouteInfo<AlbumRouteArgs> {
name, name,
builder: (data) { builder: (data) {
final args = data.argsAs<AlbumRouteArgs>(); final args = data.argsAs<AlbumRouteArgs>();
return _i2.AlbumPage( return _i2.AlbumPage(key: args.key, id: args.id, album: args.album);
key: args.key,
id: args.id,
album: args.album,
);
}, },
); );
} }
class AlbumRouteArgs { class AlbumRouteArgs {
const AlbumRouteArgs({ const AlbumRouteArgs({this.key, required this.id, required this.album});
this.key,
required this.id,
required this.album,
});
final _i42.Key? key; final _i42.Key? key;
@ -136,10 +122,7 @@ class ArtistRoute extends _i41.PageRouteInfo<ArtistRouteArgs> {
List<_i41.PageRouteInfo>? children, List<_i41.PageRouteInfo>? children,
}) : super( }) : super(
ArtistRoute.name, ArtistRoute.name,
args: ArtistRouteArgs( args: ArtistRouteArgs(artistId: artistId, key: key),
artistId: artistId,
key: key,
),
rawPathParams: {'id': artistId}, rawPathParams: {'id': artistId},
initialChildren: children, initialChildren: children,
); );
@ -151,20 +134,15 @@ class ArtistRoute extends _i41.PageRouteInfo<ArtistRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<ArtistRouteArgs>( final args = data.argsAs<ArtistRouteArgs>(
orElse: () => ArtistRouteArgs(artistId: pathParams.getString('id'))); orElse: () => ArtistRouteArgs(artistId: pathParams.getString('id')),
return _i3.ArtistPage(
args.artistId,
key: args.key,
); );
return _i3.ArtistPage(args.artistId, key: args.key);
}, },
); );
} }
class ArtistRouteArgs { class ArtistRouteArgs {
const ArtistRouteArgs({ const ArtistRouteArgs({required this.artistId, this.key});
required this.artistId,
this.key,
});
final String artistId; final String artistId;
@ -180,10 +158,7 @@ class ArtistRouteArgs {
/// [_i4.BlackListPage] /// [_i4.BlackListPage]
class BlackListRoute extends _i41.PageRouteInfo<void> { class BlackListRoute extends _i41.PageRouteInfo<void> {
const BlackListRoute({List<_i41.PageRouteInfo>? children}) const BlackListRoute({List<_i41.PageRouteInfo>? children})
: super( : super(BlackListRoute.name, initialChildren: children);
BlackListRoute.name,
initialChildren: children,
);
static const String name = 'BlackListRoute'; static const String name = 'BlackListRoute';
@ -199,10 +174,7 @@ class BlackListRoute extends _i41.PageRouteInfo<void> {
/// [_i5.ConnectControlPage] /// [_i5.ConnectControlPage]
class ConnectControlRoute extends _i41.PageRouteInfo<void> { class ConnectControlRoute extends _i41.PageRouteInfo<void> {
const ConnectControlRoute({List<_i41.PageRouteInfo>? children}) const ConnectControlRoute({List<_i41.PageRouteInfo>? children})
: super( : super(ConnectControlRoute.name, initialChildren: children);
ConnectControlRoute.name,
initialChildren: children,
);
static const String name = 'ConnectControlRoute'; static const String name = 'ConnectControlRoute';
@ -218,10 +190,7 @@ class ConnectControlRoute extends _i41.PageRouteInfo<void> {
/// [_i6.ConnectPage] /// [_i6.ConnectPage]
class ConnectRoute extends _i41.PageRouteInfo<void> { class ConnectRoute extends _i41.PageRouteInfo<void> {
const ConnectRoute({List<_i41.PageRouteInfo>? children}) const ConnectRoute({List<_i41.PageRouteInfo>? children})
: super( : super(ConnectRoute.name, initialChildren: children);
ConnectRoute.name,
initialChildren: children,
);
static const String name = 'ConnectRoute'; static const String name = 'ConnectRoute';
@ -237,10 +206,7 @@ class ConnectRoute extends _i41.PageRouteInfo<void> {
/// [_i7.GettingStartedPage] /// [_i7.GettingStartedPage]
class GettingStartedRoute extends _i41.PageRouteInfo<void> { class GettingStartedRoute extends _i41.PageRouteInfo<void> {
const GettingStartedRoute({List<_i41.PageRouteInfo>? children}) const GettingStartedRoute({List<_i41.PageRouteInfo>? children})
: super( : super(GettingStartedRoute.name, initialChildren: children);
GettingStartedRoute.name,
initialChildren: children,
);
static const String name = 'GettingStartedRoute'; static const String name = 'GettingStartedRoute';
@ -310,10 +276,7 @@ class HomeBrowseSectionItemsRouteArgs {
/// [_i9.HomePage] /// [_i9.HomePage]
class HomeRoute extends _i41.PageRouteInfo<void> { class HomeRoute extends _i41.PageRouteInfo<void> {
const HomeRoute({List<_i41.PageRouteInfo>? children}) const HomeRoute({List<_i41.PageRouteInfo>? children})
: super( : super(HomeRoute.name, initialChildren: children);
HomeRoute.name,
initialChildren: children,
);
static const String name = 'HomeRoute'; static const String name = 'HomeRoute';
@ -329,10 +292,7 @@ class HomeRoute extends _i41.PageRouteInfo<void> {
/// [_i10.LastFMLoginPage] /// [_i10.LastFMLoginPage]
class LastFMLoginRoute extends _i41.PageRouteInfo<void> { class LastFMLoginRoute extends _i41.PageRouteInfo<void> {
const LastFMLoginRoute({List<_i41.PageRouteInfo>? children}) const LastFMLoginRoute({List<_i41.PageRouteInfo>? children})
: super( : super(LastFMLoginRoute.name, initialChildren: children);
LastFMLoginRoute.name,
initialChildren: children,
);
static const String name = 'LastFMLoginRoute'; static const String name = 'LastFMLoginRoute';
@ -348,10 +308,7 @@ class LastFMLoginRoute extends _i41.PageRouteInfo<void> {
/// [_i11.LibraryPage] /// [_i11.LibraryPage]
class LibraryRoute extends _i41.PageRouteInfo<void> { class LibraryRoute extends _i41.PageRouteInfo<void> {
const LibraryRoute({List<_i41.PageRouteInfo>? children}) const LibraryRoute({List<_i41.PageRouteInfo>? children})
: super( : super(LibraryRoute.name, initialChildren: children);
LibraryRoute.name,
initialChildren: children,
);
static const String name = 'LibraryRoute'; static const String name = 'LibraryRoute';
@ -372,10 +329,7 @@ class LikedPlaylistRoute extends _i41.PageRouteInfo<LikedPlaylistRouteArgs> {
List<_i41.PageRouteInfo>? children, List<_i41.PageRouteInfo>? children,
}) : super( }) : super(
LikedPlaylistRoute.name, LikedPlaylistRoute.name,
args: LikedPlaylistRouteArgs( args: LikedPlaylistRouteArgs(key: key, playlist: playlist),
key: key,
playlist: playlist,
),
initialChildren: children, initialChildren: children,
); );
@ -385,19 +339,13 @@ class LikedPlaylistRoute extends _i41.PageRouteInfo<LikedPlaylistRouteArgs> {
name, name,
builder: (data) { builder: (data) {
final args = data.argsAs<LikedPlaylistRouteArgs>(); final args = data.argsAs<LikedPlaylistRouteArgs>();
return _i12.LikedPlaylistPage( return _i12.LikedPlaylistPage(key: args.key, playlist: args.playlist);
key: args.key,
playlist: args.playlist,
);
}, },
); );
} }
class LikedPlaylistRouteArgs { class LikedPlaylistRouteArgs {
const LikedPlaylistRouteArgs({ const LikedPlaylistRouteArgs({this.key, required this.playlist});
this.key,
required this.playlist,
});
final _i42.Key? key; final _i42.Key? key;
@ -471,10 +419,7 @@ class LocalLibraryRouteArgs {
/// [_i14.LogsPage] /// [_i14.LogsPage]
class LogsRoute extends _i41.PageRouteInfo<void> { class LogsRoute extends _i41.PageRouteInfo<void> {
const LogsRoute({List<_i41.PageRouteInfo>? children}) const LogsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(LogsRoute.name, initialChildren: children);
LogsRoute.name,
initialChildren: children,
);
static const String name = 'LogsRoute'; static const String name = 'LogsRoute';
@ -490,10 +435,7 @@ class LogsRoute extends _i41.PageRouteInfo<void> {
/// [_i15.LyricsPage] /// [_i15.LyricsPage]
class LyricsRoute extends _i41.PageRouteInfo<void> { class LyricsRoute extends _i41.PageRouteInfo<void> {
const LyricsRoute({List<_i41.PageRouteInfo>? children}) const LyricsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(LyricsRoute.name, initialChildren: children);
LyricsRoute.name,
initialChildren: children,
);
static const String name = 'LyricsRoute'; static const String name = 'LyricsRoute';
@ -514,10 +456,7 @@ class MiniLyricsRoute extends _i41.PageRouteInfo<MiniLyricsRouteArgs> {
List<_i41.PageRouteInfo>? children, List<_i41.PageRouteInfo>? children,
}) : super( }) : super(
MiniLyricsRoute.name, MiniLyricsRoute.name,
args: MiniLyricsRouteArgs( args: MiniLyricsRouteArgs(key: key, prevSize: prevSize),
key: key,
prevSize: prevSize,
),
initialChildren: children, initialChildren: children,
); );
@ -527,19 +466,13 @@ class MiniLyricsRoute extends _i41.PageRouteInfo<MiniLyricsRouteArgs> {
name, name,
builder: (data) { builder: (data) {
final args = data.argsAs<MiniLyricsRouteArgs>(); final args = data.argsAs<MiniLyricsRouteArgs>();
return _i16.MiniLyricsPage( return _i16.MiniLyricsPage(key: args.key, prevSize: args.prevSize);
key: args.key,
prevSize: args.prevSize,
);
}, },
); );
} }
class MiniLyricsRouteArgs { class MiniLyricsRouteArgs {
const MiniLyricsRouteArgs({ const MiniLyricsRouteArgs({this.key, required this.prevSize});
this.key,
required this.prevSize,
});
final _i44.Key? key; final _i44.Key? key;
@ -555,10 +488,7 @@ class MiniLyricsRouteArgs {
/// [_i17.PlayerLyricsPage] /// [_i17.PlayerLyricsPage]
class PlayerLyricsRoute extends _i41.PageRouteInfo<void> { class PlayerLyricsRoute extends _i41.PageRouteInfo<void> {
const PlayerLyricsRoute({List<_i41.PageRouteInfo>? children}) const PlayerLyricsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(PlayerLyricsRoute.name, initialChildren: children);
PlayerLyricsRoute.name,
initialChildren: children,
);
static const String name = 'PlayerLyricsRoute'; static const String name = 'PlayerLyricsRoute';
@ -574,10 +504,7 @@ class PlayerLyricsRoute extends _i41.PageRouteInfo<void> {
/// [_i18.PlayerQueuePage] /// [_i18.PlayerQueuePage]
class PlayerQueueRoute extends _i41.PageRouteInfo<void> { class PlayerQueueRoute extends _i41.PageRouteInfo<void> {
const PlayerQueueRoute({List<_i41.PageRouteInfo>? children}) const PlayerQueueRoute({List<_i41.PageRouteInfo>? children})
: super( : super(PlayerQueueRoute.name, initialChildren: children);
PlayerQueueRoute.name,
initialChildren: children,
);
static const String name = 'PlayerQueueRoute'; static const String name = 'PlayerQueueRoute';
@ -593,10 +520,7 @@ class PlayerQueueRoute extends _i41.PageRouteInfo<void> {
/// [_i19.PlayerTrackSourcesPage] /// [_i19.PlayerTrackSourcesPage]
class PlayerTrackSourcesRoute extends _i41.PageRouteInfo<void> { class PlayerTrackSourcesRoute extends _i41.PageRouteInfo<void> {
const PlayerTrackSourcesRoute({List<_i41.PageRouteInfo>? children}) const PlayerTrackSourcesRoute({List<_i41.PageRouteInfo>? children})
: super( : super(PlayerTrackSourcesRoute.name, initialChildren: children);
PlayerTrackSourcesRoute.name,
initialChildren: children,
);
static const String name = 'PlayerTrackSourcesRoute'; static const String name = 'PlayerTrackSourcesRoute';
@ -618,11 +542,7 @@ class PlaylistRoute extends _i41.PageRouteInfo<PlaylistRouteArgs> {
List<_i41.PageRouteInfo>? children, List<_i41.PageRouteInfo>? children,
}) : super( }) : super(
PlaylistRoute.name, PlaylistRoute.name,
args: PlaylistRouteArgs( args: PlaylistRouteArgs(key: key, id: id, playlist: playlist),
key: key,
id: id,
playlist: playlist,
),
rawPathParams: {'id': id}, rawPathParams: {'id': id},
initialChildren: children, initialChildren: children,
); );
@ -643,11 +563,7 @@ class PlaylistRoute extends _i41.PageRouteInfo<PlaylistRouteArgs> {
} }
class PlaylistRouteArgs { class PlaylistRouteArgs {
const PlaylistRouteArgs({ const PlaylistRouteArgs({this.key, required this.id, required this.playlist});
this.key,
required this.id,
required this.playlist,
});
final _i42.Key? key; final _i42.Key? key;
@ -665,10 +581,7 @@ class PlaylistRouteArgs {
/// [_i21.ProfilePage] /// [_i21.ProfilePage]
class ProfileRoute extends _i41.PageRouteInfo<void> { class ProfileRoute extends _i41.PageRouteInfo<void> {
const ProfileRoute({List<_i41.PageRouteInfo>? children}) const ProfileRoute({List<_i41.PageRouteInfo>? children})
: super( : super(ProfileRoute.name, initialChildren: children);
ProfileRoute.name,
initialChildren: children,
);
static const String name = 'ProfileRoute'; static const String name = 'ProfileRoute';
@ -684,10 +597,7 @@ class ProfileRoute extends _i41.PageRouteInfo<void> {
/// [_i22.RootAppPage] /// [_i22.RootAppPage]
class RootAppRoute extends _i41.PageRouteInfo<void> { class RootAppRoute extends _i41.PageRouteInfo<void> {
const RootAppRoute({List<_i41.PageRouteInfo>? children}) const RootAppRoute({List<_i41.PageRouteInfo>? children})
: super( : super(RootAppRoute.name, initialChildren: children);
RootAppRoute.name,
initialChildren: children,
);
static const String name = 'RootAppRoute'; static const String name = 'RootAppRoute';
@ -703,10 +613,7 @@ class RootAppRoute extends _i41.PageRouteInfo<void> {
/// [_i23.SearchPage] /// [_i23.SearchPage]
class SearchRoute extends _i41.PageRouteInfo<void> { class SearchRoute extends _i41.PageRouteInfo<void> {
const SearchRoute({List<_i41.PageRouteInfo>? children}) const SearchRoute({List<_i41.PageRouteInfo>? children})
: super( : super(SearchRoute.name, initialChildren: children);
SearchRoute.name,
initialChildren: children,
);
static const String name = 'SearchRoute'; static const String name = 'SearchRoute';
@ -775,10 +682,7 @@ class SettingsMetadataProviderFormRouteArgs {
/// [_i25.SettingsMetadataProviderPage] /// [_i25.SettingsMetadataProviderPage]
class SettingsMetadataProviderRoute extends _i41.PageRouteInfo<void> { class SettingsMetadataProviderRoute extends _i41.PageRouteInfo<void> {
const SettingsMetadataProviderRoute({List<_i41.PageRouteInfo>? children}) const SettingsMetadataProviderRoute({List<_i41.PageRouteInfo>? children})
: super( : super(SettingsMetadataProviderRoute.name, initialChildren: children);
SettingsMetadataProviderRoute.name,
initialChildren: children,
);
static const String name = 'SettingsMetadataProviderRoute'; static const String name = 'SettingsMetadataProviderRoute';
@ -794,10 +698,7 @@ class SettingsMetadataProviderRoute extends _i41.PageRouteInfo<void> {
/// [_i26.SettingsPage] /// [_i26.SettingsPage]
class SettingsRoute extends _i41.PageRouteInfo<void> { class SettingsRoute extends _i41.PageRouteInfo<void> {
const SettingsRoute({List<_i41.PageRouteInfo>? children}) const SettingsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(SettingsRoute.name, initialChildren: children);
SettingsRoute.name,
initialChildren: children,
);
static const String name = 'SettingsRoute'; static const String name = 'SettingsRoute';
@ -813,10 +714,7 @@ class SettingsRoute extends _i41.PageRouteInfo<void> {
/// [_i27.SettingsScrobblingPage] /// [_i27.SettingsScrobblingPage]
class SettingsScrobblingRoute extends _i41.PageRouteInfo<void> { class SettingsScrobblingRoute extends _i41.PageRouteInfo<void> {
const SettingsScrobblingRoute({List<_i41.PageRouteInfo>? children}) const SettingsScrobblingRoute({List<_i41.PageRouteInfo>? children})
: super( : super(SettingsScrobblingRoute.name, initialChildren: children);
SettingsScrobblingRoute.name,
initialChildren: children,
);
static const String name = 'SettingsScrobblingRoute'; static const String name = 'SettingsScrobblingRoute';
@ -832,10 +730,7 @@ class SettingsScrobblingRoute extends _i41.PageRouteInfo<void> {
/// [_i28.StatsAlbumsPage] /// [_i28.StatsAlbumsPage]
class StatsAlbumsRoute extends _i41.PageRouteInfo<void> { class StatsAlbumsRoute extends _i41.PageRouteInfo<void> {
const StatsAlbumsRoute({List<_i41.PageRouteInfo>? children}) const StatsAlbumsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(StatsAlbumsRoute.name, initialChildren: children);
StatsAlbumsRoute.name,
initialChildren: children,
);
static const String name = 'StatsAlbumsRoute'; static const String name = 'StatsAlbumsRoute';
@ -851,10 +746,7 @@ class StatsAlbumsRoute extends _i41.PageRouteInfo<void> {
/// [_i29.StatsArtistsPage] /// [_i29.StatsArtistsPage]
class StatsArtistsRoute extends _i41.PageRouteInfo<void> { class StatsArtistsRoute extends _i41.PageRouteInfo<void> {
const StatsArtistsRoute({List<_i41.PageRouteInfo>? children}) const StatsArtistsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(StatsArtistsRoute.name, initialChildren: children);
StatsArtistsRoute.name,
initialChildren: children,
);
static const String name = 'StatsArtistsRoute'; static const String name = 'StatsArtistsRoute';
@ -870,10 +762,7 @@ class StatsArtistsRoute extends _i41.PageRouteInfo<void> {
/// [_i30.StatsMinutesPage] /// [_i30.StatsMinutesPage]
class StatsMinutesRoute extends _i41.PageRouteInfo<void> { class StatsMinutesRoute extends _i41.PageRouteInfo<void> {
const StatsMinutesRoute({List<_i41.PageRouteInfo>? children}) const StatsMinutesRoute({List<_i41.PageRouteInfo>? children})
: super( : super(StatsMinutesRoute.name, initialChildren: children);
StatsMinutesRoute.name,
initialChildren: children,
);
static const String name = 'StatsMinutesRoute'; static const String name = 'StatsMinutesRoute';
@ -889,10 +778,7 @@ class StatsMinutesRoute extends _i41.PageRouteInfo<void> {
/// [_i31.StatsPage] /// [_i31.StatsPage]
class StatsRoute extends _i41.PageRouteInfo<void> { class StatsRoute extends _i41.PageRouteInfo<void> {
const StatsRoute({List<_i41.PageRouteInfo>? children}) const StatsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(StatsRoute.name, initialChildren: children);
StatsRoute.name,
initialChildren: children,
);
static const String name = 'StatsRoute'; static const String name = 'StatsRoute';
@ -908,10 +794,7 @@ class StatsRoute extends _i41.PageRouteInfo<void> {
/// [_i32.StatsPlaylistsPage] /// [_i32.StatsPlaylistsPage]
class StatsPlaylistsRoute extends _i41.PageRouteInfo<void> { class StatsPlaylistsRoute extends _i41.PageRouteInfo<void> {
const StatsPlaylistsRoute({List<_i41.PageRouteInfo>? children}) const StatsPlaylistsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(StatsPlaylistsRoute.name, initialChildren: children);
StatsPlaylistsRoute.name,
initialChildren: children,
);
static const String name = 'StatsPlaylistsRoute'; static const String name = 'StatsPlaylistsRoute';
@ -927,10 +810,7 @@ class StatsPlaylistsRoute extends _i41.PageRouteInfo<void> {
/// [_i33.StatsStreamFeesPage] /// [_i33.StatsStreamFeesPage]
class StatsStreamFeesRoute extends _i41.PageRouteInfo<void> { class StatsStreamFeesRoute extends _i41.PageRouteInfo<void> {
const StatsStreamFeesRoute({List<_i41.PageRouteInfo>? children}) const StatsStreamFeesRoute({List<_i41.PageRouteInfo>? children})
: super( : super(StatsStreamFeesRoute.name, initialChildren: children);
StatsStreamFeesRoute.name,
initialChildren: children,
);
static const String name = 'StatsStreamFeesRoute'; static const String name = 'StatsStreamFeesRoute';
@ -946,10 +826,7 @@ class StatsStreamFeesRoute extends _i41.PageRouteInfo<void> {
/// [_i34.StatsStreamsPage] /// [_i34.StatsStreamsPage]
class StatsStreamsRoute extends _i41.PageRouteInfo<void> { class StatsStreamsRoute extends _i41.PageRouteInfo<void> {
const StatsStreamsRoute({List<_i41.PageRouteInfo>? children}) const StatsStreamsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(StatsStreamsRoute.name, initialChildren: children);
StatsStreamsRoute.name,
initialChildren: children,
);
static const String name = 'StatsStreamsRoute'; static const String name = 'StatsStreamsRoute';
@ -970,10 +847,7 @@ class TrackRoute extends _i41.PageRouteInfo<TrackRouteArgs> {
List<_i41.PageRouteInfo>? children, List<_i41.PageRouteInfo>? children,
}) : super( }) : super(
TrackRoute.name, TrackRoute.name,
args: TrackRouteArgs( args: TrackRouteArgs(key: key, trackId: trackId),
key: key,
trackId: trackId,
),
rawPathParams: {'id': trackId}, rawPathParams: {'id': trackId},
initialChildren: children, initialChildren: children,
); );
@ -985,20 +859,15 @@ class TrackRoute extends _i41.PageRouteInfo<TrackRouteArgs> {
builder: (data) { builder: (data) {
final pathParams = data.inheritedPathParams; final pathParams = data.inheritedPathParams;
final args = data.argsAs<TrackRouteArgs>( final args = data.argsAs<TrackRouteArgs>(
orElse: () => TrackRouteArgs(trackId: pathParams.getString('id'))); orElse: () => TrackRouteArgs(trackId: pathParams.getString('id')),
return _i35.TrackPage(
key: args.key,
trackId: args.trackId,
); );
return _i35.TrackPage(key: args.key, trackId: args.trackId);
}, },
); );
} }
class TrackRouteArgs { class TrackRouteArgs {
const TrackRouteArgs({ const TrackRouteArgs({this.key, required this.trackId});
this.key,
required this.trackId,
});
final _i44.Key? key; final _i44.Key? key;
@ -1014,10 +883,7 @@ class TrackRouteArgs {
/// [_i36.UserAlbumsPage] /// [_i36.UserAlbumsPage]
class UserAlbumsRoute extends _i41.PageRouteInfo<void> { class UserAlbumsRoute extends _i41.PageRouteInfo<void> {
const UserAlbumsRoute({List<_i41.PageRouteInfo>? children}) const UserAlbumsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(UserAlbumsRoute.name, initialChildren: children);
UserAlbumsRoute.name,
initialChildren: children,
);
static const String name = 'UserAlbumsRoute'; static const String name = 'UserAlbumsRoute';
@ -1033,10 +899,7 @@ class UserAlbumsRoute extends _i41.PageRouteInfo<void> {
/// [_i37.UserArtistsPage] /// [_i37.UserArtistsPage]
class UserArtistsRoute extends _i41.PageRouteInfo<void> { class UserArtistsRoute extends _i41.PageRouteInfo<void> {
const UserArtistsRoute({List<_i41.PageRouteInfo>? children}) const UserArtistsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(UserArtistsRoute.name, initialChildren: children);
UserArtistsRoute.name,
initialChildren: children,
);
static const String name = 'UserArtistsRoute'; static const String name = 'UserArtistsRoute';
@ -1052,10 +915,7 @@ class UserArtistsRoute extends _i41.PageRouteInfo<void> {
/// [_i38.UserDownloadsPage] /// [_i38.UserDownloadsPage]
class UserDownloadsRoute extends _i41.PageRouteInfo<void> { class UserDownloadsRoute extends _i41.PageRouteInfo<void> {
const UserDownloadsRoute({List<_i41.PageRouteInfo>? children}) const UserDownloadsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(UserDownloadsRoute.name, initialChildren: children);
UserDownloadsRoute.name,
initialChildren: children,
);
static const String name = 'UserDownloadsRoute'; static const String name = 'UserDownloadsRoute';
@ -1071,10 +931,7 @@ class UserDownloadsRoute extends _i41.PageRouteInfo<void> {
/// [_i39.UserLocalLibraryPage] /// [_i39.UserLocalLibraryPage]
class UserLocalLibraryRoute extends _i41.PageRouteInfo<void> { class UserLocalLibraryRoute extends _i41.PageRouteInfo<void> {
const UserLocalLibraryRoute({List<_i41.PageRouteInfo>? children}) const UserLocalLibraryRoute({List<_i41.PageRouteInfo>? children})
: super( : super(UserLocalLibraryRoute.name, initialChildren: children);
UserLocalLibraryRoute.name,
initialChildren: children,
);
static const String name = 'UserLocalLibraryRoute'; static const String name = 'UserLocalLibraryRoute';
@ -1090,10 +947,7 @@ class UserLocalLibraryRoute extends _i41.PageRouteInfo<void> {
/// [_i40.UserPlaylistsPage] /// [_i40.UserPlaylistsPage]
class UserPlaylistsRoute extends _i41.PageRouteInfo<void> { class UserPlaylistsRoute extends _i41.PageRouteInfo<void> {
const UserPlaylistsRoute({List<_i41.PageRouteInfo>? children}) const UserPlaylistsRoute({List<_i41.PageRouteInfo>? children})
: super( : super(UserPlaylistsRoute.name, initialChildren: children);
UserPlaylistsRoute.name,
initialChildren: children,
);
static const String name = 'UserPlaylistsRoute'; static const String name = 'UserPlaylistsRoute';

View File

@ -80,6 +80,7 @@ abstract class SpotubeIcons {
static const hoverOff = Icons.back_hand_outlined; static const hoverOff = Icons.back_hand_outlined;
static const dragHandle = Icons.drag_indicator; static const dragHandle = Icons.drag_indicator;
static const lightning = Icons.flash_on_rounded; static const lightning = Icons.flash_on_rounded;
static const lightningOutlined = FeatherIcons.zap;
static const colorSync = FeatherIcons.activity; static const colorSync = FeatherIcons.activity;
static const language = FeatherIcons.globe; static const language = FeatherIcons.globe;
static const error = FeatherIcons.alertTriangle; static const error = FeatherIcons.alertTriangle;
@ -134,7 +135,7 @@ abstract class SpotubeIcons {
static const list = FeatherIcons.list; static const list = FeatherIcons.list;
static const device = FeatherIcons.smartphone; static const device = FeatherIcons.smartphone;
static const engine = FeatherIcons.server; static const engine = FeatherIcons.server;
static const extensions = FeatherIcons.package; static const extensions = Icons.extension_rounded;
static const message = FeatherIcons.send; static const message = FeatherIcons.send;
static const upload = FeatherIcons.uploadCloud; static const upload = FeatherIcons.uploadCloud;
static const plugin = Icons.extension_outlined; static const plugin = Icons.extension_outlined;

View File

@ -0,0 +1,69 @@
import 'package:flutter/services.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LinkOpenPermissionDialog extends StatelessWidget {
final String? href;
const LinkOpenPermissionDialog({super.key, this.href});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 450),
child: AlertDialog(
title: Row(
spacing: 8,
children: [
const Icon(SpotubeIcons.warning),
Text(context.l10n.open_link_in_browser),
],
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text:
"${context.l10n.do_you_want_to_open_the_following_link}:\n",
),
if (href != null)
TextSpan(
text: "$href\n\n",
style: const TextStyle(color: Colors.blue),
),
TextSpan(text: context.l10n.unsafe_url_warning),
],
),
),
actions: [
Button.ghost(
onPressed: () => Navigator.of(context).pop(false),
child: Text(context.l10n.cancel),
),
Button.ghost(
onPressed: () {
if (href != null) {
Clipboard.setData(ClipboardData(text: href!));
}
Navigator.of(context).pop(false);
},
child: Text(context.l10n.copy_link),
),
Button.destructive(
onPressed: () {
if (href != null) {
launchUrlString(
href!,
mode: LaunchMode.externalApplication,
);
}
Navigator.of(context).pop(true);
},
child: Text(context.l10n.open),
),
],
),
);
}
}

View File

@ -12,13 +12,12 @@ class ReplaceDownloadedDialog extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final groupValue = ref.watch(replaceDownloadedFileState);
final replaceAll = ref.watch(replaceDownloadedFileState); final replaceAll = ref.watch(replaceDownloadedFileState);
return AlertDialog( return AlertDialog(
title: Text(context.l10n.track_exists(track.name)), title: Text(context.l10n.track_exists(track.name)),
content: RadioGroup( content: RadioGroup(
value: groupValue, value: replaceAll,
onChanged: (value) { onChanged: (value) {
ref.read(replaceDownloadedFileState.notifier).state = value; ref.read(replaceDownloadedFileState.notifier).state = value;
}, },

View File

@ -7,8 +7,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/provider/server/track_sources.dart';
class TrackDetailsDialog extends HookConsumerWidget { class TrackDetailsDialog extends HookConsumerWidget {
final SpotubeFullTrackObject track; final SpotubeFullTrackObject track;
@ -21,8 +20,7 @@ class TrackDetailsDialog extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final sourcedTrack = final sourcedTrack = ref.read(sourcedTrackProvider(track));
ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track)));
final detailsMap = { final detailsMap = {
context.l10n.title: track.name, context.l10n.title: track.name,
@ -39,8 +37,7 @@ class TrackDetailsDialog extends HookConsumerWidget {
// style: const TextStyle(color: Colors.blue), // style: const TextStyle(color: Colors.blue),
// ), // ),
context.l10n.duration: sourcedTrack.asData != null context.l10n.duration: sourcedTrack.asData != null
? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs) ? sourcedTrack.asData!.value.info.duration.toHumanReadableString()
.toHumanReadableString()
: Duration(milliseconds: track.durationMs).toHumanReadableString(), : Duration(milliseconds: track.durationMs).toHumanReadableString(),
if (track.album.releaseDate != null) if (track.album.releaseDate != null)
context.l10n.released: track.album.releaseDate, context.l10n.released: track.album.releaseDate,
@ -57,7 +54,7 @@ class TrackDetailsDialog extends HookConsumerWidget {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
context.l10n.channel: Text(sourceInfo.artists), context.l10n.channel: Text(sourceInfo.artists.join(", ")),
if (sourcedTrack.asData?.value.url != null) if (sourcedTrack.asData?.value.url != null)
context.l10n.streamUrl: Hyperlink( context.l10n.streamUrl: Hyperlink(
sourcedTrack.asData!.value.url ?? "", sourcedTrack.asData!.value.url ?? "",

View File

@ -1,9 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/dialogs/link_open_permission_dialog.dart';
import 'package:spotube/extensions/context.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class AppMarkdown extends StatelessWidget { class AppMarkdown extends StatelessWidget {
@ -28,61 +26,7 @@ class AppMarkdown extends StatelessWidget {
final allowOpeningLink = await showDialog<bool>( final allowOpeningLink = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
return ConstrainedBox( return LinkOpenPermissionDialog(href: href);
constraints: const BoxConstraints(maxWidth: 450),
child: AlertDialog(
title: Row(
spacing: 8,
children: [
const Icon(SpotubeIcons.warning),
Text(context.l10n.open_link_in_browser),
],
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text:
"${context.l10n.do_you_want_to_open_the_following_link}:\n",
),
if (href != null)
TextSpan(
text: "$href\n\n",
style: const TextStyle(color: Colors.blue),
),
TextSpan(text: context.l10n.unsafe_url_warning),
],
),
),
actions: [
Button.ghost(
onPressed: () => Navigator.of(context).pop(false),
child: Text(context.l10n.cancel),
),
Button.ghost(
onPressed: () {
if (href != null) {
Clipboard.setData(ClipboardData(text: href));
}
Navigator.of(context).pop(false);
},
child: Text(context.l10n.copy_link),
),
Button.destructive(
onPressed: () {
if (href != null) {
launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}
Navigator.of(context).pop(true);
},
child: Text(context.l10n.open),
),
],
),
);
}, },
); );

View File

@ -8,12 +8,10 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
ToastOverlay showToastForAction( ToastOverlay showToastForAction(
BuildContext context, BuildContext context,
@ -70,8 +68,6 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
final downloader = ref.watch(downloadManagerProvider.notifier); final downloader = ref.watch(downloadManagerProvider.notifier);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryActionsProvider); final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final audioSource =
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
final state = ref.watch(presentationStateProvider(options.collection)); final state = ref.watch(presentationStateProvider(options.collection));
final notifier = final notifier =
@ -85,16 +81,15 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
}) async { }) async {
final fullTrackObjects = final fullTrackObjects =
tracks.whereType<SpotubeFullTrackObject>().toList(); tracks.whereType<SpotubeFullTrackObject>().toList();
final confirmed = audioSource == AudioSource.piped || final confirmed = await showDialog<bool>(
(await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
return const ConfirmDownloadDialog(); return const ConfirmDownloadDialog();
}, },
) ?? ) ??
false); false;
if (confirmed != true) return; if (confirmed != true) return;
downloader.batchAddToQueue(fullTrackObjects); downloader.addAllToQueue(fullTrackObjects);
notifier.deselectAllTracks(); notifier.deselectAllTracks();
if (!context.mounted) return; if (!context.mounted) return;
showToastForAction(context, action, fullTrackObjects.length); showToastForAction(context, action, fullTrackObjects.length);

View File

@ -1,10 +1,8 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
@ -35,7 +33,6 @@ class TrackOptions extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final ThemeData(:colorScheme) = Theme.of(context);
final trackOptionActions = ref.watch(trackOptionActionsProvider(track)); final trackOptionActions = ref.watch(trackOptionActionsProvider(track));
final ( final (
@ -45,7 +42,7 @@ class TrackOptions extends HookConsumerWidget {
:isActiveTrack, :isActiveTrack,
:isAuthenticated, :isAuthenticated,
:isLiked, :isLiked,
:progressNotifier :downloadTask
) = ref.watch(trackOptionsStateProvider(track)); ) = ref.watch(trackOptionsStateProvider(track));
final isLocalTrack = track is SpotubeLocalTrackObject; final isLocalTrack = track is SpotubeLocalTrackObject;
@ -59,7 +56,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.delete, TrackOptionValue.delete,
playlistId, playlistId,
); );
@ -73,7 +70,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.album, TrackOptionValue.album,
playlistId, playlistId,
); );
@ -97,7 +94,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.addToQueue, TrackOptionValue.addToQueue,
playlistId, playlistId,
); );
@ -110,7 +107,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.playNext, TrackOptionValue.playNext,
playlistId, playlistId,
); );
@ -124,7 +121,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.removeFromQueue, TrackOptionValue.removeFromQueue,
playlistId, playlistId,
); );
@ -139,7 +136,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.favorite, TrackOptionValue.favorite,
playlistId, playlistId,
); );
@ -162,7 +159,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.startRadio, TrackOptionValue.startRadio,
playlistId, playlistId,
); );
@ -175,7 +172,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.addToPlaylist, TrackOptionValue.addToPlaylist,
playlistId, playlistId,
); );
@ -190,7 +187,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.removeFromPlaylist, TrackOptionValue.removeFromPlaylist,
playlistId, playlistId,
); );
@ -204,7 +201,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.download, TrackOptionValue.download,
playlistId, playlistId,
); );
@ -212,12 +209,19 @@ class TrackOptions extends HookConsumerWidget {
}, },
enabled: !isInDownloadQueue, enabled: !isInDownloadQueue,
leading: isInDownloadQueue leading: isInDownloadQueue
? HookBuilder(builder: (context) { ? StreamBuilder(
final progress = useListenable(progressNotifier); stream: downloadTask?.downloadedBytesStream,
builder: (context, snapshot) {
final progress = downloadTask?.totalSizeBytes == null ||
downloadTask?.totalSizeBytes == 0
? 0
: (snapshot.data ?? 0) /
downloadTask!.totalSizeBytes!;
return CircularProgressIndicator( return CircularProgressIndicator(
value: progress?.value, value: progress.toDouble(),
); );
}) },
)
: const Icon(SpotubeIcons.download), : const Icon(SpotubeIcons.download),
title: Text(context.l10n.download_track), title: Text(context.l10n.download_track),
), ),
@ -226,7 +230,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.blacklist, TrackOptionValue.blacklist,
playlistId, playlistId,
); );
@ -250,7 +254,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.share, TrackOptionValue.share,
playlistId, playlistId,
); );
@ -264,25 +268,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.songlink,
playlistId,
);
onTapItem?.call();
},
leading: Assets.images.logos.songlinkTransparent.image(
width: 22,
height: 22,
color: colorScheme.foreground.withValues(alpha: 0.5),
),
title: Text(context.l10n.song_link),
),
if (!isLocalTrack)
ButtonTile(
style: ButtonVariance.menu,
onPressed: () async {
await trackOptionActions.action(
context,
TrackOptionValue.details, TrackOptionValue.details,
playlistId, playlistId,
); );

View File

@ -39,6 +39,7 @@ class TrackTile extends HookConsumerWidget {
final int? index; final int? index;
final SpotubeTrackObject track; final SpotubeTrackObject track;
final bool selected; final bool selected;
final bool selectionMode;
final ValueChanged<bool?>? onChanged; final ValueChanged<bool?>? onChanged;
final Future<void> Function()? onTap; final Future<void> Function()? onTap;
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
@ -53,6 +54,7 @@ class TrackTile extends HookConsumerWidget {
this.index, this.index,
required this.track, required this.track,
this.selected = false, this.selected = false,
this.selectionMode = false,
required this.playlist, required this.playlist,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
@ -81,6 +83,12 @@ class TrackTile extends HookConsumerWidget {
[track.album.images], [track.album.images],
); );
// Treat either explicit selectionMode or presence of onChanged as selection
// context. Some lists enable selection by providing `onChanged` without
// toggling a dedicated `selectionMode` flag (e.g. playlists), so we must
// disable inner navigation in both cases.
final effectiveSelection = selectionMode || onChanged != null;
return LayoutBuilder(builder: (context, constrains) { return LayoutBuilder(builder: (context, constrains) {
return Listener( return Listener(
onPointerDown: (event) { onPointerDown: (event) {
@ -222,6 +230,8 @@ class TrackTile extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
flex: 6, flex: 6,
child: AbsorbPointer(
absorbing: selectionMode,
child: switch (track) { child: switch (track) {
SpotubeLocalTrackObject() => Text( SpotubeLocalTrackObject() => Text(
track.name, track.name,
@ -237,7 +247,9 @@ class TrackTile extends HookConsumerWidget {
padding: (context, states, value) => padding: (context, states, value) =>
EdgeInsets.zero, EdgeInsets.zero,
), ),
onPressed: () { onPressed: effectiveSelection
? null
: () {
context context
.navigateTo(TrackRoute(trackId: track.id)); .navigateTo(TrackRoute(trackId: track.id));
}, },
@ -252,6 +264,7 @@ class TrackTile extends HookConsumerWidget {
), ),
}, },
), ),
),
if (constrains.mdAndUp) ...[ if (constrains.mdAndUp) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
@ -288,9 +301,13 @@ class TrackTile extends HookConsumerWidget {
: ClipRect( : ClipRect(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 40), constraints: const BoxConstraints(maxHeight: 40),
child: AbsorbPointer(
absorbing: effectiveSelection,
child: ArtistLink( child: ArtistLink(
artists: track.artists, artists: track.artists,
onOverflowArtistClick: () { onOverflowArtistClick: effectiveSelection
? () {}
: () {
context.navigateTo( context.navigateTo(
TrackRoute(trackId: track.id), TrackRoute(trackId: track.id),
); );
@ -299,6 +316,7 @@ class TrackTile extends HookConsumerWidget {
), ),
), ),
), ),
),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

168
lib/extensions/dio.dart Normal file
View File

@ -0,0 +1,168 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
extension ChunkDownloaderDioExtension on Dio {
Future<Response> chunkDownload(
String urlPath,
dynamic savePath, {
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
bool deleteOnError = true,
FileAccessMode fileAccessMode = FileAccessMode.write,
String lengthHeader = Headers.contentLengthHeader,
Object? data,
Options? options,
int connections = 4,
}) async {
final targetFile = File(savePath.toString());
final tempRootDir = await getTemporaryDirectory();
final tempSaveDir = Directory(
join(
tempRootDir.path,
'Spotube',
'.chunk_dl_${targetFile.uri.pathSegments.last}',
),
);
if (await tempSaveDir.exists()) await tempSaveDir.delete(recursive: true);
await tempSaveDir.create(recursive: true);
try {
int? totalLength;
bool supportsRange = false;
Response? headResp;
try {
headResp = await head(
urlPath,
queryParameters: queryParameters,
options: Options(
headers: {'Range': 'bytes=0-0'},
followRedirects: true,
),
);
} catch (_) {
// Some servers reject HEAD -> ignore
}
final lengthStr = headResp?.headers[lengthHeader]?.first;
if (lengthStr != null) {
final parsed = int.tryParse(lengthStr);
if (parsed != null && parsed > 1) {
totalLength = parsed;
}
}
supportsRange = headResp?.statusCode == 206 ||
headResp?.headers.value(HttpHeaders.acceptRangesHeader) == 'bytes';
if (totalLength == null || totalLength <= 1) {
final resp = await get<ResponseBody>(
urlPath,
options: Options(
responseType: ResponseType.stream,
),
queryParameters: queryParameters,
cancelToken: cancelToken,
);
final len = int.tryParse(resp.headers[lengthHeader]?.first ?? '');
if (len == null || len <= 1) {
// cant safely chunk fallback
return download(
urlPath,
savePath,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
deleteOnError: deleteOnError,
options: options,
data: data,
);
}
totalLength = len;
supportsRange =
resp.headers.value(HttpHeaders.acceptRangesHeader)?.toLowerCase() ==
'bytes';
}
if (!supportsRange || connections <= 1) {
return download(
urlPath,
savePath,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
deleteOnError: deleteOnError,
options: options,
data: data,
);
}
final chunkSize = (totalLength / connections).ceil();
int downloaded = 0;
final partFiles = List.generate(
connections,
(i) => File(join(tempSaveDir.path, 'part_$i')),
);
final futures = List.generate(connections, (i) async {
final start = i * chunkSize;
final end = (i + 1) * chunkSize - 1;
if (start >= totalLength!) return;
final resp = await get<ResponseBody>(
urlPath,
options: Options(
responseType: ResponseType.stream,
headers: {'Range': 'bytes=$start-$end'},
),
queryParameters: queryParameters,
cancelToken: cancelToken,
);
final file = partFiles[i];
if (await file.exists()) await file.delete();
await file.create(recursive: true);
final sink = file.openWrite();
await for (final chunk in resp.data!.stream) {
sink.add(chunk);
downloaded += chunk.length;
onReceiveProgress?.call(downloaded, totalLength);
}
await sink.close();
});
await Future.wait(futures);
final targetSink = targetFile.openWrite();
for (final f in partFiles) {
await targetSink.addStream(f.openRead());
}
await targetSink.close();
await tempSaveDir.delete(recursive: true);
return Response(
requestOptions: RequestOptions(path: urlPath),
data: targetFile,
statusCode: 200,
statusMessage: 'Chunked download completed ($connections connections)',
);
} catch (e) {
if (deleteOnError) {
if (await targetFile.exists()) await targetFile.delete();
if (await tempSaveDir.exists()) {
await tempSaveDir.delete(recursive: true);
}
}
rethrow;
}
}
}

View File

@ -434,7 +434,10 @@
"update_available": "Update available", "update_available": "Update available",
"supports_scrobbling": "Supports scrobbling", "supports_scrobbling": "Supports scrobbling",
"plugin_scrobbling_info": "This plugin scrobbles your music to generate your listening history.", "plugin_scrobbling_info": "This plugin scrobbles your music to generate your listening history.",
"default_plugin": "Default", "default_metadata_source": "Default metadata source",
"set_default_metadata_source": "Set default metadata source",
"default_audio_source": "Default audio source",
"set_default_audio_source": "Set default audio source",
"set_default": "Set default", "set_default": "Set default",
"support": "Support", "support": "Support",
"support_plugin_development": "Support plugin development", "support_plugin_development": "Support plugin development",
@ -452,14 +455,17 @@
"disclaimer": "Disclaimer", "disclaimer": "Disclaimer",
"third_party_plugin_dmca_notice": "The Spotube team does not hold any responsibility (including legal) for any \"Third-party\" plugins.\nPlease use them at your own risk. For any bugs/issues, please report them to the plugin repository.\n\nIf any \"Third-party\" plugin is breaking ToS/DMCA of any service/legal entity, please ask the \"Third-party\" plugin author or the hosting platform .e.g GitHub/Codeberg to take action. Above listed (\"Third-party\" labelled) are all public/community maintained plugins. We're not curating them, so we cannot take any action on them.\n\n", "third_party_plugin_dmca_notice": "The Spotube team does not hold any responsibility (including legal) for any \"Third-party\" plugins.\nPlease use them at your own risk. For any bugs/issues, please report them to the plugin repository.\n\nIf any \"Third-party\" plugin is breaking ToS/DMCA of any service/legal entity, please ask the \"Third-party\" plugin author or the hosting platform .e.g GitHub/Codeberg to take action. Above listed (\"Third-party\" labelled) are all public/community maintained plugins. We're not curating them, so we cannot take any action on them.\n\n",
"input_does_not_match_format": "Input doesn't match the required format", "input_does_not_match_format": "Input doesn't match the required format",
"metadata_provider_plugins": "Metadata Provider Plugins", "plugins": "Plugins",
"paste_plugin_download_url": "Paste download url or GitHub/Codeberg repo url or direct link to .smplug file", "paste_plugin_download_url": "Paste download url or GitHub/Codeberg repo url or direct link to .smplug file",
"download_and_install_plugin_from_url": "Download and install plugin from url", "download_and_install_plugin_from_url": "Download and install plugin from url",
"failed_to_add_plugin_error": "Failed to add plugin: {error}", "failed_to_add_plugin_error": "Failed to add plugin: {error}",
"upload_plugin_from_file": "Upload plugin from file", "upload_plugin_from_file": "Upload plugin from file",
"installed": "Installed", "installed": "Installed",
"available_plugins": "Available plugins", "available_plugins": "Available plugins",
"configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider", "configure_plugins": "Configure your own metadata provider and audio source plugins",
"audio_scrobblers": "Audio Scrobblers", "audio_scrobblers": "Audio Scrobblers",
"scrobbling": "Scrobbling" "scrobbling": "Scrobbling",
"source": "Source: ",
"uncompressed": "Uncompressed",
"dab_music_source_description": "For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching."
} }

View File

@ -2763,11 +2763,29 @@ abstract class AppLocalizations {
/// **'This plugin scrobbles your music to generate your listening history.'** /// **'This plugin scrobbles your music to generate your listening history.'**
String get plugin_scrobbling_info; String get plugin_scrobbling_info;
/// No description provided for @default_plugin. /// No description provided for @default_metadata_source.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Default'** /// **'Default metadata source'**
String get default_plugin; String get default_metadata_source;
/// No description provided for @set_default_metadata_source.
///
/// In en, this message translates to:
/// **'Set default metadata source'**
String get set_default_metadata_source;
/// No description provided for @default_audio_source.
///
/// In en, this message translates to:
/// **'Default audio source'**
String get default_audio_source;
/// No description provided for @set_default_audio_source.
///
/// In en, this message translates to:
/// **'Set default audio source'**
String get set_default_audio_source;
/// No description provided for @set_default. /// No description provided for @set_default.
/// ///
@ -2871,11 +2889,11 @@ abstract class AppLocalizations {
/// **'Input doesn\'t match the required format'** /// **'Input doesn\'t match the required format'**
String get input_does_not_match_format; String get input_does_not_match_format;
/// No description provided for @metadata_provider_plugins. /// No description provided for @plugins.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Metadata Provider Plugins'** /// **'Plugins'**
String get metadata_provider_plugins; String get plugins;
/// No description provided for @paste_plugin_download_url. /// No description provided for @paste_plugin_download_url.
/// ///
@ -2913,11 +2931,11 @@ abstract class AppLocalizations {
/// **'Available plugins'** /// **'Available plugins'**
String get available_plugins; String get available_plugins;
/// No description provided for @configure_your_own_metadata_plugin. /// No description provided for @configure_plugins.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Configure your own playlist/album/artist/feed metadata provider'** /// **'Configure your own metadata provider and audio source plugins'**
String get configure_your_own_metadata_plugin; String get configure_plugins;
/// No description provided for @audio_scrobblers. /// No description provided for @audio_scrobblers.
/// ///
@ -2930,6 +2948,24 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Scrobbling'** /// **'Scrobbling'**
String get scrobbling; String get scrobbling;
/// No description provided for @source.
///
/// In en, this message translates to:
/// **'Source: '**
String get source;
/// No description provided for @uncompressed.
///
/// In en, this message translates to:
/// **'Uncompressed'**
String get uncompressed;
/// No description provided for @dab_music_source_description.
///
/// In en, this message translates to:
/// **'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'**
String get dab_music_source_description;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@ -1443,7 +1443,16 @@ class AppLocalizationsAr extends AppLocalizations {
'تقوم هذه الإضافة بتتبع مقاطعك الموسيقية لإنشاء سجل الاستماع الخاص بك.'; 'تقوم هذه الإضافة بتتبع مقاطعك الموسيقية لإنشاء سجل الاستماع الخاص بك.';
@override @override
String get default_plugin => 'الافتراضي'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'تعيين كافتراضي'; String get set_default => 'تعيين كافتراضي';
@ -1504,7 +1513,7 @@ class AppLocalizationsAr extends AppLocalizations {
'المدخل لا يتوافق مع التنسيق المطلوب'; 'المدخل لا يتوافق مع التنسيق المطلوب';
@override @override
String get metadata_provider_plugins => 'إضافات مزود البيانات'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1529,12 +1538,22 @@ class AppLocalizationsAr extends AppLocalizations {
String get available_plugins => 'الإضافات المتوفّرة'; String get available_plugins => 'الإضافات المتوفّرة';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'تهيئة مزوّد بيانات للقائمة/الألبوم/الفنان/المصدر خاص بك'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'أجهزة تتبع الصوت'; String get audio_scrobblers => 'أجهزة تتبع الصوت';
@override @override
String get scrobbling => 'التتبع'; String get scrobbling => 'التتبع';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1443,7 +1443,16 @@ class AppLocalizationsBn extends AppLocalizations {
'এই প্লাগইনটি আপনার সঙ্গীত স্ক্রোব্বল করে আপনার শোনা ইতিহাস তৈরি করে।'; 'এই প্লাগইনটি আপনার সঙ্গীত স্ক্রোব্বল করে আপনার শোনা ইতিহাস তৈরি করে।';
@override @override
String get default_plugin => 'ডিফল্ট'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'ডিফল্ট হিসাবে নির্ধারণ করুন'; String get set_default => 'ডিফল্ট হিসাবে নির্ধারণ করুন';
@ -1505,7 +1514,7 @@ class AppLocalizationsBn extends AppLocalizations {
'ইনপুট প্রয়োজনীয় ফরম্যাটের সাথে মেলে না'; 'ইনপুট প্রয়োজনীয় ফরম্যাটের সাথে মেলে না';
@override @override
String get metadata_provider_plugins => 'মেটাডেটা প্রদানকারী প্লাগইনসমূহ'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1530,12 +1539,22 @@ class AppLocalizationsBn extends AppLocalizations {
String get available_plugins => 'উপলব্ধ প্লাগইনগুলো'; String get available_plugins => 'উপলব্ধ প্লাগইনগুলো';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'নিজস্ব প্লেলিস্ট/অ্যালবাম/শিল্পী/ফিড মেটাডেটা প্রদানকারী কনফিগার করুন'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'অডিও স্ক্রোব্বলার্স'; String get audio_scrobblers => 'অডিও স্ক্রোব্বলার্স';
@override @override
String get scrobbling => 'স্ক্রোব্বলিং'; String get scrobbling => 'স্ক্রোব্বলিং';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1450,7 +1450,16 @@ class AppLocalizationsCa extends AppLocalizations {
'Aquest complement fa scrobbling de la teva música per generar lhistorial descoltes.'; 'Aquest complement fa scrobbling de la teva música per generar lhistorial descoltes.';
@override @override
String get default_plugin => 'Predeterminat'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Establir com a predeterminat'; String get set_default => 'Establir com a predeterminat';
@ -1514,8 +1523,7 @@ class AppLocalizationsCa extends AppLocalizations {
'Lentrada no coincideix amb el format requerit'; 'Lentrada no coincideix amb el format requerit';
@override @override
String get metadata_provider_plugins => String get plugins => 'Plugins';
'Complements de proveïdor de metadades';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1540,12 +1548,22 @@ class AppLocalizationsCa extends AppLocalizations {
String get available_plugins => 'Complements disponibles'; String get available_plugins => 'Complements disponibles';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Configura el teu propi proveïdor de metadades per llistes/reproduccions àlbum/artista/flux'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Scrobblers dàudio'; String get audio_scrobblers => 'Scrobblers dàudio';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1442,7 +1442,16 @@ class AppLocalizationsCs extends AppLocalizations {
'Tento plugin scrobbles vaši hudbu pro vytvoření historie poslechů.'; 'Tento plugin scrobbles vaši hudbu pro vytvoření historie poslechů.';
@override @override
String get default_plugin => 'Výchozí'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Nastavit jako výchozí'; String get set_default => 'Nastavit jako výchozí';
@ -1505,7 +1514,7 @@ class AppLocalizationsCs extends AppLocalizations {
'Vstup neodpovídá požadovanému formátu'; 'Vstup neodpovídá požadovanému formátu';
@override @override
String get metadata_provider_plugins => 'Pluginy poskytovatelů metadat'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1530,12 +1539,22 @@ class AppLocalizationsCs extends AppLocalizations {
String get available_plugins => 'Dostupné pluginy'; String get available_plugins => 'Dostupné pluginy';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Nakonfigurujte si vlastního poskytovatele metadat pro playlist/album/umělec/fid'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Audio scrobblers'; String get audio_scrobblers => 'Audio scrobblers';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1455,7 +1455,16 @@ class AppLocalizationsDe extends AppLocalizations {
'Dieses Plugin scrobbelt Ihre Musik, um Ihre Hörhistorie zu erstellen.'; 'Dieses Plugin scrobbelt Ihre Musik, um Ihre Hörhistorie zu erstellen.';
@override @override
String get default_plugin => 'Standard'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Als Standard festlegen'; String get set_default => 'Als Standard festlegen';
@ -1517,7 +1526,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Eingabe entspricht nicht dem geforderten Format'; 'Eingabe entspricht nicht dem geforderten Format';
@override @override
String get metadata_provider_plugins => 'Plugins für Metadatenanbieter'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1542,12 +1551,22 @@ class AppLocalizationsDe extends AppLocalizations {
String get available_plugins => 'Verfügbare Plugins'; String get available_plugins => 'Verfügbare Plugins';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Eigenen Anbieter für Playlist-/Album-/Künstler-/Feed-Metadaten konfigurieren'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Audio-Scrobbler'; String get audio_scrobblers => 'Audio-Scrobbler';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1442,7 +1442,16 @@ class AppLocalizationsEn extends AppLocalizations {
'This plugin scrobbles your music to generate your listening history.'; 'This plugin scrobbles your music to generate your listening history.';
@override @override
String get default_plugin => 'Default'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Set default'; String get set_default => 'Set default';
@ -1503,7 +1512,7 @@ class AppLocalizationsEn extends AppLocalizations {
'Input doesn\'t match the required format'; 'Input doesn\'t match the required format';
@override @override
String get metadata_provider_plugins => 'Metadata Provider Plugins'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1528,12 +1537,22 @@ class AppLocalizationsEn extends AppLocalizations {
String get available_plugins => 'Available plugins'; String get available_plugins => 'Available plugins';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Configure your own playlist/album/artist/feed metadata provider'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Audio Scrobblers'; String get audio_scrobblers => 'Audio Scrobblers';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1452,7 +1452,16 @@ class AppLocalizationsEs extends AppLocalizations {
'Este complemento scrobblea tu música para generar tu historial de reproducción.'; 'Este complemento scrobblea tu música para generar tu historial de reproducción.';
@override @override
String get default_plugin => 'Predeterminado'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Establecer como predeterminado'; String get set_default => 'Establecer como predeterminado';
@ -1517,8 +1526,7 @@ class AppLocalizationsEs extends AppLocalizations {
'La entrada no coincide con el formato requerido'; 'La entrada no coincide con el formato requerido';
@override @override
String get metadata_provider_plugins => String get plugins => 'Plugins';
'Complementos de proveedor de metadatos';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1543,12 +1551,22 @@ class AppLocalizationsEs extends AppLocalizations {
String get available_plugins => 'Complementos disponibles'; String get available_plugins => 'Complementos disponibles';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Configura tu propio proveedor de metadatos para listas/álbum/artista/feeds'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Scrobblers de audio'; String get audio_scrobblers => 'Scrobblers de audio';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1451,7 +1451,16 @@ class AppLocalizationsEu extends AppLocalizations {
'Plugin honek zure musika scrobbled egiten du zure entzuteen historia sortzeko.'; 'Plugin honek zure musika scrobbled egiten du zure entzuteen historia sortzeko.';
@override @override
String get default_plugin => 'Lehenetsia'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Lehenetsi gisa ezarri'; String get set_default => 'Lehenetsi gisa ezarri';
@ -1515,7 +1524,7 @@ class AppLocalizationsEu extends AppLocalizations {
'Sarrera ezin da beharrezko formatutik desberdina izan'; 'Sarrera ezin da beharrezko formatutik desberdina izan';
@override @override
String get metadata_provider_plugins => 'Metadaten hornitzailearen pluginak'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1540,12 +1549,22 @@ class AppLocalizationsEu extends AppLocalizations {
String get available_plugins => 'Eskaintzen diren pluginak'; String get available_plugins => 'Eskaintzen diren pluginak';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Konfiguratu zureko playlists-/album-/artista-/feed-metadaten hornitzailea'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Audio scrobbler-ak'; String get audio_scrobblers => 'Audio scrobbler-ak';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1441,7 +1441,16 @@ class AppLocalizationsFa extends AppLocalizations {
'این افزونه موسیقی شما را اسکراب می‌کند تا تاریخچهٔ شنیداری‌تان را تولید کند.'; 'این افزونه موسیقی شما را اسکراب می‌کند تا تاریخچهٔ شنیداری‌تان را تولید کند.';
@override @override
String get default_plugin => 'پیش‌فرض'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'تنظیم به عنوان پیش‌فرض'; String get set_default => 'تنظیم به عنوان پیش‌فرض';
@ -1503,7 +1512,7 @@ class AppLocalizationsFa extends AppLocalizations {
'ورودی با قالب مورد نیاز تطابق ندارد'; 'ورودی با قالب مورد نیاز تطابق ندارد';
@override @override
String get metadata_provider_plugins => 'افزونه‌های ارائه‌دهندهٔ متادیتا'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1528,12 +1537,22 @@ class AppLocalizationsFa extends AppLocalizations {
String get available_plugins => 'افزونه‌های موجود'; String get available_plugins => 'افزونه‌های موجود';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'پیکربندی ارائه‌دهندهٔ متادیتا برای پلی‌لیست/آلبوم/هنرمند/فید به‌صورت سفارشی'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'اسکراب‌بلرهای صوتی'; String get audio_scrobblers => 'اسکراب‌بلرهای صوتی';
@override @override
String get scrobbling => 'اسکراب‌بلینگ'; String get scrobbling => 'اسکراب‌بلینگ';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1443,7 +1443,16 @@ class AppLocalizationsFi extends AppLocalizations {
'Tämä lisäosa scrobblaa musiikkisi luodakseen kuunteluhistoriasi.'; 'Tämä lisäosa scrobblaa musiikkisi luodakseen kuunteluhistoriasi.';
@override @override
String get default_plugin => 'Oletus'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Aseta oletukseksi'; String get set_default => 'Aseta oletukseksi';
@ -1503,7 +1512,7 @@ class AppLocalizationsFi extends AppLocalizations {
String get input_does_not_match_format => 'Syöte ei vastaa vaadittua muotoa'; String get input_does_not_match_format => 'Syöte ei vastaa vaadittua muotoa';
@override @override
String get metadata_provider_plugins => 'Metatietojen tarjoajan lisäosat'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1528,12 +1537,22 @@ class AppLocalizationsFi extends AppLocalizations {
String get available_plugins => 'Saatavilla olevat lisäosat'; String get available_plugins => 'Saatavilla olevat lisäosat';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Määritä oma soittolistan/albumin/artistin/syötteen metatietojen tarjoaja'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Äänen scrobblerit'; String get audio_scrobblers => 'Äänen scrobblerit';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1457,7 +1457,16 @@ class AppLocalizationsFr extends AppLocalizations {
'Ce plugin scrobble votre musique pour générer votre historique d\'écoute.'; 'Ce plugin scrobble votre musique pour générer votre historique d\'écoute.';
@override @override
String get default_plugin => 'Par défaut'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Définir par défaut'; String get set_default => 'Définir par défaut';
@ -1521,8 +1530,7 @@ class AppLocalizationsFr extends AppLocalizations {
'L\'entrée ne correspond pas au format requis'; 'L\'entrée ne correspond pas au format requis';
@override @override
String get metadata_provider_plugins => String get plugins => 'Plugins';
'Plugins de fournisseur de métadonnées';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1548,12 +1556,22 @@ class AppLocalizationsFr extends AppLocalizations {
String get available_plugins => 'Plugins disponibles'; String get available_plugins => 'Plugins disponibles';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Configurer votre propre fournisseur de métadonnées de playlist/album/artiste/flux'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Scrobblers audio'; String get audio_scrobblers => 'Scrobblers audio';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1448,7 +1448,16 @@ class AppLocalizationsHi extends AppLocalizations {
'यह प्लगइन आपके सुनने के इतिहास को उत्पन्न करने के लिए आपके संगीत को स्क्रॉबल करता है।'; 'यह प्लगइन आपके सुनने के इतिहास को उत्पन्न करने के लिए आपके संगीत को स्क्रॉबल करता है।';
@override @override
String get default_plugin => 'डिफ़ॉल्ट'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'डिफ़ॉल्ट सेट करें'; String get set_default => 'डिफ़ॉल्ट सेट करें';
@ -1509,7 +1518,7 @@ class AppLocalizationsHi extends AppLocalizations {
'इनपुट आवश्यक प्रारूप से मेल नहीं खाता है'; 'इनपुट आवश्यक प्रारूप से मेल नहीं खाता है';
@override @override
String get metadata_provider_plugins => 'मेटाडेटा प्रदाता प्लगइन'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1534,12 +1543,22 @@ class AppLocalizationsHi extends AppLocalizations {
String get available_plugins => 'उपलब्ध प्लगइन'; String get available_plugins => 'उपलब्ध प्लगइन';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'अपनी खुद की प्लेलिस्ट/एल्बम/कलाकार/फ़ीड मेटाडेटा प्रदाता कॉन्फ़िगर करें'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'ऑडियो स्क्रॉबलर्स'; String get audio_scrobblers => 'ऑडियो स्क्रॉबलर्स';
@override @override
String get scrobbling => 'स्क्रॉबलिंग'; String get scrobbling => 'स्क्रॉबलिंग';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1449,7 +1449,16 @@ class AppLocalizationsId extends AppLocalizations {
'Plugin ini scrobble musik Anda untuk menghasilkan riwayat mendengarkan Anda.'; 'Plugin ini scrobble musik Anda untuk menghasilkan riwayat mendengarkan Anda.';
@override @override
String get default_plugin => 'Bawaan'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Atur sebagai bawaan'; String get set_default => 'Atur sebagai bawaan';
@ -1511,7 +1520,7 @@ class AppLocalizationsId extends AppLocalizations {
'Masukan tidak cocok dengan format yang diperlukan'; 'Masukan tidak cocok dengan format yang diperlukan';
@override @override
String get metadata_provider_plugins => 'Plugin Penyedia Metadata'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1536,12 +1545,22 @@ class AppLocalizationsId extends AppLocalizations {
String get available_plugins => 'Plugin yang tersedia'; String get available_plugins => 'Plugin yang tersedia';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Konfigurasi penyedia metadata playlist/album/artis/feed Anda sendiri'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Scrobblers Audio'; String get audio_scrobblers => 'Scrobblers Audio';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1448,7 +1448,16 @@ class AppLocalizationsIt extends AppLocalizations {
'Questo plugin scrobbla la tua musica per generare la tua cronologia di ascolti.'; 'Questo plugin scrobbla la tua musica per generare la tua cronologia di ascolti.';
@override @override
String get default_plugin => 'Predefinito'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Imposta come predefinito'; String get set_default => 'Imposta come predefinito';
@ -1510,7 +1519,7 @@ class AppLocalizationsIt extends AppLocalizations {
'L\'input non corrisponde al formato richiesto'; 'L\'input non corrisponde al formato richiesto';
@override @override
String get metadata_provider_plugins => 'Plugin del provider di metadati'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1535,12 +1544,22 @@ class AppLocalizationsIt extends AppLocalizations {
String get available_plugins => 'Plugin disponibili'; String get available_plugins => 'Plugin disponibili';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Configura il tuo provider di metadati per playlist/album/artista/feed'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Scrobbler audio'; String get audio_scrobblers => 'Scrobbler audio';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1416,7 +1416,16 @@ class AppLocalizationsJa extends AppLocalizations {
String get plugin_scrobbling_info => 'このプラグインは、あなたの音楽をscrobbleして視聴履歴を生成します。'; String get plugin_scrobbling_info => 'このプラグインは、あなたの音楽をscrobbleして視聴履歴を生成します。';
@override @override
String get default_plugin => 'デフォルト'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'デフォルトに設定'; String get set_default => 'デフォルトに設定';
@ -1474,7 +1483,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get input_does_not_match_format => '入力が必須フォーマットと一致しません'; String get input_does_not_match_format => '入力が必須フォーマットと一致しません';
@override @override
String get metadata_provider_plugins => 'メタデータプロバイダープラグイン'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1499,12 +1508,22 @@ class AppLocalizationsJa extends AppLocalizations {
String get available_plugins => '利用可能なプラグイン'; String get available_plugins => '利用可能なプラグイン';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'独自のプレイリスト/アルバム/アーティスト/フィードのメタデータプロバイダーを構成'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'オーディオスクロッブラー'; String get audio_scrobblers => 'オーディオスクロッブラー';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1448,7 +1448,16 @@ class AppLocalizationsKa extends AppLocalizations {
'ეს პლაგინი აწარმოებს თქვენი მუსიკის სქრობლინგს, რათა შექმნას თქვენი მოსმენის ისტორია.'; 'ეს პლაგინი აწარმოებს თქვენი მუსიკის სქრობლინგს, რათა შექმნას თქვენი მოსმენის ისტორია.';
@override @override
String get default_plugin => 'ნაგულისხმევი'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'ნაგულისხმევად დაყენება'; String get set_default => 'ნაგულისხმევად დაყენება';
@ -1511,8 +1520,7 @@ class AppLocalizationsKa extends AppLocalizations {
'შეყვანა არ ემთხვევა საჭირო ფორმატს'; 'შეყვანა არ ემთხვევა საჭირო ფორმატს';
@override @override
String get metadata_provider_plugins => String get plugins => 'Plugins';
'მეტამონაცემების პროვაიდერების პლაგინები';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1537,12 +1545,22 @@ class AppLocalizationsKa extends AppLocalizations {
String get available_plugins => 'ხელმისაწვდომი პლაგინები'; String get available_plugins => 'ხელმისაწვდომი პლაგინები';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'დააყენეთ თქვენი საკუთარი პლეილისტის/ალბომის/არტისტის/ფიდის მეტამონაცემების პროვაიდერი'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'აუდიო სქრობლერები'; String get audio_scrobblers => 'აუდიო სქრობლერები';
@override @override
String get scrobbling => 'სქრობლინგი'; String get scrobbling => 'სქრობლინგი';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1421,7 +1421,16 @@ class AppLocalizationsKo extends AppLocalizations {
String get plugin_scrobbling_info => '이 플러그인은 음악을 스크로블하여 청취 기록을 생성합니다.'; String get plugin_scrobbling_info => '이 플러그인은 음악을 스크로블하여 청취 기록을 생성합니다.';
@override @override
String get default_plugin => '기본'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => '기본값으로 설정'; String get set_default => '기본값으로 설정';
@ -1479,7 +1488,7 @@ class AppLocalizationsKo extends AppLocalizations {
String get input_does_not_match_format => '입력이 필요한 형식과 일치하지 않습니다'; String get input_does_not_match_format => '입력이 필요한 형식과 일치하지 않습니다';
@override @override
String get metadata_provider_plugins => '메타데이터 제공자 플러그인'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1503,12 +1512,22 @@ class AppLocalizationsKo extends AppLocalizations {
String get available_plugins => '사용 가능한 플러그인'; String get available_plugins => '사용 가능한 플러그인';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'자신만의 플레이리스트/앨범/아티스트/피드 메타데이터 제공자 구성'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => '오디오 스크로블러'; String get audio_scrobblers => '오디오 스크로블러';
@override @override
String get scrobbling => '스크로블링'; String get scrobbling => '스크로블링';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1454,7 +1454,16 @@ class AppLocalizationsNe extends AppLocalizations {
'यो प्लगइनले तपाईंको सुन्ने इतिहास उत्पन्न गर्न तपाईंको संगीतलाई स्क्रब्बल गर्दछ।'; 'यो प्लगइनले तपाईंको सुन्ने इतिहास उत्पन्न गर्न तपाईंको संगीतलाई स्क्रब्बल गर्दछ।';
@override @override
String get default_plugin => 'पूर्वनिर्धारित'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'पूर्वनिर्धारित सेट गर्नुहोस्'; String get set_default => 'पूर्वनिर्धारित सेट गर्नुहोस्';
@ -1515,7 +1524,7 @@ class AppLocalizationsNe extends AppLocalizations {
String get input_does_not_match_format => 'इनपुट आवश्यक ढाँचासँग मेल खाँदैन'; String get input_does_not_match_format => 'इनपुट आवश्यक ढाँचासँग मेल खाँदैन';
@override @override
String get metadata_provider_plugins => 'मेटाडेटा प्रदायक प्लगइनहरू'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1540,12 +1549,22 @@ class AppLocalizationsNe extends AppLocalizations {
String get available_plugins => 'उपलब्ध प्लगइनहरू'; String get available_plugins => 'उपलब्ध प्लगइनहरू';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'तपाईंको आफ्नै प्लेलिस्ट/एल्बम/कलाकार/फिड मेटाडेटा प्रदायक कन्फिगर गर्नुहोस्'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'अडियो स्क्रब्बलरहरू'; String get audio_scrobblers => 'अडियो स्क्रब्बलरहरू';
@override @override
String get scrobbling => 'स्क्रब्बलिंग'; String get scrobbling => 'स्क्रब्बलिंग';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1446,7 +1446,16 @@ class AppLocalizationsNl extends AppLocalizations {
'Deze plugin scrobblet uw muziek om uw luistergeschiedenis te genereren.'; 'Deze plugin scrobblet uw muziek om uw luistergeschiedenis te genereren.';
@override @override
String get default_plugin => 'Standaard'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Instellen als standaard'; String get set_default => 'Instellen als standaard';
@ -1509,7 +1518,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Invoer komt niet overeen met het vereiste formaat'; 'Invoer komt niet overeen met het vereiste formaat';
@override @override
String get metadata_provider_plugins => 'Metadata-aanbieder Plugins'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1534,12 +1543,22 @@ class AppLocalizationsNl extends AppLocalizations {
String get available_plugins => 'Beschikbare plugins'; String get available_plugins => 'Beschikbare plugins';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Configureer uw eigen metadata-aanbieder voor afspeellijst/album/artiest/feed'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Audioscrobblers'; String get audio_scrobblers => 'Audioscrobblers';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1449,7 +1449,16 @@ class AppLocalizationsPl extends AppLocalizations {
'Ta wtyczka scrobbluje Twoją muzykę, aby wygenerować historię odsłuchań.'; 'Ta wtyczka scrobbluje Twoją muzykę, aby wygenerować historię odsłuchań.';
@override @override
String get default_plugin => 'Domyślna'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Ustaw jako domyślną'; String get set_default => 'Ustaw jako domyślną';
@ -1511,7 +1520,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Wprowadzony tekst nie pasuje do wymaganego formatu'; 'Wprowadzony tekst nie pasuje do wymaganego formatu';
@override @override
String get metadata_provider_plugins => 'Wtyczki dostawców metadanych'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1536,12 +1545,22 @@ class AppLocalizationsPl extends AppLocalizations {
String get available_plugins => 'Dostępne wtyczki'; String get available_plugins => 'Dostępne wtyczki';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Skonfiguruj własnego dostawcę metadanych dla playlisty/albumu/artysty/kanału'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Scrobblery audio'; String get audio_scrobblers => 'Scrobblery audio';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1446,7 +1446,16 @@ class AppLocalizationsPt extends AppLocalizations {
'Este plugin faz o scrobbling de sua música para gerar seu histórico de audição.'; 'Este plugin faz o scrobbling de sua música para gerar seu histórico de audição.';
@override @override
String get default_plugin => 'Padrão'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Definir como padrão'; String get set_default => 'Definir como padrão';
@ -1508,7 +1517,7 @@ class AppLocalizationsPt extends AppLocalizations {
'A entrada não corresponde ao formato exigido'; 'A entrada não corresponde ao formato exigido';
@override @override
String get metadata_provider_plugins => 'Plugins do provedor de metadados'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1533,12 +1542,22 @@ class AppLocalizationsPt extends AppLocalizations {
String get available_plugins => 'Plugins disponíveis'; String get available_plugins => 'Plugins disponíveis';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Configure seu próprio provedor de metadados de playlist/álbum/artista/feed'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Scrobblers de áudio'; String get audio_scrobblers => 'Scrobblers de áudio';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1448,7 +1448,16 @@ class AppLocalizationsRu extends AppLocalizations {
'Этот плагин скробблит вашу музыку для создания вашей истории прослушиваний.'; 'Этот плагин скробблит вашу музыку для создания вашей истории прослушиваний.';
@override @override
String get default_plugin => 'По умолчанию'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Установить по умолчанию'; String get set_default => 'Установить по умолчанию';
@ -1511,7 +1520,7 @@ class AppLocalizationsRu extends AppLocalizations {
'Введенные данные не соответствуют требуемому формату'; 'Введенные данные не соответствуют требуемому формату';
@override @override
String get metadata_provider_plugins => 'Плагины поставщика метаданных'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1536,12 +1545,22 @@ class AppLocalizationsRu extends AppLocalizations {
String get available_plugins => 'Доступные плагины'; String get available_plugins => 'Доступные плагины';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Настройте свой собственный поставщик метаданных для плейлиста/альбома/артиста/ленты'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Аудио скробблеры'; String get audio_scrobblers => 'Аудио скробблеры';
@override @override
String get scrobbling => 'Скробблинг'; String get scrobbling => 'Скробблинг';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1455,7 +1455,16 @@ class AppLocalizationsTa extends AppLocalizations {
'இந்த பிளகின் உங்கள் கேட்பதின் வரலாற்றை உருவாக்க உங்கள் இசையை ஸ்க்ரோப்ள் செய்கிறது.'; 'இந்த பிளகின் உங்கள் கேட்பதின் வரலாற்றை உருவாக்க உங்கள் இசையை ஸ்க்ரோப்ள் செய்கிறது.';
@override @override
String get default_plugin => 'இயல்புநிலை'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'இயல்புநிலையாக அமைக்கவும்'; String get set_default => 'இயல்புநிலையாக அமைக்கவும்';
@ -1517,7 +1526,7 @@ class AppLocalizationsTa extends AppLocalizations {
'உள்ளீடு தேவையான வடிவத்துடன் பொருந்தவில்லை'; 'உள்ளீடு தேவையான வடிவத்துடன் பொருந்தவில்லை';
@override @override
String get metadata_provider_plugins => 'மெட்டாடேட்டா வழங்குநர் பிளகின்கள்'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1542,12 +1551,22 @@ class AppLocalizationsTa extends AppLocalizations {
String get available_plugins => 'கிடைக்கக்கூடிய பிளகின்கள்'; String get available_plugins => 'கிடைக்கக்கூடிய பிளகின்கள்';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'உங்கள் சொந்த பிளேலிஸ்ட்/ஆல்பம்/கலைஞர்/ஊட்ட மெட்டாடேட்டா வழங்குநரை உள்ளமைக்கவும்'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'ஆடியோ ஸ்க்ரோப்ளர்கள்'; String get audio_scrobblers => 'ஆடியோ ஸ்க்ரோப்ளர்கள்';
@override @override
String get scrobbling => 'ஸ்க்ரோப்ளிங்'; String get scrobbling => 'ஸ்க்ரோப்ளிங்';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1440,7 +1440,16 @@ class AppLocalizationsTh extends AppLocalizations {
'ปลั๊กอินนี้จะ scrobble เพลงของคุณเพื่อสร้างประวัติการฟังของคุณ'; 'ปลั๊กอินนี้จะ scrobble เพลงของคุณเพื่อสร้างประวัติการฟังของคุณ';
@override @override
String get default_plugin => 'ค่าเริ่มต้น'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'ตั้งค่าเริ่มต้น'; String get set_default => 'ตั้งค่าเริ่มต้น';
@ -1500,7 +1509,7 @@ class AppLocalizationsTh extends AppLocalizations {
String get input_does_not_match_format => 'อินพุตไม่ตรงกับรูปแบบที่ต้องการ'; String get input_does_not_match_format => 'อินพุตไม่ตรงกับรูปแบบที่ต้องการ';
@override @override
String get metadata_provider_plugins => 'ปลั๊กอินผู้ให้บริการเมตาดาต้า'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1525,12 +1534,22 @@ class AppLocalizationsTh extends AppLocalizations {
String get available_plugins => 'ปลั๊กอินที่มีอยู่'; String get available_plugins => 'ปลั๊กอินที่มีอยู่';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'กำหนดค่าผู้ให้บริการเมตาดาต้าเพลย์ลิสต์/อัลบั้ม/ศิลปิน/ฟีดของคุณเอง'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'เครื่อง scrobbler เสียง'; String get audio_scrobblers => 'เครื่อง scrobbler เสียง';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1456,7 +1456,16 @@ class AppLocalizationsTl extends AppLocalizations {
'Sinis-scrobble ng plugin na ito ang iyong musika upang mabuo ang iyong kasaysayan ng pakikinig.'; 'Sinis-scrobble ng plugin na ito ang iyong musika upang mabuo ang iyong kasaysayan ng pakikinig.';
@override @override
String get default_plugin => 'Default'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Itakda bilang default'; String get set_default => 'Itakda bilang default';
@ -1518,7 +1527,7 @@ class AppLocalizationsTl extends AppLocalizations {
'Ang input ay hindi tumutugma sa kinakailangang format'; 'Ang input ay hindi tumutugma sa kinakailangang format';
@override @override
String get metadata_provider_plugins => 'Mga Plugin ng Metadata Provider'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1543,12 +1552,22 @@ class AppLocalizationsTl extends AppLocalizations {
String get available_plugins => 'Mga available na plugin'; String get available_plugins => 'Mga available na plugin';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'I-configure ang iyong sariling playlist/album/artist/feed metadata provider'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Mga Audio Scrobbler'; String get audio_scrobblers => 'Mga Audio Scrobbler';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1450,7 +1450,16 @@ class AppLocalizationsTr extends AppLocalizations {
'Bu eklenti, dinleme geçmişinizi oluşturmak için müziğinizi scrobble eder.'; 'Bu eklenti, dinleme geçmişinizi oluşturmak için müziğinizi scrobble eder.';
@override @override
String get default_plugin => 'Varsayılan'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Varsayılan olarak ayarla'; String get set_default => 'Varsayılan olarak ayarla';
@ -1511,7 +1520,7 @@ class AppLocalizationsTr extends AppLocalizations {
String get input_does_not_match_format => 'Girdi, gerekli biçimle eşleşmiyor'; String get input_does_not_match_format => 'Girdi, gerekli biçimle eşleşmiyor';
@override @override
String get metadata_provider_plugins => 'Meta Veri Sağlayıcısı Eklentileri'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1536,12 +1545,22 @@ class AppLocalizationsTr extends AppLocalizations {
String get available_plugins => 'Mevcut eklentiler'; String get available_plugins => 'Mevcut eklentiler';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Kendi çalma listenizi/albümünüzü/sanatçınızı/akış meta veri sağlayıcınızı yapılandırın'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Ses Scrobbler\'lar'; String get audio_scrobblers => 'Ses Scrobbler\'lar';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1446,7 +1446,16 @@ class AppLocalizationsUk extends AppLocalizations {
'Цей плагін скроббить вашу музику, щоб створити вашу історію прослуховувань.'; 'Цей плагін скроббить вашу музику, щоб створити вашу історію прослуховувань.';
@override @override
String get default_plugin => 'За замовчуванням'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Встановити за замовчуванням'; String get set_default => 'Встановити за замовчуванням';
@ -1507,7 +1516,7 @@ class AppLocalizationsUk extends AppLocalizations {
'Введені дані не відповідають необхідному формату'; 'Введені дані не відповідають необхідному формату';
@override @override
String get metadata_provider_plugins => 'Плагіни провайдера метаданих'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1532,12 +1541,22 @@ class AppLocalizationsUk extends AppLocalizations {
String get available_plugins => 'Доступні плагіни'; String get available_plugins => 'Доступні плагіни';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Налаштуйте свій власний провайдер метаданих для плейлиста/альбому/виконавця/стрічки'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Аудіо скробблери'; String get audio_scrobblers => 'Аудіо скробблери';
@override @override
String get scrobbling => 'Скроблінг'; String get scrobbling => 'Скроблінг';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1450,7 +1450,16 @@ class AppLocalizationsVi extends AppLocalizations {
'Plugin này scrobble nhạc của bạn để tạo lịch sử nghe của bạn.'; 'Plugin này scrobble nhạc của bạn để tạo lịch sử nghe của bạn.';
@override @override
String get default_plugin => 'Mặc định'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => 'Đặt làm mặc định'; String get set_default => 'Đặt làm mặc định';
@ -1513,7 +1522,7 @@ class AppLocalizationsVi extends AppLocalizations {
'Đầu vào không khớp với định dạng yêu cầu'; 'Đầu vào không khớp với định dạng yêu cầu';
@override @override
String get metadata_provider_plugins => 'Plugin Nhà cung cấp siêu dữ liệu'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1538,12 +1547,22 @@ class AppLocalizationsVi extends AppLocalizations {
String get available_plugins => 'Các plugin có sẵn'; String get available_plugins => 'Các plugin có sẵn';
@override @override
String get configure_your_own_metadata_plugin => String get configure_plugins =>
'Cấu hình nhà cung cấp siêu dữ liệu danh sách phát/album/nghệ sĩ/nguồn cấp dữ liệu của riêng bạn'; 'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => 'Bộ scrobbler âm thanh'; String get audio_scrobblers => 'Bộ scrobbler âm thanh';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1412,7 +1412,16 @@ class AppLocalizationsZh extends AppLocalizations {
String get plugin_scrobbling_info => '此插件会 scrobble 您的音乐以生成您的收听历史记录。'; String get plugin_scrobbling_info => '此插件会 scrobble 您的音乐以生成您的收听历史记录。';
@override @override
String get default_plugin => '默认'; String get default_metadata_source => 'Default metadata source';
@override
String get set_default_metadata_source => 'Set default metadata source';
@override
String get default_audio_source => 'Default audio source';
@override
String get set_default_audio_source => 'Set default audio source';
@override @override
String get set_default => '设为默认'; String get set_default => '设为默认';
@ -1469,7 +1478,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get input_does_not_match_format => '输入与所需格式不匹配'; String get input_does_not_match_format => '输入与所需格式不匹配';
@override @override
String get metadata_provider_plugins => '元数据提供者插件'; String get plugins => 'Plugins';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
@ -1493,13 +1502,24 @@ class AppLocalizationsZh extends AppLocalizations {
String get available_plugins => '可用插件'; String get available_plugins => '可用插件';
@override @override
String get configure_your_own_metadata_plugin => '配置您自己的播放列表/专辑/艺人/订阅元数据提供者'; String get configure_plugins =>
'Configure your own metadata provider and audio source plugins';
@override @override
String get audio_scrobblers => '音频 Scrobblers'; String get audio_scrobblers => '音频 Scrobblers';
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }
/// The translations for Chinese, as used in Taiwan (`zh_TW`). /// The translations for Chinese, as used in Taiwan (`zh_TW`).
@ -2909,9 +2929,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override @override
String get plugin_scrobbling_info => '此外掛程式會 Scrobble 您的音樂以產生您的收聽記錄。'; String get plugin_scrobbling_info => '此外掛程式會 Scrobble 您的音樂以產生您的收聽記錄。';
@override
String get default_plugin => '預設';
@override @override
String get set_default => '設為預設'; String get set_default => '設為預設';
@ -2966,9 +2983,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override @override
String get input_does_not_match_format => '輸入不符合所需格式'; String get input_does_not_match_format => '輸入不符合所需格式';
@override
String get metadata_provider_plugins => '中繼資料供應商外掛程式';
@override @override
String get paste_plugin_download_url => String get paste_plugin_download_url =>
'貼上下載網址、GitHub/Codeberg 儲存庫網址或 .smplug 檔案的直接連結'; '貼上下載網址、GitHub/Codeberg 儲存庫網址或 .smplug 檔案的直接連結';
@ -2990,9 +3004,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override @override
String get available_plugins => '可用的外掛程式'; String get available_plugins => '可用的外掛程式';
@override
String get configure_your_own_metadata_plugin => '設定您自己的播放清單/專輯/藝人/動態中繼資料供應商';
@override @override
String get audio_scrobblers => '音訊 Scrobblers'; String get audio_scrobblers => '音訊 Scrobblers';

View File

@ -83,6 +83,8 @@ Future<void> main(List<String> rawArgs) async {
// force High Refresh Rate on some Android devices (like One Plus) // force High Refresh Rate on some Android devices (like One Plus)
if (kIsAndroid) { if (kIsAndroid) {
await FlutterDisplayMode.setHighRefreshRate(); await FlutterDisplayMode.setHighRefreshRate();
}
if (kIsAndroid || kIsDesktop) {
await NewPipeExtractor.init(); await NewPipeExtractor.init();
} }
@ -150,11 +152,13 @@ class Spotube extends HookConsumerWidget {
ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
ref.listen(bonsoirProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {});
ref.listen(connectClientsProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {});
ref.listen(metadataPluginsProvider, (_, __) {});
ref.listen(metadataPluginProvider, (_, __) {});
ref.listen(serverProvider, (_, __) {}); ref.listen(serverProvider, (_, __) {});
ref.listen(trayManagerProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {});
ref.listen(metadataPluginsProvider, (_, __) {});
ref.listen(metadataPluginProvider, (_, __) {});
ref.listen(audioSourcePluginProvider, (_, __) {});
ref.listen(metadataPluginUpdateCheckerProvider, (_, __) {}); ref.listen(metadataPluginUpdateCheckerProvider, (_, __) {});
ref.listen(audioSourcePluginUpdateCheckerProvider, (_, __) {});
useFixWindowStretching(); useFixWindowStretching();
useDisableBatteryOptimizations(); useDisableBatteryOptimizations();

View File

@ -112,8 +112,13 @@ mixin _$WebSocketLoadEventData {
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this WebSocketLoadEventData to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$WebSocketLoadEventDataCopyWith<WebSocketLoadEventData> get copyWith => $WebSocketLoadEventDataCopyWith<WebSocketLoadEventData> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -142,6 +147,8 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res,
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -190,6 +197,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
$Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then) $Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -213,6 +222,8 @@ class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
)); ));
} }
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SpotubeSimplePlaylistObjectCopyWith<$Res>? get collection { $SpotubeSimplePlaylistObjectCopyWith<$Res>? get collection {
@ -281,12 +292,14 @@ class _$WebSocketLoadEventDataPlaylistImpl
other.initialIndex == initialIndex)); other.initialIndex == initialIndex));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(_tracks), collection, initialIndex); const DeepCollectionEquality().hash(_tracks), collection, initialIndex);
@JsonKey(ignore: true) /// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$WebSocketLoadEventDataPlaylistImplCopyWith< _$$WebSocketLoadEventDataPlaylistImplCopyWith<
@ -420,8 +433,11 @@ abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
SpotubeSimplePlaylistObject? get collection; SpotubeSimplePlaylistObject? get collection;
@override @override
int? get initialIndex; int? get initialIndex;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$WebSocketLoadEventDataPlaylistImplCopyWith< _$$WebSocketLoadEventDataPlaylistImplCopyWith<
_$WebSocketLoadEventDataPlaylistImpl> _$WebSocketLoadEventDataPlaylistImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
@ -456,6 +472,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
$Res Function(_$WebSocketLoadEventDataAlbumImpl) _then) $Res Function(_$WebSocketLoadEventDataAlbumImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -479,6 +497,8 @@ class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
)); ));
} }
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SpotubeSimpleAlbumObjectCopyWith<$Res>? get collection { $SpotubeSimpleAlbumObjectCopyWith<$Res>? get collection {
@ -545,12 +565,14 @@ class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
other.initialIndex == initialIndex)); other.initialIndex == initialIndex));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(_tracks), collection, initialIndex); const DeepCollectionEquality().hash(_tracks), collection, initialIndex);
@JsonKey(ignore: true) /// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
@ -683,8 +705,11 @@ abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
SpotubeSimpleAlbumObject? get collection; SpotubeSimpleAlbumObject? get collection;
@override @override
int? get initialIndex; int? get initialIndex;
/// Create a copy of WebSocketLoadEventData
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl> _$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
get copyWith => throw _privateConstructorUsedError; get copyWith => throw _privateConstructorUsedError;
} }

View File

@ -16,13 +16,13 @@ import 'package:spotube/models/metadata/market.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:flutter/widgets.dart' hide Table, Key, View;
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart';
import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart';
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
import 'package:spotube/utils/platform.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
@ -58,14 +58,14 @@ part 'typeconverters/subtitle.dart';
AudioPlayerStateTable, AudioPlayerStateTable,
HistoryTable, HistoryTable,
LyricsTable, LyricsTable,
MetadataPluginsTable, PluginsTable,
], ],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 8; int get schemaVersion => 10;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -199,6 +199,28 @@ class AppDatabase extends _$AppDatabase {
} }
}); });
}, },
from8To9: (m, schema) async {
await m.renameTable(schema.pluginsTable, "metadata_plugins_table");
await m.renameColumn(
schema.pluginsTable,
"selected",
pluginsTable.selectedForMetadata,
);
await m.addColumn(
schema.pluginsTable,
pluginsTable.selectedForAudioSource,
);
},
from9To10: (m, schema) async {
await m.dropColumn(schema.preferencesTable, "piped_instance");
await m.dropColumn(schema.preferencesTable, "invidious_instance");
await m.addColumn(
schema.sourceMatchTable,
sourceMatchTable.sourceInfo,
);
await customStatement("DROP INDEX IF EXISTS uniq_track_match;");
await m.dropColumn(schema.sourceMatchTable, "source_id");
},
), ),
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0; import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1; import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/models/metadata/market.dart';
import 'package:spotube/services/sourced_track/enums.dart';
// GENERATED BY drift_dev, DO NOT MODIFY. // GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema2 extends i0.VersionedSchema { final class Schema2 extends i0.VersionedSchema {
@ -329,8 +329,7 @@ class Shape2 extends i0.VersionedTable {
i1.GeneratedColumn<String> _column_7(String aliasedName) => i1.GeneratedColumn<String> _column_7(String aliasedName) =>
i1.GeneratedColumn<String>('audio_quality', aliasedName, false, i1.GeneratedColumn<String>('audio_quality', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("high"));
defaultValue: Constant(SourceQualities.high.name));
i1.GeneratedColumn<bool> _column_8(String aliasedName) => i1.GeneratedColumn<bool> _column_8(String aliasedName) =>
i1.GeneratedColumn<bool>('album_color_sync', aliasedName, false, i1.GeneratedColumn<bool>('album_color_sync', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
@ -417,16 +416,13 @@ i1.GeneratedColumn<String> _column_25(String aliasedName) =>
defaultValue: Constant(ThemeMode.system.name)); defaultValue: Constant(ThemeMode.system.name));
i1.GeneratedColumn<String> _column_26(String aliasedName) => i1.GeneratedColumn<String> _column_26(String aliasedName) =>
i1.GeneratedColumn<String>('audio_source', aliasedName, false, i1.GeneratedColumn<String>('audio_source', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("youtube"));
defaultValue: Constant(AudioSource.youtube.name));
i1.GeneratedColumn<String> _column_27(String aliasedName) => i1.GeneratedColumn<String> _column_27(String aliasedName) =>
i1.GeneratedColumn<String>('stream_music_codec', aliasedName, false, i1.GeneratedColumn<String>('stream_music_codec', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("weba"));
defaultValue: Constant(SourceCodecs.weba.name));
i1.GeneratedColumn<String> _column_28(String aliasedName) => i1.GeneratedColumn<String> _column_28(String aliasedName) =>
i1.GeneratedColumn<String>('download_music_codec', aliasedName, false, i1.GeneratedColumn<String>('download_music_codec', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("m4a"));
defaultValue: Constant(SourceCodecs.m4a.name));
i1.GeneratedColumn<bool> _column_29(String aliasedName) => i1.GeneratedColumn<bool> _column_29(String aliasedName) =>
i1.GeneratedColumn<bool>('discord_presence', aliasedName, false, i1.GeneratedColumn<bool>('discord_presence', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
@ -511,8 +507,7 @@ i1.GeneratedColumn<String> _column_38(String aliasedName) =>
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_39(String aliasedName) => i1.GeneratedColumn<String> _column_39(String aliasedName) =>
i1.GeneratedColumn<String>('source_type', aliasedName, false, i1.GeneratedColumn<String>('source_type', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("youtube"));
defaultValue: Constant(SourceType.youtube.name));
class Shape6 extends i0.VersionedTable { class Shape6 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased(); Shape6({required super.source, required super.alias}) : super.aliased();
@ -1407,7 +1402,7 @@ final class Schema5 extends i0.VersionedSchema {
i1.GeneratedColumn<String> _column_55(String aliasedName) => i1.GeneratedColumn<String> _column_55(String aliasedName) =>
i1.GeneratedColumn<String>('accent_color_scheme', aliasedName, false, i1.GeneratedColumn<String>('accent_color_scheme', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultValue: const Constant("Slate:0xff64748b")); defaultValue: const Constant("Orange:0xFFf97315"));
final class Schema6 extends i0.VersionedSchema { final class Schema6 extends i0.VersionedSchema {
Schema6({required super.database}) : super(version: 6); Schema6({required super.database}) : super(version: 6);
@ -2053,7 +2048,7 @@ final class Schema8 extends i0.VersionedSchema {
_column_13, _column_13,
_column_14, _column_14,
_column_15, _column_15,
_column_55, _column_69,
_column_17, _column_17,
_column_18, _column_18,
_column_19, _column_19,
@ -2188,7 +2183,7 @@ final class Schema8 extends i0.VersionedSchema {
_column_65, _column_65,
_column_66, _column_66,
_column_67, _column_67,
_column_69, _column_70,
], ],
attachedDatabase: database, attachedDatabase: database,
), ),
@ -2200,8 +2195,550 @@ final class Schema8 extends i0.VersionedSchema {
} }
i1.GeneratedColumn<String> _column_69(String aliasedName) => i1.GeneratedColumn<String> _column_69(String aliasedName) =>
i1.GeneratedColumn<String>('accent_color_scheme', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: const Constant("Slate:0xff64748b"));
i1.GeneratedColumn<String> _column_70(String aliasedName) =>
i1.GeneratedColumn<String>('plugin_api_version', aliasedName, false, i1.GeneratedColumn<String>('plugin_api_version', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: const Constant('1.0.0')); type: i1.DriftSqlType.string, defaultValue: const Constant('1.0.0'));
final class Schema9 extends i0.VersionedSchema {
Schema9({required super.database}) : super(version: 9);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
audioPlayerStateTable,
historyTable,
lyricsTable,
pluginsTable,
uniqueBlacklist,
uniqTrackMatch,
];
late final Shape0 authenticationTable = Shape0(
source: i0.VersionedTable(
entityName: 'authentication_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 blacklistTable = Shape1(
source: i0.VersionedTable(
entityName: 'blacklist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_5,
_column_6,
],
attachedDatabase: database,
),
alias: null);
late final Shape13 preferencesTable = Shape13(
source: i0.VersionedTable(
entityName: 'preferences_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
_column_69,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_25,
_column_26,
_column_54,
_column_27,
_column_28,
_column_29,
_column_30,
_column_31,
_column_56,
_column_53,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 scrobblerTable = Shape3(
source: i0.VersionedTable(
entityName: 'scrobbler_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_33,
_column_34,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 skipSegmentTable = Shape4(
source: i0.VersionedTable(
entityName: 'skip_segment_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_35,
_column_36,
_column_37,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 sourceMatchTable = Shape5(
source: i0.VersionedTable(
entityName: 'source_match_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_38,
_column_39,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 audioPlayerStateTable = Shape14(
source: i0.VersionedTable(
entityName: 'audio_player_state_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_40,
_column_41,
_column_42,
_column_43,
_column_57,
_column_58,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 historyTable = Shape9(
source: i0.VersionedTable(
entityName: 'history_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_50,
_column_51,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 lyricsTable = Shape10(
source: i0.VersionedTable(
entityName: 'lyrics_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape16 pluginsTable = Shape16(
source: i0.VersionedTable(
entityName: 'plugins_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_59,
_column_60,
_column_61,
_column_62,
_column_63,
_column_64,
_column_65,
_column_71,
_column_72,
_column_67,
_column_73,
],
attachedDatabase: database,
),
alias: null);
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
}
class Shape16 extends i0.VersionedTable {
Shape16({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get version =>
columnsByName['version']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get author =>
columnsByName['author']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get entryPoint =>
columnsByName['entry_point']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get apis =>
columnsByName['apis']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get abilities =>
columnsByName['abilities']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get selectedForMetadata =>
columnsByName['selected_for_metadata']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get selectedForAudioSource =>
columnsByName['selected_for_audio_source']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get repository =>
columnsByName['repository']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get pluginApiVersion =>
columnsByName['plugin_api_version']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<bool> _column_71(String aliasedName) =>
i1.GeneratedColumn<bool>('selected_for_metadata', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("selected_for_metadata" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<bool> _column_72(String aliasedName) =>
i1.GeneratedColumn<bool>('selected_for_audio_source', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("selected_for_audio_source" IN (0, 1))'),
defaultValue: const Constant(false));
i1.GeneratedColumn<String> _column_73(String aliasedName) =>
i1.GeneratedColumn<String>('plugin_api_version', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: const Constant('2.0.0'));
final class Schema10 extends i0.VersionedSchema {
Schema10({required super.database}) : super(version: 10);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
audioPlayerStateTable,
historyTable,
lyricsTable,
pluginsTable,
uniqueBlacklist,
uniqTrackMatch,
];
late final Shape0 authenticationTable = Shape0(
source: i0.VersionedTable(
entityName: 'authentication_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 blacklistTable = Shape1(
source: i0.VersionedTable(
entityName: 'blacklist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_5,
_column_6,
],
attachedDatabase: database,
),
alias: null);
late final Shape17 preferencesTable = Shape17(
source: i0.VersionedTable(
entityName: 'preferences_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
_column_69,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_25,
_column_74,
_column_54,
_column_29,
_column_30,
_column_31,
_column_56,
_column_53,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 scrobblerTable = Shape3(
source: i0.VersionedTable(
entityName: 'scrobbler_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_33,
_column_34,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 skipSegmentTable = Shape4(
source: i0.VersionedTable(
entityName: 'skip_segment_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_35,
_column_36,
_column_37,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape18 sourceMatchTable = Shape18(
source: i0.VersionedTable(
entityName: 'source_match_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_75,
_column_76,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 audioPlayerStateTable = Shape14(
source: i0.VersionedTable(
entityName: 'audio_player_state_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_40,
_column_41,
_column_42,
_column_43,
_column_57,
_column_58,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 historyTable = Shape9(
source: i0.VersionedTable(
entityName: 'history_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_50,
_column_51,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 lyricsTable = Shape10(
source: i0.VersionedTable(
entityName: 'lyrics_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape16 pluginsTable = Shape16(
source: i0.VersionedTable(
entityName: 'plugins_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_59,
_column_60,
_column_61,
_column_62,
_column_63,
_column_64,
_column_65,
_column_71,
_column_72,
_column_67,
_column_73,
],
attachedDatabase: database,
),
alias: null);
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)');
}
class Shape17 extends i0.VersionedTable {
Shape17({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get albumColorSync =>
columnsByName['album_color_sync']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get amoledDarkTheme =>
columnsByName['amoled_dark_theme']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get checkUpdate =>
columnsByName['check_update']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get normalizeAudio =>
columnsByName['normalize_audio']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get showSystemTrayIcon =>
columnsByName['show_system_tray_icon']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get systemTitleBar =>
columnsByName['system_title_bar']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get skipNonMusic =>
columnsByName['skip_non_music']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get closeBehavior =>
columnsByName['close_behavior']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get accentColorScheme =>
columnsByName['accent_color_scheme']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get layoutMode =>
columnsByName['layout_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get locale =>
columnsByName['locale']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get market =>
columnsByName['market']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get searchMode =>
columnsByName['search_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadLocation =>
columnsByName['download_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get localLibraryLocation =>
columnsByName['local_library_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get themeMode =>
columnsByName['theme_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get audioSourceId =>
columnsByName['audio_source_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get youtubeClientEngine =>
columnsByName['youtube_client_engine']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get discordPresence =>
columnsByName['discord_presence']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get endlessPlayback =>
columnsByName['endless_playback']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get enableConnect =>
columnsByName['enable_connect']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get connectPort =>
columnsByName['connect_port']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get cacheMusic =>
columnsByName['cache_music']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_74(String aliasedName) =>
i1.GeneratedColumn<String>('audio_source_id', aliasedName, true,
type: i1.DriftSqlType.string);
class Shape18 extends i0.VersionedTable {
Shape18({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get trackId =>
columnsByName['track_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sourceInfo =>
columnsByName['source_info']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sourceType =>
columnsByName['source_type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_75(String aliasedName) =>
i1.GeneratedColumn<String>('source_info', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: const Constant("{}"));
i1.GeneratedColumn<String> _column_76(String aliasedName) =>
i1.GeneratedColumn<String>('source_type', aliasedName, false,
type: i1.DriftSqlType.string);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -2210,6 +2747,8 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -2248,6 +2787,16 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from7To8(migrator, schema); await from7To8(migrator, schema);
return 8; return 8;
case 8:
final schema = Schema9(database: database);
final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema);
return 9;
case 9:
final schema = Schema10(database: database);
final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema);
return 10;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -2262,6 +2811,8 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
@ -2272,4 +2823,6 @@ i1.OnUpgrade stepByStep({
from5To6: from5To6, from5To6: from5To6,
from6To7: from6To7, from6To7: from6To7,
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9,
from9To10: from9To10,
)); ));

View File

@ -1,6 +1,6 @@
part of '../database.dart'; part of '../database.dart';
class MetadataPluginsTable extends Table { class PluginsTable extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 50)(); TextColumn get name => text().withLength(min: 1, max: 50)();
TextColumn get description => text()(); TextColumn get description => text()();
@ -9,8 +9,11 @@ class MetadataPluginsTable extends Table {
TextColumn get entryPoint => text()(); TextColumn get entryPoint => text()();
TextColumn get apis => text().map(const StringListConverter())(); TextColumn get apis => text().map(const StringListConverter())();
TextColumn get abilities => text().map(const StringListConverter())(); TextColumn get abilities => text().map(const StringListConverter())();
BoolColumn get selected => boolean().withDefault(const Constant(false))(); BoolColumn get selectedForMetadata =>
boolean().withDefault(const Constant(false))();
BoolColumn get selectedForAudioSource =>
boolean().withDefault(const Constant(false))();
TextColumn get repository => text().nullable()(); TextColumn get repository => text().nullable()();
TextColumn get pluginApiVersion => TextColumn get pluginApiVersion =>
text().withDefault(const Constant('1.0.0'))(); text().withDefault(const Constant('2.0.0'))();
} }

View File

@ -11,15 +11,6 @@ enum CloseBehavior {
close, close,
} }
enum AudioSource {
youtube,
piped,
jiosaavn,
invidious;
String get label => name[0].toUpperCase() + name.substring(1);
}
enum YoutubeClientEngine { enum YoutubeClientEngine {
ytDlp("yt-dlp"), ytDlp("yt-dlp"),
youtubeExplode("YouTubeExplode"), youtubeExplode("YouTubeExplode"),
@ -39,14 +30,6 @@ enum YoutubeClientEngine {
} }
} }
enum MusicCodec {
m4a._("M4a (Best for downloaded music)"),
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
final String label;
const MusicCodec._(this.label);
}
enum SearchMode { enum SearchMode {
youtube._("YouTube"), youtube._("YouTube"),
youtubeMusic._("YouTube Music"); youtubeMusic._("YouTube Music");
@ -62,8 +45,6 @@ enum SearchMode {
class PreferencesTable extends Table { class PreferencesTable extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get audioQuality => textEnum<SourceQualities>()
.withDefault(Constant(SourceQualities.high.name))();
BoolColumn get albumColorSync => BoolColumn get albumColorSync =>
boolean().withDefault(const Constant(true))(); boolean().withDefault(const Constant(true))();
BoolColumn get amoledDarkTheme => BoolColumn get amoledDarkTheme =>
@ -95,20 +76,11 @@ class PreferencesTable extends Table {
TextColumn get downloadLocation => text().withDefault(const Constant(""))(); TextColumn get downloadLocation => text().withDefault(const Constant(""))();
TextColumn get localLibraryLocation => TextColumn get localLibraryLocation =>
text().withDefault(const Constant("")).map(const StringListConverter())(); text().withDefault(const Constant("")).map(const StringListConverter())();
TextColumn get pipedInstance =>
text().withDefault(const Constant("https://pipedapi.kavin.rocks"))();
TextColumn get invidiousInstance =>
text().withDefault(const Constant("https://inv.nadeko.net"))();
TextColumn get themeMode => TextColumn get themeMode =>
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))(); textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
TextColumn get audioSource => TextColumn get audioSourceId => text().nullable()();
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>() TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>()
.withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))(); .withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))();
TextColumn get streamMusicCodec =>
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))();
TextColumn get downloadMusicCodec =>
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.m4a.name))();
BoolColumn get discordPresence => BoolColumn get discordPresence =>
boolean().withDefault(const Constant(true))(); boolean().withDefault(const Constant(true))();
BoolColumn get endlessPlayback => BoolColumn get endlessPlayback =>
@ -122,7 +94,6 @@ class PreferencesTable extends Table {
static PreferencesTableData defaults() { static PreferencesTableData defaults() {
return PreferencesTableData( return PreferencesTableData(
id: 0, id: 0,
audioQuality: SourceQualities.high,
albumColorSync: true, albumColorSync: true,
amoledDarkTheme: false, amoledDarkTheme: false,
checkUpdate: true, checkUpdate: true,
@ -138,13 +109,11 @@ class PreferencesTable extends Table {
searchMode: SearchMode.youtube, searchMode: SearchMode.youtube,
downloadLocation: "", downloadLocation: "",
localLibraryLocation: [], localLibraryLocation: [],
pipedInstance: "https://pipedapi.kavin.rocks",
invidiousInstance: "https://inv.nadeko.net",
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
audioSource: AudioSource.youtube, audioSourceId: null,
youtubeClientEngine: YoutubeClientEngine.youtubeExplode, youtubeClientEngine: kIsIOS
streamMusicCodec: SourceCodecs.m4a, ? YoutubeClientEngine.youtubeExplode
downloadMusicCodec: SourceCodecs.m4a, : YoutubeClientEngine.newPipe,
discordPresence: true, discordPresence: true,
endlessPlayback: true, endlessPlayback: true,
enableConnect: false, enableConnect: false,

View File

@ -1,25 +1,9 @@
part of '../database.dart'; part of '../database.dart';
enum SourceType {
youtube._("YouTube"),
youtubeMusic._("YouTube Music"),
jiosaavn._("JioSaavn");
final String label;
const SourceType._(this.label);
}
@TableIndex(
name: "uniq_track_match",
columns: {#trackId, #sourceId, #sourceType},
unique: true,
)
class SourceMatchTable extends Table { class SourceMatchTable extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get trackId => text()(); TextColumn get trackId => text()();
TextColumn get sourceId => text()(); TextColumn get sourceInfo => text().withDefault(const Constant("{}"))();
TextColumn get sourceType => TextColumn get sourceType => text()();
textEnum<SourceType>().withDefault(Constant(SourceType.youtube.name))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
} }

View File

@ -0,0 +1,110 @@
part of 'metadata.dart';
final oneOptionalDecimalFormatter = NumberFormat('0.#', 'en_US');
enum SpotubeMediaCompressionType {
lossy,
lossless,
}
@Freezed(unionKey: 'type')
class SpotubeAudioSourceContainerPreset
with _$SpotubeAudioSourceContainerPreset {
const SpotubeAudioSourceContainerPreset._();
@FreezedUnionValue("lossy")
factory SpotubeAudioSourceContainerPreset.lossy({
required SpotubeMediaCompressionType type,
required String name,
required List<SpotubeAudioLossyContainerQuality> qualities,
}) = SpotubeAudioSourceContainerPresetLossy;
@FreezedUnionValue("lossless")
factory SpotubeAudioSourceContainerPreset.lossless({
required SpotubeMediaCompressionType type,
required String name,
required List<SpotubeAudioLosslessContainerQuality> qualities,
}) = SpotubeAudioSourceContainerPresetLossless;
factory SpotubeAudioSourceContainerPreset.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioSourceContainerPresetFromJson(json);
String getFileExtension() {
return switch (name) {
"mp4" => "m4a",
"webm" => "weba",
_ => name,
};
}
}
@freezed
class SpotubeAudioLossyContainerQuality
with _$SpotubeAudioLossyContainerQuality {
const SpotubeAudioLossyContainerQuality._();
factory SpotubeAudioLossyContainerQuality({
required int bitrate, // bits per second
}) = _SpotubeAudioLossyContainerQuality;
factory SpotubeAudioLossyContainerQuality.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioLossyContainerQualityFromJson(json);
@override
toString() {
return "${oneOptionalDecimalFormatter.format(bitrate / 1000)}kbps";
}
}
@freezed
class SpotubeAudioLosslessContainerQuality
with _$SpotubeAudioLosslessContainerQuality {
const SpotubeAudioLosslessContainerQuality._();
factory SpotubeAudioLosslessContainerQuality({
required int bitDepth, // bit
required int sampleRate, // hz
}) = _SpotubeAudioLosslessContainerQuality;
factory SpotubeAudioLosslessContainerQuality.fromJson(
Map<String, dynamic> json) =>
_$SpotubeAudioLosslessContainerQualityFromJson(json);
@override
toString() {
return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz";
}
}
@freezed
class SpotubeAudioSourceMatchObject with _$SpotubeAudioSourceMatchObject {
factory SpotubeAudioSourceMatchObject({
required String id,
required String title,
required List<String> artists,
required Duration duration,
String? thumbnail,
required String externalUri,
}) = _SpotubeAudioSourceMatchObject;
factory SpotubeAudioSourceMatchObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeAudioSourceMatchObjectFromJson(json);
}
@freezed
class SpotubeAudioSourceStreamObject with _$SpotubeAudioSourceStreamObject {
factory SpotubeAudioSourceStreamObject({
required String url,
required String container,
required SpotubeMediaCompressionType type,
String? codec,
double? bitrate,
int? bitDepth,
double? sampleRate,
}) = _SpotubeAudioSourceStreamObject;
factory SpotubeAudioSourceStreamObject.fromJson(Map<String, dynamic> json) =>
_$SpotubeAudioSourceStreamObjectFromJson(json);
}

View File

@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -15,6 +16,7 @@ import 'package:spotube/utils/primitive_utils.dart';
part 'metadata.g.dart'; part 'metadata.g.dart';
part 'metadata.freezed.dart'; part 'metadata.freezed.dart';
part 'audio_source.dart';
part 'album.dart'; part 'album.dart';
part 'artist.dart'; part 'artist.dart';
part 'browse.dart'; part 'browse.dart';

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,123 @@ part of 'metadata.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_$SpotubeAudioSourceContainerPresetLossyImpl
_$$SpotubeAudioSourceContainerPresetLossyImplFromJson(Map json) =>
_$SpotubeAudioSourceContainerPresetLossyImpl(
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
name: json['name'] as String,
qualities: (json['qualities'] as List<dynamic>)
.map((e) => SpotubeAudioLossyContainerQuality.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLossyImplToJson(
_$SpotubeAudioSourceContainerPresetLossyImpl instance) =>
<String, dynamic>{
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
'name': instance.name,
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
};
const _$SpotubeMediaCompressionTypeEnumMap = {
SpotubeMediaCompressionType.lossy: 'lossy',
SpotubeMediaCompressionType.lossless: 'lossless',
};
_$SpotubeAudioSourceContainerPresetLosslessImpl
_$$SpotubeAudioSourceContainerPresetLosslessImplFromJson(Map json) =>
_$SpotubeAudioSourceContainerPresetLosslessImpl(
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
name: json['name'] as String,
qualities: (json['qualities'] as List<dynamic>)
.map((e) => SpotubeAudioLosslessContainerQuality.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(),
);
Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLosslessImplToJson(
_$SpotubeAudioSourceContainerPresetLosslessImpl instance) =>
<String, dynamic>{
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
'name': instance.name,
'qualities': instance.qualities.map((e) => e.toJson()).toList(),
};
_$SpotubeAudioLossyContainerQualityImpl
_$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) =>
_$SpotubeAudioLossyContainerQualityImpl(
bitrate: (json['bitrate'] as num).toInt(),
);
Map<String, dynamic> _$$SpotubeAudioLossyContainerQualityImplToJson(
_$SpotubeAudioLossyContainerQualityImpl instance) =>
<String, dynamic>{
'bitrate': instance.bitrate,
};
_$SpotubeAudioLosslessContainerQualityImpl
_$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) =>
_$SpotubeAudioLosslessContainerQualityImpl(
bitDepth: (json['bitDepth'] as num).toInt(),
sampleRate: (json['sampleRate'] as num).toInt(),
);
Map<String, dynamic> _$$SpotubeAudioLosslessContainerQualityImplToJson(
_$SpotubeAudioLosslessContainerQualityImpl instance) =>
<String, dynamic>{
'bitDepth': instance.bitDepth,
'sampleRate': instance.sampleRate,
};
_$SpotubeAudioSourceMatchObjectImpl
_$$SpotubeAudioSourceMatchObjectImplFromJson(Map json) =>
_$SpotubeAudioSourceMatchObjectImpl(
id: json['id'] as String,
title: json['title'] as String,
artists: (json['artists'] as List<dynamic>)
.map((e) => e as String)
.toList(),
duration: Duration(microseconds: (json['duration'] as num).toInt()),
thumbnail: json['thumbnail'] as String?,
externalUri: json['externalUri'] as String,
);
Map<String, dynamic> _$$SpotubeAudioSourceMatchObjectImplToJson(
_$SpotubeAudioSourceMatchObjectImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'artists': instance.artists,
'duration': instance.duration.inMicroseconds,
'thumbnail': instance.thumbnail,
'externalUri': instance.externalUri,
};
_$SpotubeAudioSourceStreamObjectImpl
_$$SpotubeAudioSourceStreamObjectImplFromJson(Map json) =>
_$SpotubeAudioSourceStreamObjectImpl(
url: json['url'] as String,
container: json['container'] as String,
type: $enumDecode(_$SpotubeMediaCompressionTypeEnumMap, json['type']),
codec: json['codec'] as String?,
bitrate: (json['bitrate'] as num?)?.toDouble(),
bitDepth: (json['bitDepth'] as num?)?.toInt(),
sampleRate: (json['sampleRate'] as num?)?.toDouble(),
);
Map<String, dynamic> _$$SpotubeAudioSourceStreamObjectImplToJson(
_$SpotubeAudioSourceStreamObjectImpl instance) =>
<String, dynamic>{
'url': instance.url,
'container': instance.container,
'type': _$SpotubeMediaCompressionTypeEnumMap[instance.type]!,
'codec': instance.codec,
'bitrate': instance.bitrate,
'bitDepth': instance.bitDepth,
'sampleRate': instance.sampleRate,
};
_$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) => _$SpotubeFullAlbumObjectImpl _$$SpotubeFullAlbumObjectImplFromJson(Map json) =>
_$SpotubeFullAlbumObjectImpl( _$SpotubeFullAlbumObjectImpl(
id: json['id'] as String, id: json['id'] as String,
@ -419,7 +536,6 @@ Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) => _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
_$PluginConfigurationImpl( _$PluginConfigurationImpl(
type: $enumDecode(_$PluginTypeEnumMap, json['type']),
name: json['name'] as String, name: json['name'] as String,
description: json['description'] as String, description: json['description'] as String,
version: json['version'] as String, version: json['version'] as String,
@ -440,7 +556,6 @@ _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
Map<String, dynamic> _$$PluginConfigurationImplToJson( Map<String, dynamic> _$$PluginConfigurationImplToJson(
_$PluginConfigurationImpl instance) => _$PluginConfigurationImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'type': _$PluginTypeEnumMap[instance.type]!,
'name': instance.name, 'name': instance.name,
'description': instance.description, 'description': instance.description,
'version': instance.version, 'version': instance.version,
@ -453,10 +568,6 @@ Map<String, dynamic> _$$PluginConfigurationImplToJson(
'repository': instance.repository, 'repository': instance.repository,
}; };
const _$PluginTypeEnumMap = {
PluginType.metadata: 'metadata',
};
const _$PluginApisEnumMap = { const _$PluginApisEnumMap = {
PluginApis.webview: 'webview', PluginApis.webview: 'webview',
PluginApis.localstorage: 'localstorage', PluginApis.localstorage: 'localstorage',
@ -466,6 +577,8 @@ const _$PluginApisEnumMap = {
const _$PluginAbilitiesEnumMap = { const _$PluginAbilitiesEnumMap = {
PluginAbilities.authentication: 'authentication', PluginAbilities.authentication: 'authentication',
PluginAbilities.scrobbling: 'scrobbling', PluginAbilities.scrobbling: 'scrobbling',
PluginAbilities.metadata: 'metadata',
PluginAbilities.audioSource: 'audio-source',
}; };
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) => _$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
@ -490,6 +603,8 @@ _$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
owner: json['owner'] as String, owner: json['owner'] as String,
description: json['description'] as String, description: json['description'] as String,
repoUrl: json['repoUrl'] as String, repoUrl: json['repoUrl'] as String,
topics:
(json['topics'] as List<dynamic>).map((e) => e as String).toList(),
); );
Map<String, dynamic> _$$MetadataPluginRepositoryImplToJson( Map<String, dynamic> _$$MetadataPluginRepositoryImplToJson(
@ -499,4 +614,5 @@ Map<String, dynamic> _$$MetadataPluginRepositoryImplToJson(
'owner': instance.owner, 'owner': instance.owner,
'description': instance.description, 'description': instance.description,
'repoUrl': instance.repoUrl, 'repoUrl': instance.repoUrl,
'topics': instance.topics,
}; };

View File

@ -1,17 +1,20 @@
part of 'metadata.dart'; part of 'metadata.dart';
enum PluginType { metadata }
enum PluginApis { webview, localstorage, timezone } enum PluginApis { webview, localstorage, timezone }
enum PluginAbilities { authentication, scrobbling } enum PluginAbilities {
authentication,
scrobbling,
metadata,
@JsonValue('audio-source')
audioSource,
}
@freezed @freezed
class PluginConfiguration with _$PluginConfiguration { class PluginConfiguration with _$PluginConfiguration {
const PluginConfiguration._(); const PluginConfiguration._();
factory PluginConfiguration({ factory PluginConfiguration({
required PluginType type,
required String name, required String name,
required String description, required String description,
required String version, required String version,

View File

@ -7,6 +7,7 @@ class MetadataPluginRepository with _$MetadataPluginRepository {
required String owner, required String owner,
required String description, required String description,
required String repoUrl, required String repoUrl,
required List<String> topics,
}) = _MetadataPluginRepository; }) = _MetadataPluginRepository;
factory MetadataPluginRepository.fromJson(Map<String, dynamic> json) => factory MetadataPluginRepository.fromJson(Map<String, dynamic> json) =>

View File

@ -1,121 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
part 'track_sources.freezed.dart';
part 'track_sources.g.dart'; part 'track_sources.g.dart';
@freezed
class TrackSourceQuery with _$TrackSourceQuery {
TrackSourceQuery._();
factory TrackSourceQuery({
required String id,
required String title,
required List<String> artists,
required String album,
required int durationMs,
required String isrc,
required bool explicit,
}) = _TrackSourceQuery;
factory TrackSourceQuery.fromJson(Map<String, dynamic> json) =>
_$TrackSourceQueryFromJson(json);
factory TrackSourceQuery.fromTrack(SpotubeFullTrackObject track) {
return TrackSourceQuery(
id: track.id,
title: track.name,
artists: track.artists.map((e) => e.name).toList(),
album: track.album.name,
durationMs: track.durationMs,
isrc: track.isrc,
explicit: track.explicit,
);
}
/// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery].
factory TrackSourceQuery.parseUri(String url) {
final isLocal = !url.startsWith("http");
if (isLocal) {
try {
return TrackSourceQuery(
id: url,
title: '',
artists: [],
album: '',
durationMs: 0,
isrc: '',
explicit: false,
);
} catch (e, stackTrace) {
AppLogger.log.e(
"Failed to parse local track URI: $url\n$e",
stackTrace: stackTrace,
);
}
}
final uri = Uri.parse(url);
return TrackSourceQuery(
id: uri.pathSegments.last,
title: uri.queryParameters['title'] ?? '',
artists: uri.queryParameters['artists']?.split(',') ?? [],
album: uri.queryParameters['album'] ?? '',
durationMs: int.tryParse(uri.queryParameters['durationMs'] ?? '0') ?? 0,
isrc: uri.queryParameters['isrc'] ?? '',
explicit: uri.queryParameters['explicit']?.toLowerCase() == 'true',
);
}
String queryString() {
return toJson()
.entries
.map((e) =>
"${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List<String> ? e.value.join(",") : e.value.toString())}")
.join("&");
}
}
@freezed
class TrackSourceInfo with _$TrackSourceInfo {
factory TrackSourceInfo({
required String id,
required String title,
required String artists,
required String thumbnail,
required String pageUrl,
required int durationMs,
}) = _TrackSourceInfo;
factory TrackSourceInfo.fromJson(Map<String, dynamic> json) =>
_$TrackSourceInfoFromJson(json);
}
@freezed
class TrackSource with _$TrackSource {
factory TrackSource({
required String url,
required SourceQualities quality,
required SourceCodecs codec,
required String bitrate,
}) = _TrackSource;
factory TrackSource.fromJson(Map<String, dynamic> json) =>
_$TrackSourceFromJson(json);
}
@JsonSerializable() @JsonSerializable()
class BasicSourcedTrack { class BasicSourcedTrack {
final TrackSourceQuery query; final SpotubeFullTrackObject query;
final AudioSource source; final SpotubeAudioSourceMatchObject info;
final TrackSourceInfo info; final String source;
final List<TrackSource> sources; final List<SpotubeAudioSourceStreamObject> sources;
final List<TrackSourceInfo> siblings; final List<SpotubeAudioSourceMatchObject> siblings;
BasicSourcedTrack({ BasicSourcedTrack({
required this.query, required this.query,
required this.source, required this.source,

View File

@ -1,739 +0,0 @@
// 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 'track_sources.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#adding-getters-and-methods-to-our-models');
TrackSourceQuery _$TrackSourceQueryFromJson(Map<String, dynamic> json) {
return _TrackSourceQuery.fromJson(json);
}
/// @nodoc
mixin _$TrackSourceQuery {
String get id => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
List<String> get artists => throw _privateConstructorUsedError;
String get album => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError;
String get isrc => throw _privateConstructorUsedError;
bool get explicit => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$TrackSourceQueryCopyWith<TrackSourceQuery> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrackSourceQueryCopyWith<$Res> {
factory $TrackSourceQueryCopyWith(
TrackSourceQuery value, $Res Function(TrackSourceQuery) then) =
_$TrackSourceQueryCopyWithImpl<$Res, TrackSourceQuery>;
@useResult
$Res call(
{String id,
String title,
List<String> artists,
String album,
int durationMs,
String isrc,
bool explicit});
}
/// @nodoc
class _$TrackSourceQueryCopyWithImpl<$Res, $Val extends TrackSourceQuery>
implements $TrackSourceQueryCopyWith<$Res> {
_$TrackSourceQueryCopyWithImpl(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? id = null,
Object? title = null,
Object? artists = null,
Object? album = null,
Object? durationMs = null,
Object? isrc = null,
Object? explicit = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value.artists
: artists // ignore: cast_nullable_to_non_nullable
as List<String>,
album: null == album
? _value.album
: album // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
isrc: null == isrc
? _value.isrc
: isrc // ignore: cast_nullable_to_non_nullable
as String,
explicit: null == explicit
? _value.explicit
: explicit // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$TrackSourceQueryImplCopyWith<$Res>
implements $TrackSourceQueryCopyWith<$Res> {
factory _$$TrackSourceQueryImplCopyWith(_$TrackSourceQueryImpl value,
$Res Function(_$TrackSourceQueryImpl) then) =
__$$TrackSourceQueryImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String title,
List<String> artists,
String album,
int durationMs,
String isrc,
bool explicit});
}
/// @nodoc
class __$$TrackSourceQueryImplCopyWithImpl<$Res>
extends _$TrackSourceQueryCopyWithImpl<$Res, _$TrackSourceQueryImpl>
implements _$$TrackSourceQueryImplCopyWith<$Res> {
__$$TrackSourceQueryImplCopyWithImpl(_$TrackSourceQueryImpl _value,
$Res Function(_$TrackSourceQueryImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = null,
Object? artists = null,
Object? album = null,
Object? durationMs = null,
Object? isrc = null,
Object? explicit = null,
}) {
return _then(_$TrackSourceQueryImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value._artists
: artists // ignore: cast_nullable_to_non_nullable
as List<String>,
album: null == album
? _value.album
: album // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
isrc: null == isrc
? _value.isrc
: isrc // ignore: cast_nullable_to_non_nullable
as String,
explicit: null == explicit
? _value.explicit
: explicit // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TrackSourceQueryImpl extends _TrackSourceQuery {
_$TrackSourceQueryImpl(
{required this.id,
required this.title,
required final List<String> artists,
required this.album,
required this.durationMs,
required this.isrc,
required this.explicit})
: _artists = artists,
super._();
factory _$TrackSourceQueryImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceQueryImplFromJson(json);
@override
final String id;
@override
final String title;
final List<String> _artists;
@override
List<String> get artists {
if (_artists is EqualUnmodifiableListView) return _artists;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_artists);
}
@override
final String album;
@override
final int durationMs;
@override
final String isrc;
@override
final bool explicit;
@override
String toString() {
return 'TrackSourceQuery(id: $id, title: $title, artists: $artists, album: $album, durationMs: $durationMs, isrc: $isrc, explicit: $explicit)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrackSourceQueryImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.title, title) || other.title == title) &&
const DeepCollectionEquality().equals(other._artists, _artists) &&
(identical(other.album, album) || other.album == album) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.isrc, isrc) || other.isrc == isrc) &&
(identical(other.explicit, explicit) ||
other.explicit == explicit));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
id,
title,
const DeepCollectionEquality().hash(_artists),
album,
durationMs,
isrc,
explicit);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
__$$TrackSourceQueryImplCopyWithImpl<_$TrackSourceQueryImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TrackSourceQueryImplToJson(
this,
);
}
}
abstract class _TrackSourceQuery extends TrackSourceQuery {
factory _TrackSourceQuery(
{required final String id,
required final String title,
required final List<String> artists,
required final String album,
required final int durationMs,
required final String isrc,
required final bool explicit}) = _$TrackSourceQueryImpl;
_TrackSourceQuery._() : super._();
factory _TrackSourceQuery.fromJson(Map<String, dynamic> json) =
_$TrackSourceQueryImpl.fromJson;
@override
String get id;
@override
String get title;
@override
List<String> get artists;
@override
String get album;
@override
int get durationMs;
@override
String get isrc;
@override
bool get explicit;
@override
@JsonKey(ignore: true)
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
throw _privateConstructorUsedError;
}
TrackSourceInfo _$TrackSourceInfoFromJson(Map<String, dynamic> json) {
return _TrackSourceInfo.fromJson(json);
}
/// @nodoc
mixin _$TrackSourceInfo {
String get id => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
String get artists => throw _privateConstructorUsedError;
String get thumbnail => throw _privateConstructorUsedError;
String get pageUrl => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$TrackSourceInfoCopyWith<TrackSourceInfo> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrackSourceInfoCopyWith<$Res> {
factory $TrackSourceInfoCopyWith(
TrackSourceInfo value, $Res Function(TrackSourceInfo) then) =
_$TrackSourceInfoCopyWithImpl<$Res, TrackSourceInfo>;
@useResult
$Res call(
{String id,
String title,
String artists,
String thumbnail,
String pageUrl,
int durationMs});
}
/// @nodoc
class _$TrackSourceInfoCopyWithImpl<$Res, $Val extends TrackSourceInfo>
implements $TrackSourceInfoCopyWith<$Res> {
_$TrackSourceInfoCopyWithImpl(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? id = null,
Object? title = null,
Object? artists = null,
Object? thumbnail = null,
Object? pageUrl = null,
Object? durationMs = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value.artists
: artists // ignore: cast_nullable_to_non_nullable
as String,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
pageUrl: null == pageUrl
? _value.pageUrl
: pageUrl // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$TrackSourceInfoImplCopyWith<$Res>
implements $TrackSourceInfoCopyWith<$Res> {
factory _$$TrackSourceInfoImplCopyWith(_$TrackSourceInfoImpl value,
$Res Function(_$TrackSourceInfoImpl) then) =
__$$TrackSourceInfoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String title,
String artists,
String thumbnail,
String pageUrl,
int durationMs});
}
/// @nodoc
class __$$TrackSourceInfoImplCopyWithImpl<$Res>
extends _$TrackSourceInfoCopyWithImpl<$Res, _$TrackSourceInfoImpl>
implements _$$TrackSourceInfoImplCopyWith<$Res> {
__$$TrackSourceInfoImplCopyWithImpl(
_$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = null,
Object? artists = null,
Object? thumbnail = null,
Object? pageUrl = null,
Object? durationMs = null,
}) {
return _then(_$TrackSourceInfoImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value.artists
: artists // ignore: cast_nullable_to_non_nullable
as String,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
pageUrl: null == pageUrl
? _value.pageUrl
: pageUrl // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TrackSourceInfoImpl implements _TrackSourceInfo {
_$TrackSourceInfoImpl(
{required this.id,
required this.title,
required this.artists,
required this.thumbnail,
required this.pageUrl,
required this.durationMs});
factory _$TrackSourceInfoImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceInfoImplFromJson(json);
@override
final String id;
@override
final String title;
@override
final String artists;
@override
final String thumbnail;
@override
final String pageUrl;
@override
final int durationMs;
@override
String toString() {
return 'TrackSourceInfo(id: $id, title: $title, artists: $artists, thumbnail: $thumbnail, pageUrl: $pageUrl, durationMs: $durationMs)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrackSourceInfoImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.artists, artists) || other.artists == artists) &&
(identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) &&
(identical(other.pageUrl, pageUrl) || other.pageUrl == pageUrl) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType, id, title, artists, thumbnail, pageUrl, durationMs);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
__$$TrackSourceInfoImplCopyWithImpl<_$TrackSourceInfoImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TrackSourceInfoImplToJson(
this,
);
}
}
abstract class _TrackSourceInfo implements TrackSourceInfo {
factory _TrackSourceInfo(
{required final String id,
required final String title,
required final String artists,
required final String thumbnail,
required final String pageUrl,
required final int durationMs}) = _$TrackSourceInfoImpl;
factory _TrackSourceInfo.fromJson(Map<String, dynamic> json) =
_$TrackSourceInfoImpl.fromJson;
@override
String get id;
@override
String get title;
@override
String get artists;
@override
String get thumbnail;
@override
String get pageUrl;
@override
int get durationMs;
@override
@JsonKey(ignore: true)
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
TrackSource _$TrackSourceFromJson(Map<String, dynamic> json) {
return _TrackSource.fromJson(json);
}
/// @nodoc
mixin _$TrackSource {
String get url => throw _privateConstructorUsedError;
SourceQualities get quality => throw _privateConstructorUsedError;
SourceCodecs get codec => throw _privateConstructorUsedError;
String get bitrate => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$TrackSourceCopyWith<TrackSource> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrackSourceCopyWith<$Res> {
factory $TrackSourceCopyWith(
TrackSource value, $Res Function(TrackSource) then) =
_$TrackSourceCopyWithImpl<$Res, TrackSource>;
@useResult
$Res call(
{String url,
SourceQualities quality,
SourceCodecs codec,
String bitrate});
}
/// @nodoc
class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource>
implements $TrackSourceCopyWith<$Res> {
_$TrackSourceCopyWithImpl(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? url = null,
Object? quality = null,
Object? codec = null,
Object? bitrate = null,
}) {
return _then(_value.copyWith(
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
quality: null == quality
? _value.quality
: quality // ignore: cast_nullable_to_non_nullable
as SourceQualities,
codec: null == codec
? _value.codec
: codec // ignore: cast_nullable_to_non_nullable
as SourceCodecs,
bitrate: null == bitrate
? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$TrackSourceImplCopyWith<$Res>
implements $TrackSourceCopyWith<$Res> {
factory _$$TrackSourceImplCopyWith(
_$TrackSourceImpl value, $Res Function(_$TrackSourceImpl) then) =
__$$TrackSourceImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String url,
SourceQualities quality,
SourceCodecs codec,
String bitrate});
}
/// @nodoc
class __$$TrackSourceImplCopyWithImpl<$Res>
extends _$TrackSourceCopyWithImpl<$Res, _$TrackSourceImpl>
implements _$$TrackSourceImplCopyWith<$Res> {
__$$TrackSourceImplCopyWithImpl(
_$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? url = null,
Object? quality = null,
Object? codec = null,
Object? bitrate = null,
}) {
return _then(_$TrackSourceImpl(
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
quality: null == quality
? _value.quality
: quality // ignore: cast_nullable_to_non_nullable
as SourceQualities,
codec: null == codec
? _value.codec
: codec // ignore: cast_nullable_to_non_nullable
as SourceCodecs,
bitrate: null == bitrate
? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TrackSourceImpl implements _TrackSource {
_$TrackSourceImpl(
{required this.url,
required this.quality,
required this.codec,
required this.bitrate});
factory _$TrackSourceImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceImplFromJson(json);
@override
final String url;
@override
final SourceQualities quality;
@override
final SourceCodecs codec;
@override
final String bitrate;
@override
String toString() {
return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrackSourceImpl &&
(identical(other.url, url) || other.url == url) &&
(identical(other.quality, quality) || other.quality == quality) &&
(identical(other.codec, codec) || other.codec == codec) &&
(identical(other.bitrate, bitrate) || other.bitrate == bitrate));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, url, quality, codec, bitrate);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
__$$TrackSourceImplCopyWithImpl<_$TrackSourceImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TrackSourceImplToJson(
this,
);
}
}
abstract class _TrackSource implements TrackSource {
factory _TrackSource(
{required final String url,
required final SourceQualities quality,
required final SourceCodecs codec,
required final String bitrate}) = _$TrackSourceImpl;
factory _TrackSource.fromJson(Map<String, dynamic> json) =
_$TrackSourceImpl.fromJson;
@override
String get url;
@override
SourceQualities get quality;
@override
SourceCodecs get codec;
@override
String get bitrate;
@override
@JsonKey(ignore: true)
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -7,17 +7,18 @@ part of 'track_sources.dart';
// ************************************************************************** // **************************************************************************
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack( BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
query: TrackSourceQuery.fromJson( query: SpotubeFullTrackObject.fromJson(
Map<String, dynamic>.from(json['query'] as Map)), Map<String, dynamic>.from(json['query'] as Map)),
source: $enumDecode(_$AudioSourceEnumMap, json['source']), source: json['source'] as String,
info: TrackSourceInfo.fromJson( info: SpotubeAudioSourceMatchObject.fromJson(
Map<String, dynamic>.from(json['info'] as Map)), Map<String, dynamic>.from(json['info'] as Map)),
sources: (json['sources'] as List<dynamic>) sources: (json['sources'] as List<dynamic>)
.map((e) => TrackSource.fromJson(Map<String, dynamic>.from(e as Map))) .map((e) => SpotubeAudioSourceStreamObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(), .toList(),
siblings: (json['siblings'] as List<dynamic>?) siblings: (json['siblings'] as List<dynamic>?)
?.map((e) => ?.map((e) => SpotubeAudioSourceMatchObject.fromJson(
TrackSourceInfo.fromJson(Map<String, dynamic>.from(e as Map))) Map<String, dynamic>.from(e as Map)))
.toList() ?? .toList() ??
const [], const [],
); );
@ -25,86 +26,8 @@ BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
Map<String, dynamic> _$BasicSourcedTrackToJson(BasicSourcedTrack instance) => Map<String, dynamic> _$BasicSourcedTrackToJson(BasicSourcedTrack instance) =>
<String, dynamic>{ <String, dynamic>{
'query': instance.query.toJson(), 'query': instance.query.toJson(),
'source': _$AudioSourceEnumMap[instance.source]!,
'info': instance.info.toJson(), 'info': instance.info.toJson(),
'source': instance.source,
'sources': instance.sources.map((e) => e.toJson()).toList(), 'sources': instance.sources.map((e) => e.toJson()).toList(),
'siblings': instance.siblings.map((e) => e.toJson()).toList(), 'siblings': instance.siblings.map((e) => e.toJson()).toList(),
}; };
const _$AudioSourceEnumMap = {
AudioSource.youtube: 'youtube',
AudioSource.piped: 'piped',
AudioSource.jiosaavn: 'jiosaavn',
AudioSource.invidious: 'invidious',
};
_$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) =>
_$TrackSourceQueryImpl(
id: json['id'] as String,
title: json['title'] as String,
artists:
(json['artists'] as List<dynamic>).map((e) => e as String).toList(),
album: json['album'] as String,
durationMs: (json['durationMs'] as num).toInt(),
isrc: json['isrc'] as String,
explicit: json['explicit'] as bool,
);
Map<String, dynamic> _$$TrackSourceQueryImplToJson(
_$TrackSourceQueryImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'artists': instance.artists,
'album': instance.album,
'durationMs': instance.durationMs,
'isrc': instance.isrc,
'explicit': instance.explicit,
};
_$TrackSourceInfoImpl _$$TrackSourceInfoImplFromJson(Map json) =>
_$TrackSourceInfoImpl(
id: json['id'] as String,
title: json['title'] as String,
artists: json['artists'] as String,
thumbnail: json['thumbnail'] as String,
pageUrl: json['pageUrl'] as String,
durationMs: (json['durationMs'] as num).toInt(),
);
Map<String, dynamic> _$$TrackSourceInfoImplToJson(
_$TrackSourceInfoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'artists': instance.artists,
'thumbnail': instance.thumbnail,
'pageUrl': instance.pageUrl,
'durationMs': instance.durationMs,
};
_$TrackSourceImpl _$$TrackSourceImplFromJson(Map json) => _$TrackSourceImpl(
url: json['url'] as String,
quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']),
codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']),
bitrate: json['bitrate'] as String,
);
Map<String, dynamic> _$$TrackSourceImplToJson(_$TrackSourceImpl instance) =>
<String, dynamic>{
'url': instance.url,
'quality': _$SourceQualitiesEnumMap[instance.quality]!,
'codec': _$SourceCodecsEnumMap[instance.codec]!,
'bitrate': instance.bitrate,
};
const _$SourceQualitiesEnumMap = {
SourceQualities.high: 'high',
SourceQualities.medium: 'medium',
SourceQualities.low: 'low',
};
const _$SourceCodecsEnumMap = {
SourceCodecs.m4a: 'm4a',
SourceCodecs.weba: 'weba',
};

View File

@ -6,9 +6,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
final codecs = SourceCodecs.values.map((s) => s.name); const containers = ["m4a", "mp3", "mp4", "ogg", "wav", "flac"];
class LocalFolderCacheExportDialog extends HookConsumerWidget { class LocalFolderCacheExportDialog extends HookConsumerWidget {
final Directory exportDir; final Directory exportDir;
@ -30,7 +29,8 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
final stream = cacheDir.list().where( final stream = cacheDir.list().where(
(event) => (event) =>
event is File && event is File &&
codecs.contains(path.extension(event.path).replaceAll(".", "")), containers
.contains(path.extension(event.path).replaceAll(".", "")),
); );
stream.listen( stream.listen(

View File

@ -7,44 +7,19 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/services/download_manager/download_status.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
class DownloadItem extends HookConsumerWidget { class DownloadItem extends HookConsumerWidget {
final SpotubeFullTrackObject track; final DownloadTask task;
const DownloadItem({ const DownloadItem({
super.key, super.key,
required this.track, required this.task,
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final downloadManager = ref.watch(downloadManagerProvider); final downloadManager = ref.watch(downloadManagerProvider.notifier);
final taskStatus = useState<DownloadStatus?>(null);
useEffect(() {
if (track is! SourcedTrack) return null;
final notifier = downloadManager.getStatusNotifier(track);
taskStatus.value = notifier?.value;
void listener() {
taskStatus.value = notifier?.value;
}
notifier?.addListener(listener);
return () {
notifier?.removeListener(listener);
};
}, [track]);
final isQueryingSourceInfo =
taskStatus.value == null || track is! SourcedTrack;
return ButtonTile( return ButtonTile(
style: ButtonVariance.ghost, style: ButtonVariance.ghost,
@ -55,64 +30,46 @@ class DownloadItem extends HookConsumerWidget {
child: UniversalImage( child: UniversalImage(
height: 40, height: 40,
width: 40, width: 40,
path: track.album.images.asUrlString( path: task.track.album.images.asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
), ),
), ),
), ),
), ),
title: Text(track.name), title: Text(task.track.name),
subtitle: ArtistLink( subtitle: ArtistLink(
artists: track.artists, artists: task.track.artists,
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () { onOverflowArtistClick: () {
context.navigateTo(TrackRoute(trackId: track.id)); context.navigateTo(TrackRoute(trackId: task.track.id));
}, },
), ),
trailing: isQueryingSourceInfo trailing: switch (task.status) {
? Text(context.l10n.querying_info).small()
: switch (taskStatus.value!) {
DownloadStatus.downloading => HookBuilder(builder: (context) { DownloadStatus.downloading => HookBuilder(builder: (context) {
final taskProgress = useListenable(useMemoized( return StreamBuilder(
() => downloadManager.getProgressNotifier(track), stream: task.downloadedBytesStream,
[track], builder: (context, asyncSnapshot) {
)); final progress =
task.totalSizeBytes == null || task.totalSizeBytes == 0
? 0
: (asyncSnapshot.data ?? 0) / task.totalSizeBytes!;
return Row( return Row(
children: [ children: [
CircularProgressIndicator( CircularProgressIndicator(
value: taskProgress?.value ?? 0, value: progress.toDouble(),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
IconButton.ghost(
icon: const Icon(SpotubeIcons.pause),
onPressed: () {
downloadManager.pause(track);
}),
const SizedBox(width: 10), const SizedBox(width: 10),
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.close), icon: const Icon(SpotubeIcons.close),
onPressed: () { onPressed: () {
downloadManager.cancel(track); downloadManager.cancel(task.track);
}), }),
], ],
); );
});
}), }),
DownloadStatus.paused => Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton.ghost(
icon: const Icon(SpotubeIcons.play),
onPressed: () {
downloadManager.resume(track);
}),
const SizedBox(width: 10),
IconButton.ghost(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
downloadManager.cancel(track);
})
],
),
DownloadStatus.failed || DownloadStatus.canceled => SizedBox( DownloadStatus.failed || DownloadStatus.canceled => SizedBox(
width: 100, width: 100,
child: Row( child: Row(
@ -125,7 +82,7 @@ class DownloadItem extends HookConsumerWidget {
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.refresh), icon: const Icon(SpotubeIcons.refresh),
onPressed: () { onPressed: () {
downloadManager.retry(track); downloadManager.retry(task.track);
}, },
), ),
], ],
@ -136,7 +93,7 @@ class DownloadItem extends HookConsumerWidget {
DownloadStatus.queued => IconButton.ghost( DownloadStatus.queued => IconButton.ghost(
icon: const Icon(SpotubeIcons.close), icon: const Icon(SpotubeIcons.close),
onPressed: () { onPressed: () {
downloadManager.removeFromQueue(track); downloadManager.cancel(task.track);
}), }),
}, },
); );

View File

@ -3,6 +3,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/markdown/markdown.dart'; import 'package:spotube/components/markdown/markdown.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart'; import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart';
@ -12,29 +13,60 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart'; import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
final validAbilities = {
PluginAbilities.metadata: ("Metadata", SpotubeIcons.album),
PluginAbilities.audioSource: ("Audio Source", SpotubeIcons.music),
};
class MetadataInstalledPluginItem extends HookConsumerWidget { class MetadataInstalledPluginItem extends HookConsumerWidget {
final PluginConfiguration plugin; final PluginConfiguration plugin;
final bool isDefault; final bool isDefaultMetadata;
final bool isDefaultAudioSource;
const MetadataInstalledPluginItem({ const MetadataInstalledPluginItem({
super.key, super.key,
required this.plugin, required this.plugin,
required this.isDefault, required this.isDefaultMetadata,
required this.isDefaultAudioSource,
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.sizeOf(context);
final metadataPlugin = ref.watch(metadataPluginProvider); final metadataPlugin = ref.watch(metadataPluginProvider);
final isAuthenticatedSnapshot = final audioSourcePlugin = ref.watch(audioSourcePluginProvider);
ref.watch(metadataPluginAuthenticatedProvider); final pluginSnapshot = switch ((isDefaultMetadata, isDefaultAudioSource)) {
(true, _) => metadataPlugin,
(false, true) => audioSourcePlugin,
_ => null,
};
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier); final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
final requiresAuth =
isDefault && plugin.abilities.contains(PluginAbilities.authentication); final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) &&
final supportsScrobbling = plugin.abilities.contains(PluginAbilities.authentication);
isDefault && plugin.abilities.contains(PluginAbilities.scrobbling); final supportsScrobbling = isDefaultMetadata &&
final isAuthenticated = isAuthenticatedSnapshot.asData?.value == true; plugin.abilities.contains(PluginAbilities.scrobbling);
final updateAvailable =
isDefault ? ref.watch(metadataPluginUpdateCheckerProvider) : null; final isMetadataAuthenticatedSnapshot =
final hasUpdate = isDefault && updateAvailable?.asData?.value != null; ref.watch(metadataPluginAuthenticatedProvider);
final isAudioSourceAuthenticatedSnapshot =
ref.watch(audioSourcePluginAuthenticatedProvider);
final isAuthenticated = (isDefaultMetadata &&
isMetadataAuthenticatedSnapshot.asData?.value == true) ||
(isDefaultAudioSource &&
isAudioSourceAuthenticatedSnapshot.asData?.value == true);
final metadataUpdateAvailable =
ref.watch(metadataPluginUpdateCheckerProvider);
final audioSourceUpdateAvailable =
ref.watch(audioSourcePluginUpdateCheckerProvider);
final updateAvailable = switch ((isDefaultMetadata, isDefaultAudioSource)) {
(true, _) => metadataUpdateAvailable,
(false, true) => audioSourceUpdateAvailable,
_ => null,
};
final hasUpdate = updateAvailable?.asData?.value != null;
return Card( return Card(
child: Column( child: Column(
@ -79,6 +111,18 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
spacing: 8, spacing: 8,
children: [ children: [
Text(plugin.description), Text(plugin.description),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final ability in plugin.abilities)
if (validAbilities.keys.contains(ability))
SecondaryBadge(
leading: Icon(validAbilities[ability]!.$2),
child: Text(validAbilities[ability]!.$1),
),
],
),
if (repoUrl != null) if (repoUrl != null)
Wrap( Wrap(
spacing: 8, spacing: 8,
@ -91,10 +135,27 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
) )
else ...[ else ...[
Text(context.l10n.author_name(plugin.author)), Text(context.l10n.author_name(plugin.author)),
DestructiveBadge( Container(
leading: const Icon(SpotubeIcons.warning), padding: const EdgeInsets.symmetric(
child: Text(context.l10n.third_party), horizontal: 6,
) vertical: 2,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
const Icon(SpotubeIcons.warning, size: 14),
Text(
context.l10n.third_party,
style: const TextStyle(color: Colors.white),
).xSmall
],
),
),
], ],
SecondaryBadge( SecondaryBadge(
leading: const Icon(SpotubeIcons.connect), leading: const Icon(SpotubeIcons.connect),
@ -183,34 +244,73 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
], ],
), ),
), ),
Row( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.spaceBetween,
children: [ children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (plugin.abilities.contains(PluginAbilities.metadata))
Button.secondary( Button.secondary(
enabled: !isDefault, enabled: !isDefaultMetadata,
onPressed: () async { onPressed: () async {
await pluginsNotifier.setDefaultPlugin(plugin); await pluginsNotifier.setDefaultMetadataPlugin(plugin);
}, },
child: Text( child: Text(
isDefault isDefaultMetadata
? context.l10n.default_plugin ? context.l10n.default_metadata_source
: context.l10n.set_default, : context.l10n.set_default_metadata_source,
), ),
), ),
if (isDefault) if (plugin.abilities.contains(PluginAbilities.audioSource))
Button.secondary(
enabled: !isDefaultAudioSource,
onPressed: () async {
await pluginsNotifier
.setDefaultAudioSourcePlugin(plugin);
},
child: Text(
isDefaultAudioSource
? context.l10n.default_audio_source
: context.l10n.set_default_audio_source,
),
),
],
),
Row(
mainAxisSize:
mediaQuery.smAndUp ? MainAxisSize.min : MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
spacing: 8,
children: [
if (isDefaultMetadata || isDefaultAudioSource)
Consumer(builder: (context, ref, _) { Consumer(builder: (context, ref, _) {
final supportTextSnapshot = final metadataSupportTextSnapshot =
ref.watch(metadataPluginSupportTextProvider); ref.watch(metadataPluginSupportTextProvider);
final audioSourceSupportTextSnapshot =
ref.watch(audioSourcePluginSupportTextProvider);
if (supportTextSnapshot.hasValue && final supportTextSnapshot =
supportTextSnapshot.value == null) { switch ((isDefaultMetadata, isDefaultAudioSource)) {
(true, _) => metadataSupportTextSnapshot,
(false, true) => audioSourceSupportTextSnapshot,
_ => null,
};
if ((supportTextSnapshot?.hasValue ?? false) &&
supportTextSnapshot?.value == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final bgColor = context.theme.brightness == Brightness.dark final bgColor =
context.theme.brightness == Brightness.dark
? const Color.fromARGB(255, 255, 145, 175) ? const Color.fromARGB(255, 255, 145, 175)
: Colors.pink[600]; : Colors.pink[600];
final textColor = context.theme.brightness == Brightness.dark final textColor =
context.theme.brightness == Brightness.dark
? Colors.pink[700] ? Colors.pink[700]
: Colors.pink[50]; : Colors.pink[50];
@ -241,8 +341,8 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: title: Text(
Text(context.l10n.support_plugin_development), context.l10n.support_plugin_development),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: mediaQuery.height * 0.8, maxHeight: mediaQuery.height * 0.8,
@ -252,7 +352,9 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
width: double.infinity, width: double.infinity,
child: SingleChildScrollView( child: SingleChildScrollView(
child: AppMarkdown( child: AppMarkdown(
data: supportTextSnapshot.value ?? "", data: supportTextSnapshot
?.asData?.value ??
"",
), ),
), ),
), ),
@ -271,22 +373,28 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
}, },
); );
}), }),
const Spacer(), if ((isDefaultMetadata || isDefaultAudioSource) &&
if (isDefault && requiresAuth && !isAuthenticated) requiresAuth &&
!isAuthenticated)
Button.primary( Button.primary(
onPressed: () async { onPressed: () async {
await metadataPlugin.asData?.value?.auth.authenticate(); await pluginSnapshot?.asData?.value?.auth
.authenticate();
}, },
leading: const Icon(SpotubeIcons.login), leading: const Icon(SpotubeIcons.login),
child: Text(context.l10n.login), child: Text(context.l10n.login),
) )
else if (isDefault && requiresAuth && isAuthenticated) else if ((isDefaultMetadata || isDefaultAudioSource) &&
requiresAuth &&
isAuthenticated)
Button.destructive( Button.destructive(
onPressed: () async { onPressed: () async {
await metadataPlugin.asData?.value?.auth.logout(); await pluginSnapshot?.asData?.value?.auth.logout();
}, },
leading: const Icon(SpotubeIcons.logout), leading: const Icon(SpotubeIcons.logout),
child: Text(context.l10n.logout), child: Text(context.l10n.logout),
),
],
) )
], ],
) )

View File

@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
@ -8,6 +9,12 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:change_case/change_case.dart';
final validTopics = {
"spotube-metadata-plugin": ("Metadata", SpotubeIcons.album),
"spotube-audio-source-plugin": ("Audio Source", SpotubeIcons.music),
};
class MetadataPluginRepositoryItem extends HookConsumerWidget { class MetadataPluginRepositoryItem extends HookConsumerWidget {
final MetadataPluginRepository pluginRepo; final MetadataPluginRepository pluginRepo;
@ -26,43 +33,21 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
final isInstalling = useState(false); final isInstalling = useState(false);
return Card( return Card(
child: Basic( child: Column(
title: Text(
"${pluginRepo.owner == "KRTirtho" ? "" : "${pluginRepo.owner}/"}${pluginRepo.name}"),
subtitle: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8, spacing: 8,
children: [ children: [
Text(pluginRepo.description), Basic(
Row( title: Text(
spacing: 8, pluginRepo.name.startsWith("spotube-plugin")
children: [ ? pluginRepo.name
if (pluginRepo.owner == "KRTirtho") ...[ .replaceFirst("spotube-plugin-", "")
PrimaryBadge( .trim()
leading: Icon(SpotubeIcons.done), .toCapitalCase()
child: Text(context.l10n.official), : pluginRepo.name.toCapitalCase(),
),
SecondaryBadge(
leading: host == "github.com"
? const Icon(SpotubeIcons.github)
: null,
child: Text(host),
onPressed: () {
launchUrlString(pluginRepo.repoUrl);
},
),
] else ...[
Text(context.l10n.author_name(pluginRepo.owner)),
DestructiveBadge(
leading: const Icon(SpotubeIcons.warning),
child: Text(context.l10n.third_party),
)
]
],
),
],
), ),
subtitle: Text(pluginRepo.description),
trailing: Button.primary( trailing: Button.primary(
enabled: !isInstalling.value, enabled: !isInstalling.value,
onPressed: () async { onPressed: () async {
@ -80,13 +65,14 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
context: context, context: context,
builder: (context) { builder: (context) {
final pluginAbilities = pluginConfig.apis final pluginAbilities = pluginConfig.apis
.map( .map((e) =>
(e) => context.l10n.can_access_name_api(e.name)) context.l10n.can_access_name_api(e.name))
.join("\n\n"); .join("\n\n");
return AlertDialog( return AlertDialog(
title: Text( title: Text(
context.l10n.do_you_want_to_install_this_plugin), context.l10n.do_you_want_to_install_this_plugin,
),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -95,8 +81,8 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
Text(context.l10n.third_party_plugin_warning), Text(context.l10n.third_party_plugin_warning),
const Gap(8), const Gap(8),
FutureBuilder( FutureBuilder(
future: future: pluginsNotifier
pluginsNotifier.getLogoPath(pluginConfig), .getLogoPath(pluginConfig),
builder: (context, snapshot) { builder: (context, snapshot) {
return Basic( return Basic(
leading: snapshot.hasData leading: snapshot.hasData
@ -110,16 +96,17 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
width: 36, width: 36,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: context color: context.theme
.theme.colorScheme.secondary, .colorScheme.secondary,
borderRadius: borderRadius:
BorderRadius.circular(8), BorderRadius.circular(8),
), ),
child: child: const Icon(
const Icon(SpotubeIcons.plugin), SpotubeIcons.plugin),
), ),
title: Text(pluginConfig.name), title: Text(pluginConfig.name),
subtitle: Text(pluginConfig.description), subtitle:
Text(pluginConfig.description),
); );
}, },
), ),
@ -160,11 +147,91 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
} }
}, },
leading: isInstalling.value leading: isInstalling.value
? const CircularProgressIndicator() ? SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(
color: context.theme.colorScheme.primaryForeground,
),
)
: const Icon(SpotubeIcons.add), : const Icon(SpotubeIcons.add),
child: Text(context.l10n.install), child: Text(context.l10n.install),
), ),
), ),
if (pluginRepo.owner != "KRTirtho")
Text.rich(
TextSpan(
children: [
TextSpan(text: context.l10n.source),
TextSpan(
text: pluginRepo.repoUrl.replaceAll("https://", ""),
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
launchUrlString(pluginRepo.repoUrl);
},
),
],
),
style: context.theme.typography.xSmall,
),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (pluginRepo.owner == "KRTirtho")
PrimaryBadge(
leading: const Icon(SpotubeIcons.done),
child: Text(context.l10n.official),
)
else ...[
Text(
context.l10n.author_name(pluginRepo.owner),
style: context.theme.typography.xSmall,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
const Icon(SpotubeIcons.warning, size: 14),
Text(
context.l10n.third_party,
style: const TextStyle(color: Colors.white),
).xSmall
],
),
),
],
for (final topic in pluginRepo.topics)
if (validTopics.keys.contains(topic))
SecondaryBadge(
leading: Icon(validTopics[topic]!.$2),
child: Text(validTopics[topic]!.$1),
),
SecondaryBadge(
leading: host == "github.com"
? const Icon(SpotubeIcons.github)
: null,
child: Text(host),
onPressed: () {
launchUrlString(pluginRepo.repoUrl);
},
),
],
),
],
),
); );
} }
} }

View File

@ -21,11 +21,9 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/metadata_plugin/audio_source/quality_label.dart';
import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/server/active_track_sources.dart';
import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:url_launcher/url_launcher_string.dart';
class PlayerView extends HookConsumerWidget { class PlayerView extends HookConsumerWidget {
final PanelController panelController; final PanelController panelController;
@ -45,6 +43,7 @@ class PlayerView extends HookConsumerWidget {
final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source; final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source;
final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
final mediaQuery = MediaQuery.sizeOf(context); final mediaQuery = MediaQuery.sizeOf(context);
final qualityLabel = ref.watch(audioSourceQualityLabelProvider);
final shouldHide = useState(true); final shouldHide = useState(true);
@ -109,22 +108,6 @@ class PlayerView extends HookConsumerWidget {
) )
], ],
trailing: [ trailing: [
if (currentActiveTrackSource is YoutubeSourcedTrack)
TextButton(
size: const ButtonSize(1.2),
leading: Assets.images.logos.songlinkTransparent.image(
width: 20,
height: 20,
color: theme.colorScheme.foreground,
),
onPressed: () {
final url =
"https://song.link/s/${currentActiveTrack?.id}";
launchUrlString(url);
},
child: Text(context.l10n.song_link),
),
if (!isLocalTrack) if (!isLocalTrack)
Tooltip( Tooltip(
tooltip: TooltipContainer( tooltip: TooltipContainer(
@ -267,6 +250,20 @@ class PlayerView extends HookConsumerWidget {
); );
}), }),
), ),
const Gap(25),
OutlineBadge(
style: const ButtonStyle.outline(
size: ButtonSize.normal,
density: ButtonDensity.dense,
shape: ButtonShape.rectangle,
).copyWith(
textStyle: (context, states, value) {
return value.copyWith(fontWeight: FontWeight.w500);
},
),
leading: const Icon(SpotubeIcons.lightningOutlined),
child: Text(qualityLabel),
)
], ],
), ),
), ),

View File

@ -43,8 +43,12 @@ class PlayerActions extends HookConsumerWidget {
final downloader = ref.watch(downloadManagerProvider.notifier); final downloader = ref.watch(downloadManagerProvider.notifier);
final isInQueue = useMemoized(() { final isInQueue = useMemoized(() {
if (playlist.activeTrack is! SpotubeFullTrackObject) return false; if (playlist.activeTrack is! SpotubeFullTrackObject) return false;
return downloader final downloadTask =
.isActive(playlist.activeTrack! as SpotubeFullTrackObject); downloader.getTaskByTrackId(playlist.activeTrack!.id);
return const [
DownloadStatus.queued,
DownloadStatus.downloading,
].contains(downloadTask?.status);
}, [ }, [
playlist.activeTrack, playlist.activeTrack,
downloader, downloader,

View File

@ -9,13 +9,16 @@ import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/fallbacks/not_found.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/player/player_queue_actions.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
@ -55,6 +58,9 @@ class PlayerQueue extends HookConsumerWidget {
final controller = useAutoScrollController(); final controller = useAutoScrollController();
final searchText = useState(''); final searchText = useState('');
final selectionMode = useState(false);
final selectedTrackIds = useState(<String>{});
final isSearching = useState(false); final isSearching = useState(false);
final tracks = playlist.tracks; final tracks = playlist.tracks;
@ -131,6 +137,91 @@ class PlayerQueue extends HookConsumerWidget {
surfaceOpacity: 0, surfaceOpacity: 0,
child: searchBar, child: searchBar,
) )
else if (selectionMode.value)
AppBar(
backgroundColor: Colors.transparent,
surfaceBlur: 0,
surfaceOpacity: 0,
leading: [
IconButton.ghost(
icon: const Icon(SpotubeIcons.close),
onPressed: () {
selectedTrackIds.value = {};
selectionMode.value = false;
},
)
],
title: SizedBox(
height: 30,
child: AutoSizeText(
'${selectedTrackIds.value.length} selected',
maxLines: 1,
),
),
trailing: [
PlayerQueueActionButton(
builder: (context, close) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(12),
ButtonTile(
style: const ButtonStyle.ghost(),
leading:
const Icon(SpotubeIcons.selectionCheck),
title: Text(context.l10n.select_all),
onPressed: () {
selectedTrackIds.value =
filteredTracks.map((t) => t.id).toSet();
Navigator.pop(context);
},
),
ButtonTile(
style: const ButtonStyle.ghost(),
leading: const Icon(SpotubeIcons.playlistAdd),
title: Text(context.l10n.add_to_playlist),
onPressed: () async {
final selected = filteredTracks
.where((t) =>
selectedTrackIds.value.contains(t.id))
.toList();
close();
if (selected.isEmpty) return;
final res = await showDialog<bool?>(
context: context,
builder: (context) =>
PlaylistAddTrackDialog(
tracks: selected,
openFromPlaylist: null,
),
);
if (res == true) {
selectedTrackIds.value = {};
selectionMode.value = false;
}
},
),
ButtonTile(
style: const ButtonStyle.ghost(),
leading: const Icon(SpotubeIcons.trash),
title: Text(context.l10n.remove_from_queue),
onPressed: () async {
final ids = selectedTrackIds.value.toList();
close();
if (ids.isEmpty) return;
await Future.wait(
ids.map((id) => onRemove(id)));
if (context.mounted) {
selectedTrackIds.value = {};
selectionMode.value = false;
}
},
),
const Gap(12),
],
),
),
],
)
else else
AppBar( AppBar(
trailingGap: 0, trailingGap: 0,
@ -195,6 +286,20 @@ class PlayerQueue extends HookConsumerWidget {
}, },
itemBuilder: (context, i) { itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i); final track = filteredTracks.elementAt(i);
void toggleSelection(String id) {
final s = {...selectedTrackIds.value};
if (s.contains(id)) {
s.remove(id);
} else {
s.add(id);
}
selectedTrackIds.value = s;
if (selectedTrackIds.value.isEmpty) {
selectionMode.value = false;
}
}
return AutoScrollTag( return AutoScrollTag(
key: ValueKey<int>(i), key: ValueKey<int>(i),
controller: controller, controller: controller,
@ -203,15 +308,34 @@ class PlayerQueue extends HookConsumerWidget {
playlist: playlist, playlist: playlist,
index: i, index: i,
track: track, track: track,
selectionMode: selectionMode.value,
selected:
selectedTrackIds.value.contains(track.id),
onChanged: selectionMode.value
? (_) => toggleSelection(track.id)
: null,
onTap: () async { onTap: () async {
if (selectionMode.value) {
toggleSelection(track.id);
return;
}
if (playlist.activeTrack?.id == track.id) { if (playlist.activeTrack?.id == track.id) {
return; return;
} }
await onJump(track); await onJump(track);
}, },
onLongPress: () {
if (!selectionMode.value) {
selectionMode.value = true;
selectedTrackIds.value = {track.id};
} else {
toggleSelection(track.id);
}
},
leadingActions: [ leadingActions: [
if (!isSearching.value && if (!isSearching.value &&
searchText.value.isEmpty) searchText.value.isEmpty &&
!selectionMode.value)
Padding( Padding(
padding: padding:
const EdgeInsets.only(left: 8.0), const EdgeInsets.only(left: 8.0),

View File

@ -0,0 +1,44 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart';
class PlayerQueueActionButton extends StatelessWidget {
final Widget Function(BuildContext context, VoidCallback close) builder;
const PlayerQueueActionButton({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return IconButton.ghost(
onPressed: () {
final mediaQuery = MediaQuery.sizeOf(context);
if (mediaQuery.lgAndUp) {
showDropdown(
context: context,
builder: (context) {
return SizedBox(
width: 220 * context.theme.scaling,
child: Card(
padding: EdgeInsets.zero,
child: builder(context, () => closeOverlay(context)),
),
);
},
);
} else {
openSheet(
context: context,
builder: (context) => builder(context, () => closeSheet(context)),
position: OverlayPosition.bottom,
);
}
},
icon: const Icon(SpotubeIcons.moreHorizontal),
);
}
}

View File

@ -1,60 +1,16 @@
import 'package:collection/collection.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/hooks/utils/use_debounce.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/server/active_track_sources.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
final sourceInfoToIconMap = {
AudioSource.youtube:
const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
AudioSource.jiosaavn: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
image: DecorationImage(
image: Assets.images.logos.jiosaavn.provider(),
fit: BoxFit.cover,
),
),
),
AudioSource.piped: const Icon(SpotubeIcons.piped),
AudioSource.invidious: Container(
height: 18,
width: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
image: DecorationImage(
image: Assets.images.logos.invidious.provider(),
fit: BoxFit.cover,
),
),
),
};
class SiblingTracksSheet extends HookConsumerWidget { class SiblingTracksSheet extends HookConsumerWidget {
final bool floating; final bool floating;
@ -65,94 +21,21 @@ class SiblingTracksSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final controller = useScrollController();
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final preferences = ref.watch(userPreferencesProvider);
final youtubeEngine = ref.watch(youtubeEngineProvider);
final isLoading = useState(false); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final isSearching = useState(false);
final searchMode = useState(preferences.searchMode);
final activeTrackSources = ref.watch(activeTrackSourcesProvider); final activeTrackSources = ref.watch(activeTrackSourcesProvider);
final activeTrackNotifier = activeTrackSources.asData?.value?.notifier; final activeTrackNotifier = activeTrackSources.asData?.value?.notifier;
final activeTrack = activeTrackSources.asData?.value?.track; final activeTrack = activeTrackSources.asData?.value?.track;
final activeTrackSource = activeTrackSources.asData?.value?.source; final activeTrackSource = activeTrackSources.asData?.value?.source;
final title = ServiceUtils.getTitle( final siblings = useMemoized<List<SpotubeAudioSourceMatchObject>>(
activeTrack?.name ?? "",
artists: activeTrack?.artists.map((e) => e.name).toList() ?? [],
onlyCleanArtist: true,
).trim();
final defaultSearchTerm =
"$title - ${activeTrack?.artists.asString() ?? ""}";
final searchController = useShadcnTextEditingController(
text: defaultSearchTerm,
);
final searchTerm = useDebounce<String>(
useValueListenable(searchController).text,
);
final controller = useScrollController();
final searchRequest = useMemoized(() async {
if (searchTerm.trim().isEmpty || activeTrackSource == null) {
return <TrackSourceInfo>[];
}
if (preferences.audioSource == AudioSource.jiosaavn) {
final resultsJioSaavn =
await jiosaavnClient.search.songs(searchTerm.trim());
final results = await Future.wait(
resultsJioSaavn.results.mapIndexed((i, song) async {
final siblingType = JioSaavnSourcedTrack.toSiblingType(song);
return siblingType.info;
}));
final activeSourceInfo = activeTrackSource.info;
return results
..removeWhere((element) => element.id == activeSourceInfo.id)
..insert(
0,
activeSourceInfo,
);
} else {
final resultsYt = await youtubeEngine.searchVideos(searchTerm.trim());
final searchResults = await Future.wait(
resultsYt
.map(YoutubeVideoInfo.fromVideo)
.mapIndexed((i, video) async {
if (!context.mounted) return null;
final siblingType =
await YoutubeSourcedTrack.toSiblingType(i, video, ref);
return siblingType.info;
})
.whereType<Future<TrackSourceInfo>>()
.toList(),
);
final activeSourceInfo = activeTrackSource.info;
return searchResults
..removeWhere((element) => element.id == activeSourceInfo.id)
..insert(0, activeSourceInfo);
}
}, [
searchTerm,
searchMode.value,
activeTrack,
activeTrackSource,
preferences.audioSource,
youtubeEngine,
]);
final siblings = useMemoized(
() => !isFetchingActiveTrack () => !isFetchingActiveTrack
? [ ? [
if (activeTrackSource != null) activeTrackSource.info, if (activeTrackSource != null) activeTrackSource.info,
...?activeTrackSource?.siblings, ...?activeTrackSource?.siblings,
] ]
: <TrackSourceInfo>[], : <SpotubeAudioSourceMatchObject>[],
[activeTrackSource, isFetchingActiveTrack], [activeTrackSource, isFetchingActiveTrack],
); );
@ -166,74 +49,6 @@ class SiblingTracksSheet extends HookConsumerWidget {
return null; return null;
}, [activeTrack, previousActiveTrack]); }, [activeTrack, previousActiveTrack]);
final itemBuilder = useCallback(
(TrackSourceInfo sourceInfo, AudioSource source) {
final icon = sourceInfoToIconMap[source];
return ButtonTile(
style: ButtonVariance.ghost,
padding: const EdgeInsets.symmetric(horizontal: 8),
title: Text(
sourceInfo.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
leading: UniversalImage(
path: sourceInfo.thumbnail,
height: 60,
width: 60,
),
trailing: Text(Duration(milliseconds: sourceInfo.durationMs)
.toHumanReadableString()),
subtitle: Row(
children: [
if (icon != null) icon,
Flexible(
child: Text(
"${sourceInfo.artists}",
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
enabled: !isFetchingActiveTrack && !isLoading.value,
selected: !isFetchingActiveTrack &&
sourceInfo.id == activeTrackSource?.info.id,
onPressed: () async {
if (!isFetchingActiveTrack &&
sourceInfo.id != activeTrackSource?.info.id) {
try {
isLoading.value = true;
await activeTrackNotifier?.swapWithSibling(sourceInfo);
await ref.read(audioPlayerProvider.notifier).swapActiveSource();
if (context.mounted) {
if (MediaQuery.sizeOf(context).mdAndUp) {
closeOverlay(context);
} else {
closeDrawer(context);
}
}
} finally {
if (context.mounted) {
isLoading.value = false;
}
}
}
},
);
},
[
activeTrackSource,
activeTrackNotifier,
siblings,
isFetchingActiveTrack,
isLoading.value,
],
);
final scale = context.theme.scaling;
return SafeArea( return SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -245,71 +60,15 @@ class SiblingTracksSheet extends HookConsumerWidget {
children: [ children: [
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: !isSearching.value child: Text(
? Text(
context.l10n.alternative_track_sources, context.l10n.alternative_track_sources,
).bold() ).bold()),
: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 320 * scale,
maxHeight: 38 * scale,
),
child: TextField(
autofocus: true,
controller: searchController,
placeholder: Text(context.l10n.search),
style: theme.typography.bold,
),
),
),
const Spacer(),
if (!isSearching.value) ...[
IconButton.outline(
icon: const Icon(SpotubeIcons.search, size: 18),
onPressed: () {
isSearching.value = true;
},
),
if (!floating) const BackButton(icon: SpotubeIcons.angleDown)
] else ...[
if (preferences.audioSource == AudioSource.piped)
IconButton.outline(
icon: const Icon(SpotubeIcons.filter, size: 18),
onPressed: () {
showPopover(
context: context,
alignment: Alignment.bottomRight,
builder: (context) {
return DropdownMenu(
children: SearchMode.values
.map(
(e) => MenuButton(
onPressed: (context) {
searchMode.value = e;
},
enabled: searchMode.value != e,
child: Text(e.label),
),
)
.toList(),
);
},
);
},
),
IconButton.outline(
icon: const Icon(SpotubeIcons.close, size: 18),
onPressed: () {
isSearching.value = false;
},
),
]
], ],
), ),
), ),
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: isLoading.value child: activeTrackSources.isLoading
? const SizedBox( ? const SizedBox(
width: double.infinity, width: double.infinity,
child: LinearProgressIndicator(), child: LinearProgressIndicator(),
@ -323,42 +82,62 @@ class SiblingTracksSheet extends HookConsumerWidget {
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
child: switch (isSearching.value) { child: ListView.separated(
false => ListView.separated(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
controller: controller, controller: controller,
itemCount: siblings.length, itemCount: siblings.length,
separatorBuilder: (context, index) => const Gap(8), separatorBuilder: (context, index) => const Gap(8),
itemBuilder: (context, index) => itemBuilder( itemBuilder: (context, index) {
siblings[index], final sourceInfo = siblings[index];
activeTrackSource!.source,
),
),
true => FutureBuilder(
future: searchRequest,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator());
}
return ListView.separated( return ButtonTile(
padding: const EdgeInsets.all(8.0), style: ButtonVariance.ghost,
controller: controller, padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: snapshot.data!.length, title: Text(
separatorBuilder: (context, index) => const Gap(8), sourceInfo.title,
itemBuilder: (context, index) => itemBuilder( maxLines: 2,
snapshot.data![index], overflow: TextOverflow.ellipsis,
preferences.audioSource,
), ),
leading: sourceInfo.thumbnail != null
? UniversalImage(
path: sourceInfo.thumbnail!,
height: 60,
width: 60,
)
: null,
trailing:
Text(sourceInfo.duration.toHumanReadableString()),
subtitle: Flexible(
child: Text(
sourceInfo.artists.join(", "),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
enabled: !isFetchingActiveTrack,
selected: !isFetchingActiveTrack &&
sourceInfo.id == activeTrackSource?.info.id,
onPressed: () async {
if (!isFetchingActiveTrack &&
sourceInfo.id != activeTrackSource?.info.id) {
await activeTrackNotifier
?.swapWithSibling(sourceInfo);
await ref
.read(audioPlayerProvider.notifier)
.swapActiveSource();
if (context.mounted) {
if (MediaQuery.sizeOf(context).mdAndUp) {
closeOverlay(context);
} else {
closeDrawer(context);
}
}
}
},
); );
}, },
), ),
},
), ),
), ),
), ),

View File

@ -22,7 +22,7 @@ class Sidebar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final ThemeData(:colorScheme) = Theme.of(context); final ThemeData(:colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.sizeOf(context);
final layoutMode = final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); ref.watch(userPreferencesProvider.select((s) => s.layoutMode));

View File

@ -24,7 +24,12 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
final theme = Theme.of(context); final theme = Theme.of(context);
final router = AutoRouter.of(context, watch: true); final router = AutoRouter.of(context, watch: true);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; final downloadCount = ref
.watch(downloadManagerProvider)
.where((e) =>
e.status == DownloadStatus.downloading ||
e.status == DownloadStatus.queued)
.length;
final userSnapshot = ref.watch(metadataPluginUserProvider); final userSnapshot = ref.watch(metadataPluginUserProvider);
final data = userSnapshot.asData?.value; final data = userSnapshot.asData?.value;

View File

@ -25,7 +25,12 @@ class SpotubeNavigationBar extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; final downloadCount = ref
.watch(downloadManagerProvider)
.where((e) =>
e.status == DownloadStatus.downloading ||
e.status == DownloadStatus.queued)
.length;
final layoutMode = final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); ref.watch(userPreferencesProvider.select((s) => s.layoutMode));

View File

@ -1,46 +0,0 @@
import 'dart:async';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/provider/download_manager_provider.dart';
void useDownloaderDialogs(WidgetRef ref) {
final context = useContext();
final showingDialogCompleter = useRef(Completer()..complete());
final downloader = ref.watch(downloadManagerProvider);
useEffect(() {
downloader.onFileExists = (track) async {
if (!context.mounted) return false;
if (!showingDialogCompleter.value.isCompleted) {
await showingDialogCompleter.value.future;
}
final replaceAll = ref.read(replaceDownloadedFileState);
if (replaceAll != null) return replaceAll;
showingDialogCompleter.value = Completer();
if (context.mounted) {
final result = await showDialog<bool>(
context: context,
builder: (context) => ReplaceDownloadedDialog(
track: track,
),
) ??
false;
showingDialogCompleter.value.complete();
return result;
}
// it'll never reach here as root_app is always mounted
return false;
};
return null;
}, [downloader]);
}

View File

@ -31,7 +31,7 @@ void useGlobalSubscriptions(WidgetRef ref) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => MetadataPluginUpdateAvailableDialog( builder: (context) => MetadataPluginUpdateAvailableDialog(
plugin: pluginConfig.defaultPluginConfig!, plugin: pluginConfig.defaultMetadataPluginConfig!,
update: pluginUpdate, update: pluginUpdate,
), ),
); );

View File

@ -1,30 +1,11 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
final audioSourceToIconMap = {
AudioSource.youtube: const Icon(
SpotubeIcons.youtube,
color: Colors.red,
size: 20,
),
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 20),
AudioSource.invidious: ClipRRect(
borderRadius: BorderRadius.circular(26),
child: Assets.images.logos.invidious.image(width: 26, height: 26),
),
AudioSource.jiosaavn:
Assets.images.logos.jiosaavn.image(width: 20, height: 20),
};
class GettingStartedPagePlaybackSection extends HookConsumerWidget { class GettingStartedPagePlaybackSection extends HookConsumerWidget {
final VoidCallback onNext; final VoidCallback onNext;
final VoidCallback onPrevious; final VoidCallback onPrevious;
@ -40,17 +21,19 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.read(userPreferencesProvider.notifier); final preferencesNotifier = ref.read(userPreferencesProvider.notifier);
final audioSourceToDescription = useMemoized( // final audioSourceToDescription = useMemoized(
() => { // () => {
AudioSource.youtube: "${context.l10n.youtube_source_description}\n" // AudioSource.youtube: "${context.l10n.youtube_source_description}\n"
"${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}", // "${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}",
AudioSource.piped: context.l10n.piped_source_description, // AudioSource.piped: context.l10n.piped_source_description,
AudioSource.jiosaavn: // AudioSource.jiosaavn:
"${context.l10n.jiosaavn_source_description}\n" // "${context.l10n.jiosaavn_source_description}\n"
"${context.l10n.highest_quality("320kbps mp")}", // "${context.l10n.highest_quality("320kbps mp4")}",
AudioSource.invidious: context.l10n.invidious_source_description, // AudioSource.invidious: context.l10n.invidious_source_description,
}, // AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n"
[]); // "${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}",
// },
// []);
return Center( return Center(
child: BlurCard( child: BlurCard(
@ -65,53 +48,44 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
], ],
), ),
const Gap(16), const Gap(16),
Align( // Align(
alignment: Alignment.centerLeft, // alignment: Alignment.centerLeft,
child: Text(context.l10n.select_audio_source).semiBold().large(), // child: Text(context.l10n.select_audio_source).semiBold().large(),
), // ),
const Gap(16), // const Gap(16),
Select<AudioSource>( // RadioGroup<AudioSource>(
value: preferences.audioSource, // value: preferences.audioSource,
onChanged: (value) { // onChanged: (value) {
if (value == null) return; // preferencesNotifier.setAudioSource(value);
preferencesNotifier.setAudioSource(value); // },
}, // child: Wrap(
placeholder: Text(preferences.audioSource.name.capitalize()), // spacing: 6,
itemBuilder: (context, value) => Row( // runSpacing: 6,
mainAxisSize: MainAxisSize.min, // children: [
spacing: 6, // for (final source in AudioSource.values)
children: [ // Badge(
audioSourceToIconMap[value]!, // isLabelVisible: source == AudioSource.dabMusic,
Text(value.name.capitalize()), // label: const Text("NEW"),
], // backgroundColor: Colors.lime[300],
), // textColor: Colors.black,
popup: (context) { // child: RadioCard(
return SelectPopup( // value: source,
items: SelectItemBuilder( // child: Column(
childCount: AudioSource.values.length, // mainAxisSize: MainAxisSize.min,
builder: (context, index) { // children: [
final source = AudioSource.values[index]; // audioSourceToIconMap[source]!,
// Text(source.label),
return SelectItemButton( // ],
value: source, // ),
child: Row( // ),
mainAxisSize: MainAxisSize.min, // ),
spacing: 6, // ],
children: [ // ),
audioSourceToIconMap[source]!, // ),
Text(source.name.capitalize()), // const Gap(16),
], // Text(
), // audioSourceToDescription[preferences.audioSource]!,
); // ).small().muted(),
},
),
);
},
),
const Gap(16),
Text(
audioSourceToDescription[preferences.audioSource]!,
).small().muted(),
const Gap(16), const Gap(16),
ButtonTile( ButtonTile(
title: Text(context.l10n.endless_playback), title: Text(context.l10n.endless_playback),

View File

@ -17,7 +17,12 @@ class LibraryPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final downloadingCount = ref.watch(downloadManagerProvider).$downloadCount; final downloadingCount = ref
.watch(downloadManagerProvider)
.where((e) =>
e.status == DownloadStatus.downloading ||
e.status == DownloadStatus.queued)
.length;
final router = context.watchRouter; final router = context.watchRouter;
final sidebarLibraryTileList = useMemoized( final sidebarLibraryTileList = useMemoized(
() => [ () => [

View File

@ -14,9 +14,8 @@ class UserDownloadsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final downloadManager = ref.watch(downloadManagerProvider); final downloadQueue = ref.watch(downloadManagerProvider);
final downloadManagerNotifier = ref.watch(downloadManagerProvider.notifier);
final history = downloadManager.$backHistory;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -28,16 +27,15 @@ class UserDownloadsPage extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: AutoSizeText( child: AutoSizeText(
context.l10n context.l10n.currently_downloading(downloadQueue.length),
.currently_downloading(downloadManager.$downloadCount),
maxLines: 1, maxLines: 1,
).semiBold(), ).semiBold(),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Button.destructive( Button.destructive(
onPressed: downloadManager.$downloadCount == 0 onPressed: downloadQueue.isEmpty
? null ? null
: downloadManager.cancelAll, : downloadManagerNotifier.clearAll,
child: Text(context.l10n.cancel_all), child: Text(context.l10n.cancel_all),
), ),
], ],
@ -46,9 +44,12 @@ class UserDownloadsPage extends HookConsumerWidget {
Expanded( Expanded(
child: SafeArea( child: SafeArea(
child: ListView.builder( child: ListView.builder(
itemCount: history.length, itemCount: downloadQueue.length,
padding: const EdgeInsets.only(bottom: 200),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return DownloadItem(track: history.elementAt(index)); return DownloadItem(
task: downloadQueue.elementAt(index),
);
}, },
), ),
), ),

View File

@ -14,6 +14,7 @@ import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/track_presentation/presentation_actions.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/string.dart'; import 'package:spotube/extensions/string.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart'; import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
@ -68,6 +69,37 @@ class LocalLibraryPage extends HookConsumerWidget {
} }
} }
Future<void> shufflePlayLocalTracks(
WidgetRef ref,
List<SpotubeLocalTrackObject> tracks,
) async {
final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier);
final isPlaylistPlaying = playlist.containsTracks(tracks);
final shuffledTracks = tracks.shuffled();
if (isPlaylistPlaying) return;
await playback.load(
shuffledTracks,
initialIndex: 0,
autoPlay: true,
);
}
Future<void> addToQueueLocalTracks(
BuildContext context,
WidgetRef ref,
List<SpotubeLocalTrackObject> tracks,
) async {
final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier);
final isPlaylistPlaying = playlist.containsTracks(tracks);
if (isPlaylistPlaying) return;
await playback.addTracks(tracks);
if (!context.mounted) return;
showToastForAction(context, "add-to-queue", tracks.length);
}
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scale = context.theme.scaling; final scale = context.theme.scaling;
@ -75,8 +107,12 @@ class LocalLibraryPage extends HookConsumerWidget {
final sortBy = useState<SortBy>(SortBy.none); final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
final trackSnapshot = ref.watch(localTracksProvider); final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = playlist.containsTracks( final isPlaylistPlaying = useMemoized(
trackSnapshot.asData?.value.values.flattened.toList() ?? []); () => playlist.containsTracks(
trackSnapshot.asData?.value[location] ?? [],
),
[playlist, trackSnapshot, location],
);
final searchController = useShadcnTextEditingController(); final searchController = useShadcnTextEditingController();
useValueListenable(searchController); useValueListenable(searchController);
@ -222,7 +258,10 @@ class LocalLibraryPage extends HookConsumerWidget {
child: Row( child: Row(
children: [ children: [
const Gap(5), const Gap(5),
Button.primary( Tooltip(
tooltip:
TooltipContainer(child: Text(context.l10n.play)).call,
child: IconButton.primary(
onPressed: trackSnapshot.asData?.value != null onPressed: trackSnapshot.asData?.value != null
? () async { ? () async {
if (trackSnapshot.asData?.value.isNotEmpty == if (trackSnapshot.asData?.value.isNotEmpty ==
@ -230,18 +269,68 @@ class LocalLibraryPage extends HookConsumerWidget {
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
await playLocalTracks( await playLocalTracks(
ref, ref,
trackSnapshot.asData!.value[location] ?? [], trackSnapshot.asData!.value[location] ??
[],
); );
} }
} }
} }
: null, : null,
leading: Icon( icon: Icon(
isPlaylistPlaying isPlaylistPlaying
? SpotubeIcons.stop ? SpotubeIcons.stop
: SpotubeIcons.play, : SpotubeIcons.play,
), ),
child: Text(context.l10n.play), ),
),
const Gap(5),
Tooltip(
tooltip:
TooltipContainer(child: Text(context.l10n.shuffle))
.call,
child: IconButton.outline(
onPressed: trackSnapshot.asData?.value != null
? () async {
if (trackSnapshot.asData?.value.isNotEmpty ==
true) {
if (!isPlaylistPlaying) {
await shufflePlayLocalTracks(
ref,
trackSnapshot.asData!.value[location] ??
[],
);
}
}
}
: null,
enabled: !isPlaylistPlaying,
icon: const Icon(SpotubeIcons.shuffle),
),
),
const Gap(5),
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_to_queue))
.call,
child: IconButton.outline(
onPressed: trackSnapshot.asData?.value != null
? () async {
if (trackSnapshot.asData?.value.isNotEmpty ==
true) {
if (!isPlaylistPlaying) {
await addToQueueLocalTracks(
context,
ref,
trackSnapshot.asData!.value[location] ??
[],
);
}
}
}
: null,
enabled: !isPlaylistPlaying,
icon: const Icon(SpotubeIcons.queueAdd),
),
), ),
const Spacer(), const Spacer(),
if (constraints.smAndDown) if (constraints.smAndDown)
@ -346,9 +435,11 @@ class LocalLibraryPage extends HookConsumerWidget {
controller: controller, controller: controller,
child: Skeletonizer( child: Skeletonizer(
enabled: trackSnapshot.isLoading, enabled: trackSnapshot.isLoading,
child: ListView.builder( child: CustomScrollView(
controller: controller, controller: controller,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverList.builder(
itemCount: trackSnapshot.isLoading itemCount: trackSnapshot.isLoading
? 5 ? 5
: filteredTracks.length, : filteredTracks.length,
@ -377,6 +468,9 @@ class LocalLibraryPage extends HookConsumerWidget {
); );
}, },
), ),
const SliverGap(200),
],
),
), ),
), ),
), ),
@ -398,7 +492,7 @@ class LocalLibraryPage extends HookConsumerWidget {
error: (error, stackTrace) => error: (error, stackTrace) =>
Text(error.toString() + stackTrace.toString()), Text(error.toString() + stackTrace.toString()),
); );
}) }),
], ],
), ),
), ),

View File

@ -13,7 +13,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
// ignore: depend_on_referenced_packages
enum SortBy { enum SortBy {
none, none,

View File

@ -9,7 +9,6 @@ import 'package:spotube/modules/root/bottom_player.dart';
import 'package:spotube/modules/root/sidebar/sidebar.dart'; import 'package:spotube/modules/root/sidebar/sidebar.dart';
import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart';
import 'package:spotube/modules/root/use_downloader_dialogs.dart';
import 'package:spotube/modules/root/use_global_subscriptions.dart'; import 'package:spotube/modules/root/use_global_subscriptions.dart';
import 'package:spotube/provider/glance/glance.dart'; import 'package:spotube/provider/glance/glance.dart';
@ -25,7 +24,6 @@ class RootAppPage extends HookConsumerWidget {
ref.listen(glanceProvider, (_, __) {}); ref.listen(glanceProvider, (_, __) {});
useGlobalSubscriptions(ref); useGlobalSubscriptions(ref);
useDownloaderDialogs(ref);
useEndlessPlayback(ref); useEndlessPlayback(ref);
useCheckYtDlpInstalled(ref); useCheckYtDlpInstalled(ref);

View File

@ -64,7 +64,7 @@ class BlackListPage extends HookConsumerWidget {
child: TextField( child: TextField(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
placeholder: Text(context.l10n.search), placeholder: Text(context.l10n.search),
leading: const Icon(SpotubeIcons.search), // prefixIcon: const Icon(SpotubeIcons.search),
), ),
), ),
InterScrollbar( InterScrollbar(

View File

@ -30,6 +30,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final tabState = useState<int>(0);
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []); final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
final plugins = ref.watch(metadataPluginsProvider); final plugins = ref.watch(metadataPluginsProvider);
@ -49,19 +50,50 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
final pluginRepos = pluginReposSnapshot.asData?.value.items ?? []; final pluginRepos = pluginReposSnapshot.asData?.value.items ?? [];
if (installedPluginIds.isEmpty) return pluginRepos; if (installedPluginIds.isEmpty) return pluginRepos;
return pluginRepos final availablePlugins = pluginRepos
.whereNot((repo) => installedPluginIds.contains(repo.repoUrl)) .whereNot((repo) => installedPluginIds.contains(repo.repoUrl))
.toList(); .toList();
},
[plugins.asData?.value.plugins, pluginReposSnapshot.asData?.value], if (tabState.value != 0) {
// metadata only plugins
return availablePlugins.where(
(d) {
return d.topics.contains(
tabState.value == 1
? "spotube-metadata-plugin"
: "spotube-audio-source-plugin",
); );
},
).toList();
}
return availablePlugins; // all plugins
},
[
plugins.asData?.value.plugins,
pluginReposSnapshot.asData?.value,
tabState.value,
],
);
final installedPlugins = useMemoized<List<PluginConfiguration>?>(() {
if (tabState.value == 0) return plugins.asData?.value.plugins;
return plugins.asData?.value.plugins.where((d) {
return d.abilities.contains(
tabState.value == 1
? PluginAbilities.metadata
: PluginAbilities.audioSource,
);
}).toList();
}, [tabState.value, plugins.asData?.value]);
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.metadata_provider_plugins), title: Text(context.l10n.plugins),
) )
], ],
child: Padding( child: Padding(
@ -193,6 +225,20 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
), ),
), ),
const SliverGap(12), const SliverGap(12),
SliverToBoxAdapter(
child: TabList(
index: tabState.value,
onChanged: (value) {
tabState.value = value;
},
children: const [
TabItem(child: Text("All")),
TabItem(child: Text("Metadata")),
TabItem(child: Text("Audio Source")),
],
),
),
const SliverGap(12),
if (plugins.asData?.value.plugins.isNotEmpty ?? false) if (plugins.asData?.value.plugins.isNotEmpty ?? false)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Row( child: Row(
@ -207,15 +253,20 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
), ),
const SliverGap(20), const SliverGap(20),
SliverList.separated( SliverList.separated(
itemCount: plugins.asData?.value.plugins.length ?? 0, itemCount: installedPlugins?.length ?? 0,
separatorBuilder: (context, index) => const Gap(12), separatorBuilder: (context, index) => const Gap(12),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final plugin = plugins.asData!.value.plugins[index]; final plugin = installedPlugins![index];
final isDefault = final isDefaultMetadata =
plugins.asData!.value.defaultPlugin == index; plugins.asData!.value.defaultMetadataPluginConfig?.slug ==
plugin.slug;
final isDefaultAudioSource = plugins
.asData!.value.defaultAudioSourcePluginConfig?.slug ==
plugin.slug;
return MetadataInstalledPluginItem( return MetadataInstalledPluginItem(
plugin: plugin, plugin: plugin,
isDefault: isDefault, isDefaultMetadata: isDefaultMetadata,
isDefaultAudioSource: isDefaultAudioSource,
); );
}, },
), ),
@ -249,6 +300,7 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
description: "Loading...", description: "Loading...",
repoUrl: "", repoUrl: "",
owner: "", owner: "",
topics: [],
), ),
), ),
); );

View File

@ -21,8 +21,8 @@ class SettingsAccountSection extends HookConsumerWidget {
children: [ children: [
ListTile( ListTile(
leading: const Icon(SpotubeIcons.extensions), leading: const Icon(SpotubeIcons.extensions),
title: Text(context.l10n.metadata_provider_plugins), title: Text(context.l10n.plugins),
subtitle: Text(context.l10n.configure_your_own_metadata_plugin), subtitle: Text(context.l10n.configure_plugins),
onTap: () { onTap: () {
context.pushRoute(const SettingsMetadataProviderRoute()); context.pushRoute(const SettingsMetadataProviderRoute());
}, },

View File

@ -1,30 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show ListTile; import 'package:flutter/material.dart' show ListTile;
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart';
import 'package:spotube/modules/settings/playback/edit_instance_url_dialog.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart';
import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.dart'; import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
import 'package:spotube/provider/audio_player/sources/piped_instances_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class SettingsPlaybackSection extends HookConsumerWidget { class SettingsPlaybackSection extends HookConsumerWidget {
@ -34,257 +28,15 @@ class SettingsPlaybackSection extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final sourcePresets = ref.watch(audioSourcePresetsProvider);
final sourcePresetsNotifier =
ref.watch(audioSourcePresetsProvider.notifier);
final theme = Theme.of(context); final theme = Theme.of(context);
return SectionCardWithHeading( return SectionCardWithHeading(
heading: context.l10n.playback, heading: context.l10n.playback,
children: [ children: [
AdaptiveSelectTile<SourceQualities>( AdaptiveSelectTile<YoutubeClientEngine>(
secondary: const Icon(SpotubeIcons.audioQuality),
title: Text(context.l10n.audio_quality),
value: preferences.audioQuality,
options: [
SelectItemButton(
value: SourceQualities.high,
child: Text(context.l10n.high),
),
SelectItemButton(
value: SourceQualities.medium,
child: Text(context.l10n.medium),
),
SelectItemButton(
value: SourceQualities.low,
child: Text(context.l10n.low),
),
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setAudioQuality(value);
}
},
),
AdaptiveSelectTile<AudioSource>(
secondary: const Icon(SpotubeIcons.api),
title: Text(context.l10n.audio_source),
value: preferences.audioSource,
options: AudioSource.values
.map((e) => SelectItemButton(
value: e,
child: Text(e.label),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferencesNotifier.setAudioSource(value);
},
),
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: preferences.audioSource != AudioSource.piped
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: const SizedBox.shrink(),
secondChild: Consumer(
builder: (context, ref, child) {
final instanceList = ref.watch(pipedInstancesFutureProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary: const Icon(SpotubeIcons.piped),
title: Text(context.l10n.piped_instance),
subtitle: Text(
"${context.l10n.piped_description}\n"
"${context.l10n.piped_warning}",
),
value: preferences.pipedInstance,
showValueWhenUnfolded: false,
trailing: [
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
).call,
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) =>
SettingsPlaybackEditInstanceUrlDialog(
title: context.l10n.piped_instance,
initialValue: preferences.pipedInstance,
onSave: (value) {
preferencesNotifier.setPipedInstance(value);
},
),
);
},
),
)
],
options: [
if (data
.none((e) => e.apiUrl == preferences.pipedInstance))
SelectItemButton(
value: preferences.pipedInstance,
child: Text.rich(
TextSpan(
style: theme.typography.xSmall.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(text: context.l10n.custom),
const TextSpan(text: "\n"),
TextSpan(text: preferences.pipedInstance),
],
),
),
),
for (final e in data.sortedBy((e) => e.name))
SelectItemButton(
value: e.apiUrl,
child: RichText(
text: TextSpan(
style: theme.typography.normal.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(
text: "${e.name.trim()}\n",
),
TextSpan(
text: e.locations
.map(countryCodeToEmoji)
.join(""),
style: GoogleFonts.notoColorEmoji(),
),
],
),
),
),
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setPipedInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Text(error.toString()),
);
},
),
),
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: preferences.audioSource != AudioSource.invidious
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: const SizedBox.shrink(),
secondChild: Consumer(
builder: (context, ref, child) {
final instanceList = ref.watch(invidiousInstancesProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary: const Icon(SpotubeIcons.piped),
title: Text(context.l10n.invidious_instance),
subtitle: Text(
"${context.l10n.invidious_description}\n"
"${context.l10n.invidious_warning}",
),
trailing: [
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
).call,
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) =>
SettingsPlaybackEditInstanceUrlDialog(
title: context.l10n.invidious_instance,
initialValue: preferences.invidiousInstance,
onSave: (value) {
preferencesNotifier
.setInvidiousInstance(value);
},
),
);
},
),
)
],
value: preferences.invidiousInstance,
showValueWhenUnfolded: false,
options: [
if (data.none((e) =>
e.details.uri == preferences.invidiousInstance))
SelectItemButton(
value: preferences.invidiousInstance,
child: Text.rich(
TextSpan(
style: theme.typography.xSmall.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(text: context.l10n.custom),
const TextSpan(text: "\n"),
TextSpan(text: preferences.invidiousInstance),
],
),
),
),
for (final e in data.sortedBy((e) => e.name))
SelectItemButton(
value: e.details.uri,
child: RichText(
text: TextSpan(
style: theme.typography.normal.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(
text: "${e.name.trim()}\n",
),
TextSpan(
text: countryCodeToEmoji(
e.details.region,
),
style: GoogleFonts.notoColorEmoji(),
),
],
),
),
),
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setInvidiousInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Text(error.toString()),
);
},
),
),
switch (preferences.audioSource) {
AudioSource.youtube => AdaptiveSelectTile<YoutubeClientEngine>(
secondary: const Icon(SpotubeIcons.engine), secondary: const Icon(SpotubeIcons.engine),
title: Text(context.l10n.youtube_engine), title: Text(context.l10n.youtube_engine),
value: preferences.youtubeClientEngine, value: preferences.youtubeClientEngine,
@ -300,8 +52,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
if (value == YoutubeClientEngine.ytDlp) { if (value == YoutubeClientEngine.ytDlp) {
final customPath = KVStoreService.getYoutubeEnginePath(value); final customPath = KVStoreService.getYoutubeEnginePath(value);
if (!await YtDlpEngine.isInstalled() && if (!await YtDlpEngine.isInstalled() &&
(customPath == null || (customPath == null || !await File(customPath).exists()) &&
!await File(customPath).exists()) &&
context.mounted) { context.mounted) {
final hasInstalled = await showDialog<bool>( final hasInstalled = await showDialog<bool>(
context: context, context: context,
@ -314,45 +65,70 @@ class SettingsPlaybackSection extends HookConsumerWidget {
preferencesNotifier.setYoutubeClientEngine(value); preferencesNotifier.setYoutubeClientEngine(value);
}, },
), ),
AudioSource.piped || if (sourcePresets.presets.isNotEmpty) ...[
AudioSource.invidious => AdaptiveSelectTile(
AdaptiveSelectTile<SearchMode>( secondary: const Icon(SpotubeIcons.api),
secondary: const Icon(SpotubeIcons.search), title: Text(context.l10n.streaming_music_codec),
title: Text(context.l10n.search_mode), value: sourcePresets.selectedStreamingContainerIndex,
value: preferences.searchMode, options: [
options: SearchMode.values for (final MapEntry(:key, value: preset)
.map((e) => SelectItemButton( in sourcePresets.presets.asMap().entries)
value: e, SelectItemButton(value: key, child: Text(preset.name)),
child: Text(e.label), ],
))
.toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferencesNotifier.setSearchMode(value); sourcePresetsNotifier.setSelectedStreamingContainerIndex(value);
}, },
), ),
_ => const SizedBox.shrink(), AdaptiveSelectTile(
}, secondary: const Icon(SpotubeIcons.api),
AnimatedCrossFade( title: const Text("Streaming music quality"),
duration: const Duration(milliseconds: 300), value: sourcePresets.selectedStreamingQualityIndex,
crossFadeState: preferences.searchMode == SearchMode.youtube && options: [
(preferences.audioSource == AudioSource.piped || for (final MapEntry(:key, value: quality) in sourcePresets
preferences.audioSource == AudioSource.youtube || .presets[sourcePresets.selectedStreamingContainerIndex]
preferences.audioSource == AudioSource.invidious) .qualities
? CrossFadeState.showFirst .asMap()
: CrossFadeState.showSecond, .entries)
firstChild: ListTile( SelectItemButton(value: key, child: Text(quality.toString())),
leading: const Icon(SpotubeIcons.skip), ],
title: Text(context.l10n.skip_non_music), onChanged: (value) {
trailing: Switch( if (value == null) return;
value: preferences.skipNonMusic, sourcePresetsNotifier.setSelectedStreamingQualityIndex(value);
onChanged: (state) {
preferencesNotifier.setSkipNonMusic(state);
}, },
), ),
AdaptiveSelectTile(
secondary: const Icon(SpotubeIcons.api),
title: Text(context.l10n.download_music_codec),
value: sourcePresets.selectedDownloadingContainerIndex,
options: [
for (final MapEntry(:key, value: preset)
in sourcePresets.presets.asMap().entries)
SelectItemButton(value: key, child: Text(preset.name)),
],
onChanged: (value) {
if (value == null) return;
sourcePresetsNotifier.setSelectedDownloadingContainerIndex(value);
},
), ),
secondChild: const SizedBox.shrink(), AdaptiveSelectTile(
secondary: const Icon(SpotubeIcons.api),
title: const Text("Downloading music quality"),
value: sourcePresets.selectedStreamingQualityIndex,
options: [
for (final MapEntry(:key, value: quality) in sourcePresets
.presets[sourcePresets.selectedDownloadingContainerIndex]
.qualities
.asMap()
.entries)
SelectItemButton(value: key, child: Text(quality.toString())),
],
onChanged: (value) {
if (value == null) return;
sourcePresetsNotifier.setSelectedStreamingQualityIndex(value);
},
), ),
],
ListTile( ListTile(
title: Text(context.l10n.cache_music), title: Text(context.l10n.cache_music),
subtitle: kIsMobile subtitle: kIsMobile
@ -396,48 +172,6 @@ class SettingsPlaybackSection extends HookConsumerWidget {
onChanged: preferencesNotifier.setNormalizeAudio, onChanged: preferencesNotifier.setNormalizeAudio,
), ),
), ),
if (preferences.audioSource != AudioSource.jiosaavn) ...[
AdaptiveSelectTile<SourceCodecs>(
popupConstraints: const BoxConstraints(maxWidth: 300),
secondary: const Icon(SpotubeIcons.stream),
title: Text(context.l10n.streaming_music_codec),
value: preferences.streamMusicCodec,
showValueWhenUnfolded: false,
options: SourceCodecs.values
.map((e) => SelectItemButton(
value: e,
child: Text(
e.label,
style: theme.typography.small,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferencesNotifier.setStreamMusicCodec(value);
},
),
AdaptiveSelectTile<SourceCodecs>(
popupConstraints: const BoxConstraints(maxWidth: 300),
secondary: const Icon(SpotubeIcons.file),
title: Text(context.l10n.download_music_codec),
value: preferences.downloadMusicCodec,
showValueWhenUnfolded: false,
options: SourceCodecs.values
.map((e) => SelectItemButton(
value: e,
child: Text(
e.label,
style: theme.typography.small,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferencesNotifier.setDownloadMusicCodec(value);
},
),
],
ListTile( ListTile(
leading: const Icon(SpotubeIcons.repeat), leading: const Icon(SpotubeIcons.repeat),
title: Text(context.l10n.endless_playback), title: Text(context.l10n.endless_playback),

View File

@ -7,12 +7,11 @@ import 'package:media_kit/media_kit.dart';
import 'package:spotube/extensions/list.dart'; import 'package:spotube/extensions/list.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/discord_provider.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
@ -164,8 +163,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
final tracks = <SpotubeTrackObject>[]; final tracks = <SpotubeTrackObject>[];
for (final media in playlist.medias) { for (final media in playlist.medias) {
final trackQuery = TrackSourceQuery.parseUri(media.uri); final track = trackGroupedById[SpotubeMedia.media(media).track.id]
final track = trackGroupedById[trackQuery.id]?.firstOrNull; ?.firstOrNull;
if (track != null) { if (track != null) {
tracks.add(track); tracks.add(track);
} }
@ -259,11 +258,14 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
return addTracks(tracks); return addTracks(tracks);
} }
final addableTracks = _blacklist.filter(tracks).where( final addableTracks = _blacklist
.filter(tracks)
.where(
(track) => (track) =>
allowDuplicates || allowDuplicates ||
!state.tracks.any((element) => _compareTracks(element, track)), !state.tracks.any((element) => _compareTracks(element, track)),
); )
.toList();
state = state.copyWith( state = state.copyWith(
tracks: [...addableTracks, ...state.tracks], tracks: [...addableTracks, ...state.tracks],
@ -371,13 +373,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
} }
bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) { bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) {
if ((a is SpotubeLocalTrackObject && b is! SpotubeLocalTrackObject) || if (a.runtimeType != b.runtimeType) {
(a is! SpotubeLocalTrackObject && b is SpotubeLocalTrackObject)) {
return false; return false;
} }
return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject
? (a).path == (b).path ? a.path == b.path
: a.id == b.id; : a.id == b.id;
} }
@ -398,10 +399,9 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
// because of timeout // because of timeout
final intendedActiveTrack = medias.elementAt(initialIndex); final intendedActiveTrack = medias.elementAt(initialIndex);
if (intendedActiveTrack.track is! SpotubeLocalTrackObject) { if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
await ref.read( ref.read(
trackSourcesProvider( sourcedTrackProvider(
TrackSourceQuery.fromTrack( intendedActiveTrack.track as SpotubeFullTrackObject,
intendedActiveTrack.track as SpotubeFullTrackObject),
).future, ).future,
); );
} }

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