mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +00:00
Compare commits
35 Commits
63bd413d04
...
37e60fb4a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e60fb4a6 | ||
|
|
4fae9013a7 | ||
|
|
834445eda3 | ||
|
|
f10a3d4976 | ||
|
|
3209c75144 | ||
|
|
700a69fcd1 | ||
|
|
d2dd60aa5c | ||
|
|
fda2257119 | ||
|
|
7c632c8f06 | ||
|
|
a012a8f3af | ||
|
|
64f937bd14 | ||
|
|
d1b73dbb1c | ||
|
|
e1fa9efa14 | ||
|
|
6272f376ea | ||
|
|
4b5108e54e | ||
|
|
6311831902 | ||
|
|
99a84aa6dc | ||
|
|
3bc296cf22 | ||
|
|
f6d9d64b7d | ||
|
|
439de5d7f7 | ||
|
|
88699e9a3b | ||
|
|
348c2e931b | ||
|
|
973ca20c8e | ||
|
|
7d849b1430 | ||
|
|
e5150515f3 | ||
|
|
66848c78c7 | ||
|
|
3e34bc4be6 | ||
|
|
cecb687592 | ||
|
|
e8a54d3209 | ||
|
|
ca6924f5a9 | ||
|
|
0e48b7a337 | ||
|
|
60fbf66639 | ||
|
|
97370712bc | ||
|
|
c36e819ba3 | ||
|
|
61d34963fa |
46
.github/workflows/spotube-publish-binary.yml
vendored
46
.github/workflows/spotube-publish-binary.yml
vendored
@ -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 }}
|
||||||
|
|||||||
9
.github/workflows/spotube-release-binary.yml
vendored
9
.github/workflows/spotube-release-binary.yml
vendored
@ -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
1
.gitignore
vendored
@ -9,6 +9,7 @@
|
|||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
|
|||||||
@ -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
1
android/.gitignore
vendored
@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java
|
|||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
.kotlin
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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'
|
||||||
BIN
assets/images/logos/dab-music.png
Normal file
BIN
assets/images/logos/dab-music.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
BIN
assets/plugins/spotube-plugin-youtube-audio/plugin.smplug
Normal file
BIN
assets/plugins/spotube-plugin-youtube-audio/plugin.smplug
Normal file
Binary file not shown.
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
1
drift_schemas/app_db/drift_schema_v10.json
Normal file
1
drift_schemas/app_db/drift_schema_v10.json
Normal file
File diff suppressed because one or more lines are too long
1
drift_schemas/app_db/drift_schema_v9.json
Normal file
1
drift_schemas/app_db/drift_schema_v9.json
Normal file
File diff suppressed because one or more lines are too long
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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._();
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
69
lib/components/dialogs/link_open_permission_dialog.dart
Normal file
69
lib/components/dialogs/link_open_permission_dialog.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 ?? "",
|
||||||
|
|||||||
@ -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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
168
lib/extensions/dio.dart
Normal 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) {
|
||||||
|
// can’t 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1450,7 +1450,16 @@ class AppLocalizationsCa extends AppLocalizations {
|
|||||||
'Aquest complement fa scrobbling de la teva música per generar l’historial d’escoltes.';
|
'Aquest complement fa scrobbling de la teva música per generar l’historial d’escoltes.';
|
||||||
|
|
||||||
@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 {
|
|||||||
'L’entrada no coincideix amb el format requerit';
|
'L’entrada 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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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,
|
||||||
));
|
));
|
||||||
|
|||||||
@ -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'))();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)();
|
||||||
}
|
}
|
||||||
|
|||||||
110
lib/models/metadata/audio_source.dart
Normal file
110
lib/models/metadata/audio_source.dart
Normal 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);
|
||||||
|
}
|
||||||
@ -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
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) =>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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);
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
44
lib/modules/player/player_queue_actions.dart
Normal file
44
lib/modules/player/player_queue_actions.dart
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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));
|
||||||
|
|
||||||
|
|||||||
@ -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]);
|
|
||||||
}
|
|
||||||
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@ -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),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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()),
|
||||||
);
|
);
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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: [],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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());
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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
Loading…
Reference in New Issue
Block a user