mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +00:00
Compare commits
1 Commits
937899547a
...
71cd3fc18d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71cd3fc18d |
46
.github/workflows/spotube-publish-binary.yml
vendored
46
.github/workflows/spotube-publish-binary.yml
vendored
@ -12,10 +12,10 @@ on:
|
||||
type: boolean
|
||||
default: true
|
||||
jobs:
|
||||
description: Jobs to run (flathub,aur,winget,chocolatey)
|
||||
description: Jobs to run (flathub,aur,winget,chocolatey,playstore)
|
||||
required: true
|
||||
type: string
|
||||
default: "flathub,aur,winget,chocolatey"
|
||||
default: "flathub,aur,winget,chocolatey,playstore"
|
||||
|
||||
jobs:
|
||||
flathub:
|
||||
@ -112,26 +112,26 @@ jobs:
|
||||
- name: Tagname (workflow dispatch)
|
||||
run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV
|
||||
|
||||
# - uses: robinraju/release-downloader@main
|
||||
# with:
|
||||
# repository: KRTirtho/spotube
|
||||
# tag: v${{ env.TAG_NAME }}
|
||||
# tarBall: false
|
||||
# zipBall: false
|
||||
# out-file-path: dist
|
||||
# fileName: "Spotube-playstore-all-arch.aab"
|
||||
- uses: robinraju/release-downloader@main
|
||||
with:
|
||||
repository: KRTirtho/spotube
|
||||
tag: v${{ env.TAG_NAME }}
|
||||
tarBall: false
|
||||
zipBall: false
|
||||
out-file-path: dist
|
||||
fileName: "Spotube-playstore-all-arch.aab"
|
||||
|
||||
# - name: Create service-account.json
|
||||
# run: |
|
||||
# echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
|
||||
- name: Create service-account.json
|
||||
run: |
|
||||
echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
|
||||
|
||||
# - name: Upload Android Release to Play Store
|
||||
# if: ${{!inputs.dry_run}}
|
||||
# uses: r0adkll/upload-google-play@v1
|
||||
# with:
|
||||
# serviceAccountJson: ./service-account.json
|
||||
# releaseFiles: ./dist/Spotube-playstore-all-arch.aab
|
||||
# packageName: oss.krtirtho.spotube
|
||||
# track: production
|
||||
# status: draft
|
||||
# releaseName: ${{ env.TAG_NAME }}
|
||||
- name: Upload Android Release to Play Store
|
||||
if: ${{!inputs.dry_run}}
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJson: ./service-account.json
|
||||
releaseFiles: ./dist/Spotube-playstore-all-arch.aab
|
||||
packageName: oss.krtirtho.spotube
|
||||
track: production
|
||||
status: draft
|
||||
releaseName: ${{ env.TAG_NAME }}
|
||||
|
||||
9
.github/workflows/spotube-release-binary.yml
vendored
9
.github/workflows/spotube-release-binary.yml
vendored
@ -49,6 +49,7 @@ jobs:
|
||||
arch: all
|
||||
files: |
|
||||
build/Spotube-android-all-arch.apk
|
||||
build/Spotube-playstore-all-arch.aab
|
||||
- os: windows-latest
|
||||
platform: windows
|
||||
arch: x86
|
||||
@ -76,14 +77,6 @@ jobs:
|
||||
cache: true
|
||||
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
|
||||
if: ${{matrix.platform == 'android'}}
|
||||
uses: actions/setup-java@v4
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,7 @@
|
||||
.history
|
||||
.svn/
|
||||
|
||||
.vscode
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
|
||||
22
.vscode/c_cpp_properties.json
vendored
22
.vscode/c_cpp_properties.json
vendored
@ -1,22 +0,0 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Win32",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"UNICODE",
|
||||
"_UNICODE"
|
||||
],
|
||||
"windowsSdkVersion": "10.0.19041.0",
|
||||
"compilerPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.29.30133\\bin\\Hostx64\\x64\\cl.exe",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++17",
|
||||
"intelliSenseMode": "windows-msvc-x64",
|
||||
"configurationProvider": "ms-vscode.makefile-tools"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
58
.vscode/launch.json
vendored
58
.vscode/launch.json
vendored
@ -1,58 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "spotube",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
},
|
||||
{
|
||||
"name": "spotube (mobile)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "spotube (mobile-skia)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev",
|
||||
"--no-enable-impeller"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "spotube (profile)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "spotube (release)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"flutterMode": "release"
|
||||
},
|
||||
{
|
||||
"name": "spotube (mobile) (release)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"flutterMode": "release",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev"
|
||||
]
|
||||
}
|
||||
],
|
||||
"compounds": []
|
||||
}
|
||||
34
.vscode/settings.json
vendored
34
.vscode/settings.json
vendored
@ -1,34 +0,0 @@
|
||||
{
|
||||
"cmake.configureOnOpen": false,
|
||||
"cSpell.words": [
|
||||
"acousticness",
|
||||
"ambiguate",
|
||||
"Amoled",
|
||||
"Buildless",
|
||||
"configurators",
|
||||
"danceability",
|
||||
"fuzzywuzzy",
|
||||
"gapless",
|
||||
"instrumentalness",
|
||||
"isrc",
|
||||
"Mpris",
|
||||
"RGBO",
|
||||
"riverpod",
|
||||
"Scrobblenaut",
|
||||
"shadcn",
|
||||
"skeletonizer",
|
||||
"songlink",
|
||||
"speechiness",
|
||||
"Spotube",
|
||||
"titlebar",
|
||||
"winget"
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies,flutter_launcher_icons*.yaml,flutter_native_splash*.yaml",
|
||||
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
|
||||
"*.dart": "${capture}.g.dart,${capture}.freezed.dart"
|
||||
},
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.35.2"
|
||||
}
|
||||
170
.vscode/snippets.code-snippets
vendored
170
.vscode/snippets.code-snippets
vendored
@ -1,170 +0,0 @@
|
||||
{
|
||||
"PaginatedState": {
|
||||
"scope": "dart",
|
||||
"prefix": "paginatedState",
|
||||
"description": "Generate a PaginatedState",
|
||||
"body": [
|
||||
"class ${1:Model}State extends PaginatedState<${2:Model}> {",
|
||||
" ${1:Model}State({",
|
||||
" required super.items,",
|
||||
" required super.offset,",
|
||||
" required super.limit,",
|
||||
" required super.hasMore,",
|
||||
" });",
|
||||
" ",
|
||||
" @override",
|
||||
" ${1:Model}State copyWith({",
|
||||
" List<${2:Model}>? items,",
|
||||
" int? offset,",
|
||||
" int? limit,",
|
||||
" bool? hasMore,",
|
||||
" }) {",
|
||||
" return ${1:Model}State(",
|
||||
" items: items ?? this.items,",
|
||||
" offset: offset ?? this.offset,",
|
||||
" limit: limit ?? this.limit,",
|
||||
" hasMore: hasMore ?? this.hasMore,",
|
||||
" );",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"PaginatedAsyncNotifier": {
|
||||
"scope": "dart",
|
||||
"prefix": "paginatedAsyncNotifier",
|
||||
"description": "Generate a PaginatedAsyncNotifier",
|
||||
"body": [
|
||||
"class ${1:NotifierName}Notifier extends PaginatedAsyncNotifier<${3:Item}, ${2:Model}State> {",
|
||||
" ${1:NotifierName}Notifier() : super();",
|
||||
" ",
|
||||
" @override",
|
||||
" fetch(int offset, int limit) async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
" ",
|
||||
" @override",
|
||||
" build() async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"PaginaitedNotifierWithState": {
|
||||
"scope": "dart",
|
||||
"prefix": "paginatedNotifierWithState",
|
||||
"description": "Generate a PaginatedNotifier with PaginatedState",
|
||||
"body": [
|
||||
"class $1State extends PaginatedState<$2> {",
|
||||
" $1State({",
|
||||
" required super.items,",
|
||||
" required super.offset,",
|
||||
" required super.limit,",
|
||||
" required super.hasMore,",
|
||||
" });",
|
||||
" ",
|
||||
" @override",
|
||||
" $1State copyWith({",
|
||||
" List<$2>? items,",
|
||||
" int? offset,",
|
||||
" int? limit,",
|
||||
" bool? hasMore,",
|
||||
" }) {",
|
||||
" return $1State(",
|
||||
" items: items ?? this.items,",
|
||||
" offset: offset ?? this.offset,",
|
||||
" limit: limit ?? this.limit,",
|
||||
" hasMore: hasMore ?? this.hasMore,",
|
||||
" );",
|
||||
" }",
|
||||
"}",
|
||||
" ",
|
||||
"class $1Notifier",
|
||||
" extends PaginatedAsyncNotifier<$2, $1State> {",
|
||||
" $1Notifier() : super();",
|
||||
" ",
|
||||
" @override",
|
||||
" fetch(int offset, int limit) async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
" ",
|
||||
" @override",
|
||||
" build() async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
"}",
|
||||
" ",
|
||||
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProvider<$1Notifier, $1State>(",
|
||||
" ()=> $1Notifier(),",
|
||||
");"
|
||||
]
|
||||
},
|
||||
"FamilyPaginatedAsyncNotifier": {
|
||||
"scope": "dart",
|
||||
"prefix": "familyPaginatedAsyncNotifier",
|
||||
"description": "Generate a FamilyPaginatedAsyncNotifier",
|
||||
"body": [
|
||||
"class ${1:NotifierName}Notifier extends FamilyPaginatedAsyncNotifier<${3:Item}, ${2:Model}State, {$4:Arg}> {",
|
||||
" ${1:NotifierName}Notifier() : super();",
|
||||
" ",
|
||||
" @override",
|
||||
" fetch(arg, offset, limit) async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
" ",
|
||||
" @override",
|
||||
" build(arg) async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"FamilyPaginaitedNotifierWithState": {
|
||||
"scope": "dart",
|
||||
"prefix": "familyPaginatedNotifierWithState",
|
||||
"description": "Generate a FamilyPaginatedAsyncNotifier with PaginatedState",
|
||||
"body": [
|
||||
"class $1State extends PaginatedState<$2> {",
|
||||
" $1State({",
|
||||
" required super.items,",
|
||||
" required super.offset,",
|
||||
" required super.limit,",
|
||||
" required super.hasMore,",
|
||||
" });",
|
||||
" ",
|
||||
" @override",
|
||||
" $1State copyWith({",
|
||||
" List<$2>? items,",
|
||||
" int? offset,",
|
||||
" int? limit,",
|
||||
" bool? hasMore,",
|
||||
" }) {",
|
||||
" return $1State(",
|
||||
" items: items ?? this.items,",
|
||||
" offset: offset ?? this.offset,",
|
||||
" limit: limit ?? this.limit,",
|
||||
" hasMore: hasMore ?? this.hasMore,",
|
||||
" );",
|
||||
" }",
|
||||
"}",
|
||||
" ",
|
||||
"class $1Notifier",
|
||||
" extends FamilyPaginatedAsyncNotifier<$2, $1State, $3> {",
|
||||
" $1Notifier() : super();",
|
||||
" ",
|
||||
" @override",
|
||||
" fetch(arg, offset, limit) async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
" ",
|
||||
" @override",
|
||||
" build(arg) async {",
|
||||
" throw UnimplementedError();",
|
||||
" }",
|
||||
"}",
|
||||
" ",
|
||||
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProviderFamily<$1Notifier, $1State, $3>(",
|
||||
" ()=> $1Notifier(),",
|
||||
");"
|
||||
]
|
||||
},
|
||||
}
|
||||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@ -1,4 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": []
|
||||
}
|
||||
@ -202,6 +202,7 @@ 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. [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. [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. [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
android/.gitignore
vendored
1
android/.gitignore
vendored
@ -11,4 +11,3 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
.kotlin
|
||||
BIN
assets/images/logos/songlink-transparent.png
Normal file
BIN
assets/images/logos/songlink-transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
Binary file not shown.
@ -2,7 +2,9 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../../core/env.dart';
|
||||
import 'common.dart';
|
||||
@ -22,6 +24,39 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
"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(
|
||||
join(
|
||||
"build",
|
||||
@ -36,6 +71,22 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -66,19 +66,6 @@ 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 {
|
||||
const $AssetsImagesLogosGen();
|
||||
|
||||
@ -94,30 +81,13 @@ class $AssetsImagesLogosGen {
|
||||
AssetGenImage get jiosaavn =>
|
||||
const AssetGenImage('assets/images/logos/jiosaavn.png');
|
||||
|
||||
/// List of all assets
|
||||
List<AssetGenImage> get values => [dabMusic, invidious, jiosaavn];
|
||||
}
|
||||
|
||||
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';
|
||||
/// File path: assets/images/logos/songlink-transparent.png
|
||||
AssetGenImage get songlinkTransparent =>
|
||||
const AssetGenImage('assets/images/logos/songlink-transparent.png');
|
||||
|
||||
/// List of all assets
|
||||
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];
|
||||
List<AssetGenImage> get values =>
|
||||
[dabMusic, invidious, jiosaavn, songlinkTransparent];
|
||||
}
|
||||
|
||||
class Assets {
|
||||
@ -126,7 +96,6 @@ class Assets {
|
||||
static const String license = 'LICENSE';
|
||||
static const $AssetsBrandingGen branding = $AssetsBrandingGen();
|
||||
static const $AssetsImagesGen images = $AssetsImagesGen();
|
||||
static const $AssetsPluginsGen plugins = $AssetsPluginsGen();
|
||||
|
||||
/// List of all assets
|
||||
static List<String> get values => [license];
|
||||
|
||||
@ -135,7 +135,7 @@ abstract class SpotubeIcons {
|
||||
static const list = FeatherIcons.list;
|
||||
static const device = FeatherIcons.smartphone;
|
||||
static const engine = FeatherIcons.server;
|
||||
static const extensions = Icons.extension_rounded;
|
||||
static const extensions = FeatherIcons.package;
|
||||
static const message = FeatherIcons.send;
|
||||
static const upload = FeatherIcons.uploadCloud;
|
||||
static const plugin = Icons.extension_outlined;
|
||||
|
||||
@ -7,7 +7,8 @@ import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/duration.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/server/sourced_track_provider.dart';
|
||||
import 'package:spotube/models/playback/track_sources.dart';
|
||||
import 'package:spotube/provider/server/track_sources.dart';
|
||||
|
||||
class TrackDetailsDialog extends HookConsumerWidget {
|
||||
final SpotubeFullTrackObject track;
|
||||
@ -20,7 +21,8 @@ class TrackDetailsDialog extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final theme = Theme.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final sourcedTrack = ref.read(sourcedTrackProvider(track));
|
||||
final sourcedTrack =
|
||||
ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track)));
|
||||
|
||||
final detailsMap = {
|
||||
context.l10n.title: track.name,
|
||||
@ -37,7 +39,8 @@ class TrackDetailsDialog extends HookConsumerWidget {
|
||||
// style: const TextStyle(color: Colors.blue),
|
||||
// ),
|
||||
context.l10n.duration: sourcedTrack.asData != null
|
||||
? sourcedTrack.asData!.value.info.duration.toHumanReadableString()
|
||||
? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs)
|
||||
.toHumanReadableString()
|
||||
: Duration(milliseconds: track.durationMs).toHumanReadableString(),
|
||||
if (track.album.releaseDate != null)
|
||||
context.l10n.released: track.album.releaseDate,
|
||||
@ -54,7 +57,7 @@ class TrackDetailsDialog extends HookConsumerWidget {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
context.l10n.channel: Text(sourceInfo.artists.join(", ")),
|
||||
context.l10n.channel: Text(sourceInfo.artists),
|
||||
if (sourcedTrack.asData?.value.url != null)
|
||||
context.l10n.streamUrl: Hyperlink(
|
||||
sourcedTrack.asData!.value.url ?? "",
|
||||
|
||||
@ -8,10 +8,12 @@ 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_state.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/history/history.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
ToastOverlay showToastForAction(
|
||||
BuildContext context,
|
||||
@ -68,6 +70,8 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||
final audioSource =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
||||
|
||||
final state = ref.watch(presentationStateProvider(options.collection));
|
||||
final notifier =
|
||||
@ -81,13 +85,14 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
|
||||
}) async {
|
||||
final fullTrackObjects =
|
||||
tracks.whereType<SpotubeFullTrackObject>().toList();
|
||||
final confirmed = await showDialog<bool>(
|
||||
final confirmed = audioSource == AudioSource.piped ||
|
||||
(await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const ConfirmDownloadDialog();
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
false);
|
||||
if (confirmed != true) return;
|
||||
downloader.batchAddToQueue(fullTrackObjects);
|
||||
notifier.deselectAllTracks();
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.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/routes.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/ui/button_tile.dart';
|
||||
@ -35,6 +36,7 @@ class TrackOptions extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
|
||||
final trackOptionActions = ref.watch(trackOptionActionsProvider(track));
|
||||
final (
|
||||
@ -258,6 +260,24 @@ class TrackOptions extends HookConsumerWidget {
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
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,
|
||||
|
||||
@ -434,10 +434,7 @@
|
||||
"update_available": "Update available",
|
||||
"supports_scrobbling": "Supports scrobbling",
|
||||
"plugin_scrobbling_info": "This plugin scrobbles your music to generate your listening history.",
|
||||
"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",
|
||||
"default_plugin": "Default",
|
||||
"set_default": "Set default",
|
||||
"support": "Support",
|
||||
"support_plugin_development": "Support plugin development",
|
||||
@ -455,14 +452,14 @@
|
||||
"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",
|
||||
"input_does_not_match_format": "Input doesn't match the required format",
|
||||
"plugins": "Plugins",
|
||||
"metadata_provider_plugins": "Metadata Provider Plugins",
|
||||
"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",
|
||||
"failed_to_add_plugin_error": "Failed to add plugin: {error}",
|
||||
"upload_plugin_from_file": "Upload plugin from file",
|
||||
"installed": "Installed",
|
||||
"available_plugins": "Available plugins",
|
||||
"configure_plugins": "Configure your own metadata provider and audio source plugins",
|
||||
"configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider",
|
||||
"audio_scrobblers": "Audio Scrobblers",
|
||||
"scrobbling": "Scrobbling",
|
||||
"source": "Source: ",
|
||||
|
||||
@ -2763,29 +2763,11 @@ abstract class AppLocalizations {
|
||||
/// **'This plugin scrobbles your music to generate your listening history.'**
|
||||
String get plugin_scrobbling_info;
|
||||
|
||||
/// No description provided for @default_metadata_source.
|
||||
/// No description provided for @default_plugin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Default metadata source'**
|
||||
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;
|
||||
/// **'Default'**
|
||||
String get default_plugin;
|
||||
|
||||
/// No description provided for @set_default.
|
||||
///
|
||||
@ -2889,11 +2871,11 @@ abstract class AppLocalizations {
|
||||
/// **'Input doesn\'t match the required format'**
|
||||
String get input_does_not_match_format;
|
||||
|
||||
/// No description provided for @plugins.
|
||||
/// No description provided for @metadata_provider_plugins.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Plugins'**
|
||||
String get plugins;
|
||||
/// **'Metadata Provider Plugins'**
|
||||
String get metadata_provider_plugins;
|
||||
|
||||
/// No description provided for @paste_plugin_download_url.
|
||||
///
|
||||
@ -2931,11 +2913,11 @@ abstract class AppLocalizations {
|
||||
/// **'Available plugins'**
|
||||
String get available_plugins;
|
||||
|
||||
/// No description provided for @configure_plugins.
|
||||
/// No description provided for @configure_your_own_metadata_plugin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Configure your own metadata provider and audio source plugins'**
|
||||
String get configure_plugins;
|
||||
/// **'Configure your own playlist/album/artist/feed metadata provider'**
|
||||
String get configure_your_own_metadata_plugin;
|
||||
|
||||
/// No description provided for @audio_scrobblers.
|
||||
///
|
||||
|
||||
@ -1443,16 +1443,7 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
'تقوم هذه الإضافة بتتبع مقاطعك الموسيقية لإنشاء سجل الاستماع الخاص بك.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'الافتراضي';
|
||||
|
||||
@override
|
||||
String get set_default => 'تعيين كافتراضي';
|
||||
@ -1513,7 +1504,7 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
'المدخل لا يتوافق مع التنسيق المطلوب';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'إضافات مزود البيانات';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1538,8 +1529,8 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
String get available_plugins => 'الإضافات المتوفّرة';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'تهيئة مزوّد بيانات للقائمة/الألبوم/الفنان/المصدر خاص بك';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'أجهزة تتبع الصوت';
|
||||
|
||||
@ -1443,16 +1443,7 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
'এই প্লাগইনটি আপনার সঙ্গীত স্ক্রোব্বল করে আপনার শোনা ইতিহাস তৈরি করে।';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'ডিফল্ট';
|
||||
|
||||
@override
|
||||
String get set_default => 'ডিফল্ট হিসাবে নির্ধারণ করুন';
|
||||
@ -1514,7 +1505,7 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
'ইনপুট প্রয়োজনীয় ফরম্যাটের সাথে মেলে না';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'মেটাডেটা প্রদানকারী প্লাগইনসমূহ';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1539,8 +1530,8 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
String get available_plugins => 'উপলব্ধ প্লাগইনগুলো';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'নিজস্ব প্লেলিস্ট/অ্যালবাম/শিল্পী/ফিড মেটাডেটা প্রদানকারী কনফিগার করুন';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'অডিও স্ক্রোব্বলার্স';
|
||||
|
||||
@ -1450,16 +1450,7 @@ class AppLocalizationsCa extends AppLocalizations {
|
||||
'Aquest complement fa scrobbling de la teva música per generar l’historial d’escoltes.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Predeterminat';
|
||||
|
||||
@override
|
||||
String get set_default => 'Establir com a predeterminat';
|
||||
@ -1523,7 +1514,8 @@ class AppLocalizationsCa extends AppLocalizations {
|
||||
'L’entrada no coincideix amb el format requerit';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins =>
|
||||
'Complements de proveïdor de metadades';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1548,8 +1540,8 @@ class AppLocalizationsCa extends AppLocalizations {
|
||||
String get available_plugins => 'Complements disponibles';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Configura el teu propi proveïdor de metadades per llistes/reproduccions àlbum/artista/flux';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Scrobblers d’àudio';
|
||||
|
||||
@ -1442,16 +1442,7 @@ class AppLocalizationsCs extends AppLocalizations {
|
||||
'Tento plugin scrobbles vaši hudbu pro vytvoření historie poslechů.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Výchozí';
|
||||
|
||||
@override
|
||||
String get set_default => 'Nastavit jako výchozí';
|
||||
@ -1514,7 +1505,7 @@ class AppLocalizationsCs extends AppLocalizations {
|
||||
'Vstup neodpovídá požadovanému formátu';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Pluginy poskytovatelů metadat';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1539,8 +1530,8 @@ class AppLocalizationsCs extends AppLocalizations {
|
||||
String get available_plugins => 'Dostupné pluginy';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Nakonfigurujte si vlastního poskytovatele metadat pro playlist/album/umělec/fid';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Audio scrobblers';
|
||||
|
||||
@ -1455,16 +1455,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Dieses Plugin scrobbelt Ihre Musik, um Ihre Hörhistorie zu erstellen.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Standard';
|
||||
|
||||
@override
|
||||
String get set_default => 'Als Standard festlegen';
|
||||
@ -1526,7 +1517,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Eingabe entspricht nicht dem geforderten Format';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Plugins für Metadatenanbieter';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1551,8 +1542,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get available_plugins => 'Verfügbare Plugins';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Eigenen Anbieter für Playlist-/Album-/Künstler-/Feed-Metadaten konfigurieren';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Audio-Scrobbler';
|
||||
|
||||
@ -1442,16 +1442,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
'This plugin scrobbles your music to generate your listening history.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Default';
|
||||
|
||||
@override
|
||||
String get set_default => 'Set default';
|
||||
@ -1512,7 +1503,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
'Input doesn\'t match the required format';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Metadata Provider Plugins';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1537,8 +1528,8 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get available_plugins => 'Available plugins';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Configure your own playlist/album/artist/feed metadata provider';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Audio Scrobblers';
|
||||
|
||||
@ -1452,16 +1452,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
'Este complemento scrobblea tu música para generar tu historial de reproducción.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Predeterminado';
|
||||
|
||||
@override
|
||||
String get set_default => 'Establecer como predeterminado';
|
||||
@ -1526,7 +1517,8 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
'La entrada no coincide con el formato requerido';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins =>
|
||||
'Complementos de proveedor de metadatos';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1551,8 +1543,8 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get available_plugins => 'Complementos disponibles';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Configura tu propio proveedor de metadatos para listas/álbum/artista/feeds';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Scrobblers de audio';
|
||||
|
||||
@ -1451,16 +1451,7 @@ class AppLocalizationsEu extends AppLocalizations {
|
||||
'Plugin honek zure musika scrobbled egiten du zure entzuteen historia sortzeko.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Lehenetsia';
|
||||
|
||||
@override
|
||||
String get set_default => 'Lehenetsi gisa ezarri';
|
||||
@ -1524,7 +1515,7 @@ class AppLocalizationsEu extends AppLocalizations {
|
||||
'Sarrera ezin da beharrezko formatutik desberdina izan';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Metadaten hornitzailearen pluginak';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1549,8 +1540,8 @@ class AppLocalizationsEu extends AppLocalizations {
|
||||
String get available_plugins => 'Eskaintzen diren pluginak';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Konfiguratu zureko playlists-/album-/artista-/feed-metadaten hornitzailea';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Audio scrobbler-ak';
|
||||
|
||||
@ -1441,16 +1441,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||
'این افزونه موسیقی شما را اسکراب میکند تا تاریخچهٔ شنیداریتان را تولید کند.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'پیشفرض';
|
||||
|
||||
@override
|
||||
String get set_default => 'تنظیم به عنوان پیشفرض';
|
||||
@ -1512,7 +1503,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||
'ورودی با قالب مورد نیاز تطابق ندارد';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'افزونههای ارائهدهندهٔ متادیتا';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1537,8 +1528,8 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||
String get available_plugins => 'افزونههای موجود';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'پیکربندی ارائهدهندهٔ متادیتا برای پلیلیست/آلبوم/هنرمند/فید بهصورت سفارشی';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'اسکراببلرهای صوتی';
|
||||
|
||||
@ -1443,16 +1443,7 @@ class AppLocalizationsFi extends AppLocalizations {
|
||||
'Tämä lisäosa scrobblaa musiikkisi luodakseen kuunteluhistoriasi.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Oletus';
|
||||
|
||||
@override
|
||||
String get set_default => 'Aseta oletukseksi';
|
||||
@ -1512,7 +1503,7 @@ class AppLocalizationsFi extends AppLocalizations {
|
||||
String get input_does_not_match_format => 'Syöte ei vastaa vaadittua muotoa';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Metatietojen tarjoajan lisäosat';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1537,8 +1528,8 @@ class AppLocalizationsFi extends AppLocalizations {
|
||||
String get available_plugins => 'Saatavilla olevat lisäosat';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Määritä oma soittolistan/albumin/artistin/syötteen metatietojen tarjoaja';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Äänen scrobblerit';
|
||||
|
||||
@ -1457,16 +1457,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Ce plugin scrobble votre musique pour générer votre historique d\'écoute.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Par défaut';
|
||||
|
||||
@override
|
||||
String get set_default => 'Définir par défaut';
|
||||
@ -1530,7 +1521,8 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'L\'entrée ne correspond pas au format requis';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins =>
|
||||
'Plugins de fournisseur de métadonnées';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1556,8 +1548,8 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get available_plugins => 'Plugins disponibles';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Configurer votre propre fournisseur de métadonnées de playlist/album/artiste/flux';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Scrobblers audio';
|
||||
|
||||
@ -1448,16 +1448,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
'यह प्लगइन आपके सुनने के इतिहास को उत्पन्न करने के लिए आपके संगीत को स्क्रॉबल करता है।';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'डिफ़ॉल्ट';
|
||||
|
||||
@override
|
||||
String get set_default => 'डिफ़ॉल्ट सेट करें';
|
||||
@ -1518,7 +1509,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
'इनपुट आवश्यक प्रारूप से मेल नहीं खाता है';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'मेटाडेटा प्रदाता प्लगइन';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1543,8 +1534,8 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get available_plugins => 'उपलब्ध प्लगइन';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'अपनी खुद की प्लेलिस्ट/एल्बम/कलाकार/फ़ीड मेटाडेटा प्रदाता कॉन्फ़िगर करें';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'ऑडियो स्क्रॉबलर्स';
|
||||
|
||||
@ -1449,16 +1449,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
'Plugin ini scrobble musik Anda untuk menghasilkan riwayat mendengarkan Anda.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Bawaan';
|
||||
|
||||
@override
|
||||
String get set_default => 'Atur sebagai bawaan';
|
||||
@ -1520,7 +1511,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
'Masukan tidak cocok dengan format yang diperlukan';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Plugin Penyedia Metadata';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1545,8 +1536,8 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get available_plugins => 'Plugin yang tersedia';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Konfigurasi penyedia metadata playlist/album/artis/feed Anda sendiri';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Scrobblers Audio';
|
||||
|
||||
@ -1448,16 +1448,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Questo plugin scrobbla la tua musica per generare la tua cronologia di ascolti.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Predefinito';
|
||||
|
||||
@override
|
||||
String get set_default => 'Imposta come predefinito';
|
||||
@ -1519,7 +1510,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'L\'input non corrisponde al formato richiesto';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Plugin del provider di metadati';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1544,8 +1535,8 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get available_plugins => 'Plugin disponibili';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Configura il tuo provider di metadati per playlist/album/artista/feed';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Scrobbler audio';
|
||||
|
||||
@ -1416,16 +1416,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get plugin_scrobbling_info => 'このプラグインは、あなたの音楽をscrobbleして視聴履歴を生成します。';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'デフォルト';
|
||||
|
||||
@override
|
||||
String get set_default => 'デフォルトに設定';
|
||||
@ -1483,7 +1474,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get input_does_not_match_format => '入力が必須フォーマットと一致しません';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'メタデータプロバイダープラグイン';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1508,8 +1499,8 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get available_plugins => '利用可能なプラグイン';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'独自のプレイリスト/アルバム/アーティスト/フィードのメタデータプロバイダーを構成';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'オーディオスクロッブラー';
|
||||
|
||||
@ -1448,16 +1448,7 @@ class AppLocalizationsKa extends AppLocalizations {
|
||||
'ეს პლაგინი აწარმოებს თქვენი მუსიკის სქრობლინგს, რათა შექმნას თქვენი მოსმენის ისტორია.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'ნაგულისხმევი';
|
||||
|
||||
@override
|
||||
String get set_default => 'ნაგულისხმევად დაყენება';
|
||||
@ -1520,7 +1511,8 @@ class AppLocalizationsKa extends AppLocalizations {
|
||||
'შეყვანა არ ემთხვევა საჭირო ფორმატს';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins =>
|
||||
'მეტამონაცემების პროვაიდერების პლაგინები';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1545,8 +1537,8 @@ class AppLocalizationsKa extends AppLocalizations {
|
||||
String get available_plugins => 'ხელმისაწვდომი პლაგინები';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'დააყენეთ თქვენი საკუთარი პლეილისტის/ალბომის/არტისტის/ფიდის მეტამონაცემების პროვაიდერი';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'აუდიო სქრობლერები';
|
||||
|
||||
@ -1421,16 +1421,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get plugin_scrobbling_info => '이 플러그인은 음악을 스크로블하여 청취 기록을 생성합니다.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => '기본';
|
||||
|
||||
@override
|
||||
String get set_default => '기본값으로 설정';
|
||||
@ -1488,7 +1479,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get input_does_not_match_format => '입력이 필요한 형식과 일치하지 않습니다';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => '메타데이터 제공자 플러그인';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1512,8 +1503,8 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get available_plugins => '사용 가능한 플러그인';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'자신만의 플레이리스트/앨범/아티스트/피드 메타데이터 제공자 구성';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => '오디오 스크로블러';
|
||||
|
||||
@ -1454,16 +1454,7 @@ class AppLocalizationsNe extends AppLocalizations {
|
||||
'यो प्लगइनले तपाईंको सुन्ने इतिहास उत्पन्न गर्न तपाईंको संगीतलाई स्क्रब्बल गर्दछ।';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'पूर्वनिर्धारित';
|
||||
|
||||
@override
|
||||
String get set_default => 'पूर्वनिर्धारित सेट गर्नुहोस्';
|
||||
@ -1524,7 +1515,7 @@ class AppLocalizationsNe extends AppLocalizations {
|
||||
String get input_does_not_match_format => 'इनपुट आवश्यक ढाँचासँग मेल खाँदैन';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'मेटाडेटा प्रदायक प्लगइनहरू';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1549,8 +1540,8 @@ class AppLocalizationsNe extends AppLocalizations {
|
||||
String get available_plugins => 'उपलब्ध प्लगइनहरू';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'तपाईंको आफ्नै प्लेलिस्ट/एल्बम/कलाकार/फिड मेटाडेटा प्रदायक कन्फिगर गर्नुहोस्';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'अडियो स्क्रब्बलरहरू';
|
||||
|
||||
@ -1446,16 +1446,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Deze plugin scrobblet uw muziek om uw luistergeschiedenis te genereren.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Standaard';
|
||||
|
||||
@override
|
||||
String get set_default => 'Instellen als standaard';
|
||||
@ -1518,7 +1509,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Invoer komt niet overeen met het vereiste formaat';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Metadata-aanbieder Plugins';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1543,8 +1534,8 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get available_plugins => 'Beschikbare plugins';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Configureer uw eigen metadata-aanbieder voor afspeellijst/album/artiest/feed';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Audioscrobblers';
|
||||
|
||||
@ -1449,16 +1449,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
'Ta wtyczka scrobbluje Twoją muzykę, aby wygenerować historię odsłuchań.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Domyślna';
|
||||
|
||||
@override
|
||||
String get set_default => 'Ustaw jako domyślną';
|
||||
@ -1520,7 +1511,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
'Wprowadzony tekst nie pasuje do wymaganego formatu';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Wtyczki dostawców metadanych';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1545,8 +1536,8 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get available_plugins => 'Dostępne wtyczki';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Skonfiguruj własnego dostawcę metadanych dla playlisty/albumu/artysty/kanału';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Scrobblery audio';
|
||||
|
||||
@ -1446,16 +1446,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
'Este plugin faz o scrobbling de sua música para gerar seu histórico de audição.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Padrão';
|
||||
|
||||
@override
|
||||
String get set_default => 'Definir como padrão';
|
||||
@ -1517,7 +1508,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
'A entrada não corresponde ao formato exigido';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Plugins do provedor de metadados';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1542,8 +1533,8 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get available_plugins => 'Plugins disponíveis';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Configure seu próprio provedor de metadados de playlist/álbum/artista/feed';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Scrobblers de áudio';
|
||||
|
||||
@ -1448,16 +1448,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Этот плагин скробблит вашу музыку для создания вашей истории прослушиваний.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'По умолчанию';
|
||||
|
||||
@override
|
||||
String get set_default => 'Установить по умолчанию';
|
||||
@ -1520,7 +1511,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Введенные данные не соответствуют требуемому формату';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Плагины поставщика метаданных';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1545,8 +1536,8 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get available_plugins => 'Доступные плагины';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Настройте свой собственный поставщик метаданных для плейлиста/альбома/артиста/ленты';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Аудио скробблеры';
|
||||
|
||||
@ -1455,16 +1455,7 @@ class AppLocalizationsTa extends AppLocalizations {
|
||||
'இந்த பிளகின் உங்கள் கேட்பதின் வரலாற்றை உருவாக்க உங்கள் இசையை ஸ்க்ரோப்ள் செய்கிறது.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'இயல்புநிலை';
|
||||
|
||||
@override
|
||||
String get set_default => 'இயல்புநிலையாக அமைக்கவும்';
|
||||
@ -1526,7 +1517,7 @@ class AppLocalizationsTa extends AppLocalizations {
|
||||
'உள்ளீடு தேவையான வடிவத்துடன் பொருந்தவில்லை';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'மெட்டாடேட்டா வழங்குநர் பிளகின்கள்';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1551,8 +1542,8 @@ class AppLocalizationsTa extends AppLocalizations {
|
||||
String get available_plugins => 'கிடைக்கக்கூடிய பிளகின்கள்';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'உங்கள் சொந்த பிளேலிஸ்ட்/ஆல்பம்/கலைஞர்/ஊட்ட மெட்டாடேட்டா வழங்குநரை உள்ளமைக்கவும்';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'ஆடியோ ஸ்க்ரோப்ளர்கள்';
|
||||
|
||||
@ -1440,16 +1440,7 @@ class AppLocalizationsTh extends AppLocalizations {
|
||||
'ปลั๊กอินนี้จะ scrobble เพลงของคุณเพื่อสร้างประวัติการฟังของคุณ';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'ค่าเริ่มต้น';
|
||||
|
||||
@override
|
||||
String get set_default => 'ตั้งค่าเริ่มต้น';
|
||||
@ -1509,7 +1500,7 @@ class AppLocalizationsTh extends AppLocalizations {
|
||||
String get input_does_not_match_format => 'อินพุตไม่ตรงกับรูปแบบที่ต้องการ';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'ปลั๊กอินผู้ให้บริการเมตาดาต้า';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1534,8 +1525,8 @@ class AppLocalizationsTh extends AppLocalizations {
|
||||
String get available_plugins => 'ปลั๊กอินที่มีอยู่';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'กำหนดค่าผู้ให้บริการเมตาดาต้าเพลย์ลิสต์/อัลบั้ม/ศิลปิน/ฟีดของคุณเอง';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'เครื่อง scrobbler เสียง';
|
||||
|
||||
@ -1456,16 +1456,7 @@ class AppLocalizationsTl extends AppLocalizations {
|
||||
'Sinis-scrobble ng plugin na ito ang iyong musika upang mabuo ang iyong kasaysayan ng pakikinig.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Default';
|
||||
|
||||
@override
|
||||
String get set_default => 'Itakda bilang default';
|
||||
@ -1527,7 +1518,7 @@ class AppLocalizationsTl extends AppLocalizations {
|
||||
'Ang input ay hindi tumutugma sa kinakailangang format';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Mga Plugin ng Metadata Provider';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1552,8 +1543,8 @@ class AppLocalizationsTl extends AppLocalizations {
|
||||
String get available_plugins => 'Mga available na plugin';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'I-configure ang iyong sariling playlist/album/artist/feed metadata provider';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Mga Audio Scrobbler';
|
||||
|
||||
@ -1450,16 +1450,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
'Bu eklenti, dinleme geçmişinizi oluşturmak için müziğinizi scrobble eder.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Varsayılan';
|
||||
|
||||
@override
|
||||
String get set_default => 'Varsayılan olarak ayarla';
|
||||
@ -1520,7 +1511,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get input_does_not_match_format => 'Girdi, gerekli biçimle eşleşmiyor';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Meta Veri Sağlayıcısı Eklentileri';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1545,8 +1536,8 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get available_plugins => 'Mevcut eklentiler';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Kendi çalma listenizi/albümünüzü/sanatçınızı/akış meta veri sağlayıcınızı yapılandırın';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Ses Scrobbler\'lar';
|
||||
|
||||
@ -1446,16 +1446,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
'Цей плагін скроббить вашу музику, щоб створити вашу історію прослуховувань.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'За замовчуванням';
|
||||
|
||||
@override
|
||||
String get set_default => 'Встановити за замовчуванням';
|
||||
@ -1516,7 +1507,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
'Введені дані не відповідають необхідному формату';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Плагіни провайдера метаданих';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1541,8 +1532,8 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get available_plugins => 'Доступні плагіни';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'Налаштуйте свій власний провайдер метаданих для плейлиста/альбому/виконавця/стрічки';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Аудіо скробблери';
|
||||
|
||||
@ -1450,16 +1450,7 @@ class AppLocalizationsVi extends AppLocalizations {
|
||||
'Plugin này scrobble nhạc của bạn để tạo lịch sử nghe của bạn.';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => 'Mặc định';
|
||||
|
||||
@override
|
||||
String get set_default => 'Đặt làm mặc định';
|
||||
@ -1522,7 +1513,7 @@ class AppLocalizationsVi extends AppLocalizations {
|
||||
'Đầu vào không khớp với định dạng yêu cầu';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => 'Plugin Nhà cung cấp siêu dữ liệu';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1547,8 +1538,8 @@ class AppLocalizationsVi extends AppLocalizations {
|
||||
String get available_plugins => 'Các plugin có sẵn';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin =>
|
||||
'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';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => 'Bộ scrobbler âm thanh';
|
||||
|
||||
@ -1412,16 +1412,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get plugin_scrobbling_info => '此插件会 scrobble 您的音乐以生成您的收听历史记录。';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get default_plugin => '默认';
|
||||
|
||||
@override
|
||||
String get set_default => '设为默认';
|
||||
@ -1478,7 +1469,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get input_does_not_match_format => '输入与所需格式不匹配';
|
||||
|
||||
@override
|
||||
String get plugins => 'Plugins';
|
||||
String get metadata_provider_plugins => '元数据提供者插件';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
@ -1502,8 +1493,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get available_plugins => '可用插件';
|
||||
|
||||
@override
|
||||
String get configure_plugins =>
|
||||
'Configure your own metadata provider and audio source plugins';
|
||||
String get configure_your_own_metadata_plugin => '配置您自己的播放列表/专辑/艺人/订阅元数据提供者';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => '音频 Scrobblers';
|
||||
@ -2929,6 +2919,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
@override
|
||||
String get plugin_scrobbling_info => '此外掛程式會 Scrobble 您的音樂以產生您的收聽記錄。';
|
||||
|
||||
@override
|
||||
String get default_plugin => '預設';
|
||||
|
||||
@override
|
||||
String get set_default => '設為預設';
|
||||
|
||||
@ -2983,6 +2976,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
@override
|
||||
String get input_does_not_match_format => '輸入不符合所需格式';
|
||||
|
||||
@override
|
||||
String get metadata_provider_plugins => '中繼資料供應商外掛程式';
|
||||
|
||||
@override
|
||||
String get paste_plugin_download_url =>
|
||||
'貼上下載網址、GitHub/Codeberg 儲存庫網址或 .smplug 檔案的直接連結';
|
||||
@ -3004,6 +3000,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
@override
|
||||
String get available_plugins => '可用的外掛程式';
|
||||
|
||||
@override
|
||||
String get configure_your_own_metadata_plugin => '設定您自己的播放清單/專輯/藝人/動態中繼資料供應商';
|
||||
|
||||
@override
|
||||
String get audio_scrobblers => '音訊 Scrobblers';
|
||||
|
||||
|
||||
@ -83,8 +83,6 @@ Future<void> main(List<String> rawArgs) async {
|
||||
// force High Refresh Rate on some Android devices (like One Plus)
|
||||
if (kIsAndroid) {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
}
|
||||
if (kIsAndroid || kIsDesktop) {
|
||||
await NewPipeExtractor.init();
|
||||
}
|
||||
|
||||
@ -152,13 +150,11 @@ class Spotube extends HookConsumerWidget {
|
||||
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
|
||||
ref.listen(bonsoirProvider, (_, __) {});
|
||||
ref.listen(connectClientsProvider, (_, __) {});
|
||||
ref.listen(serverProvider, (_, __) {});
|
||||
ref.listen(trayManagerProvider, (_, __) {});
|
||||
ref.listen(metadataPluginsProvider, (_, __) {});
|
||||
ref.listen(metadataPluginProvider, (_, __) {});
|
||||
ref.listen(audioSourcePluginProvider, (_, __) {});
|
||||
ref.listen(serverProvider, (_, __) {});
|
||||
ref.listen(trayManagerProvider, (_, __) {});
|
||||
ref.listen(metadataPluginUpdateCheckerProvider, (_, __) {});
|
||||
ref.listen(audioSourcePluginUpdateCheckerProvider, (_, __) {});
|
||||
|
||||
useFixWindowStretching();
|
||||
useDisableBatteryOptimizations();
|
||||
|
||||
@ -16,13 +16,13 @@ import 'package:spotube/models/metadata/market.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/services/kv_store/encrypted_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:spotube/modules/settings/color_scheme_picker_dialog.dart';
|
||||
import 'package:drift/native.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/yt_dlp_engine.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
|
||||
@ -58,14 +58,14 @@ part 'typeconverters/subtitle.dart';
|
||||
AudioPlayerStateTable,
|
||||
HistoryTable,
|
||||
LyricsTable,
|
||||
PluginsTable,
|
||||
MetadataPluginsTable,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 10;
|
||||
int get schemaVersion => 8;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@ -199,28 +199,6 @@ 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/drift.dart' as i1;
|
||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/metadata/market.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
final class Schema2 extends i0.VersionedSchema {
|
||||
@ -329,7 +329,8 @@ class Shape2 extends i0.VersionedTable {
|
||||
|
||||
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('audio_quality', aliasedName, false,
|
||||
type: i1.DriftSqlType.string, defaultValue: Constant("high"));
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: Constant(SourceQualities.high.name));
|
||||
i1.GeneratedColumn<bool> _column_8(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>('album_color_sync', aliasedName, false,
|
||||
type: i1.DriftSqlType.bool,
|
||||
@ -416,13 +417,16 @@ i1.GeneratedColumn<String> _column_25(String aliasedName) =>
|
||||
defaultValue: Constant(ThemeMode.system.name));
|
||||
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('audio_source', aliasedName, false,
|
||||
type: i1.DriftSqlType.string, defaultValue: Constant("youtube"));
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: Constant(AudioSource.youtube.name));
|
||||
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('stream_music_codec', aliasedName, false,
|
||||
type: i1.DriftSqlType.string, defaultValue: Constant("weba"));
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: Constant(SourceCodecs.weba.name));
|
||||
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('download_music_codec', aliasedName, false,
|
||||
type: i1.DriftSqlType.string, defaultValue: Constant("m4a"));
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: Constant(SourceCodecs.m4a.name));
|
||||
i1.GeneratedColumn<bool> _column_29(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>('discord_presence', aliasedName, false,
|
||||
type: i1.DriftSqlType.bool,
|
||||
@ -507,7 +511,8 @@ i1.GeneratedColumn<String> _column_38(String aliasedName) =>
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('source_type', aliasedName, false,
|
||||
type: i1.DriftSqlType.string, defaultValue: Constant("youtube"));
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: Constant(SourceType.youtube.name));
|
||||
|
||||
class Shape6 extends i0.VersionedTable {
|
||||
Shape6({required super.source, required super.alias}) : super.aliased();
|
||||
@ -1402,7 +1407,7 @@ final class Schema5 extends i0.VersionedSchema {
|
||||
i1.GeneratedColumn<String> _column_55(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('accent_color_scheme', aliasedName, false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: const Constant("Orange:0xFFf97315"));
|
||||
defaultValue: const Constant("Slate:0xff64748b"));
|
||||
|
||||
final class Schema6 extends i0.VersionedSchema {
|
||||
Schema6({required super.database}) : super(version: 6);
|
||||
@ -2048,7 +2053,7 @@ final class Schema8 extends i0.VersionedSchema {
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_69,
|
||||
_column_55,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
@ -2183,7 +2188,7 @@ final class Schema8 extends i0.VersionedSchema {
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_67,
|
||||
_column_70,
|
||||
_column_69,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
@ -2195,550 +2200,8 @@ final class Schema8 extends i0.VersionedSchema {
|
||||
}
|
||||
|
||||
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,
|
||||
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({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@ -2747,8 +2210,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
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, 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 {
|
||||
switch (currentVersion) {
|
||||
@ -2787,16 +2248,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from7To8(migrator, schema);
|
||||
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:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@ -2811,8 +2262,6 @@ i1.OnUpgrade stepByStep({
|
||||
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, 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(
|
||||
step: migrationSteps(
|
||||
@ -2823,6 +2272,4 @@ i1.OnUpgrade stepByStep({
|
||||
from5To6: from5To6,
|
||||
from6To7: from6To7,
|
||||
from7To8: from7To8,
|
||||
from8To9: from8To9,
|
||||
from9To10: from9To10,
|
||||
));
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class PluginsTable extends Table {
|
||||
class MetadataPluginsTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text().withLength(min: 1, max: 50)();
|
||||
TextColumn get description => text()();
|
||||
@ -9,11 +9,8 @@ class PluginsTable extends Table {
|
||||
TextColumn get entryPoint => text()();
|
||||
TextColumn get apis => text().map(const StringListConverter())();
|
||||
TextColumn get abilities => text().map(const StringListConverter())();
|
||||
BoolColumn get selectedForMetadata =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get selectedForAudioSource =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get selected => boolean().withDefault(const Constant(false))();
|
||||
TextColumn get repository => text().nullable()();
|
||||
TextColumn get pluginApiVersion =>
|
||||
text().withDefault(const Constant('2.0.0'))();
|
||||
text().withDefault(const Constant('1.0.0'))();
|
||||
}
|
||||
|
||||
@ -11,6 +11,17 @@ enum CloseBehavior {
|
||||
close,
|
||||
}
|
||||
|
||||
enum AudioSource {
|
||||
youtube("YouTube"),
|
||||
piped("Piped"),
|
||||
jiosaavn("JioSaavn"),
|
||||
invidious("Invidious"),
|
||||
dabMusic("DAB Music");
|
||||
|
||||
final String label;
|
||||
const AudioSource(this.label);
|
||||
}
|
||||
|
||||
enum YoutubeClientEngine {
|
||||
ytDlp("yt-dlp"),
|
||||
youtubeExplode("YouTubeExplode"),
|
||||
@ -45,6 +56,8 @@ enum SearchMode {
|
||||
|
||||
class PreferencesTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get audioQuality => textEnum<SourceQualities>()
|
||||
.withDefault(Constant(SourceQualities.high.name))();
|
||||
BoolColumn get albumColorSync =>
|
||||
boolean().withDefault(const Constant(true))();
|
||||
BoolColumn get amoledDarkTheme =>
|
||||
@ -76,11 +89,20 @@ class PreferencesTable extends Table {
|
||||
TextColumn get downloadLocation => text().withDefault(const Constant(""))();
|
||||
TextColumn get localLibraryLocation =>
|
||||
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 =>
|
||||
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
|
||||
TextColumn get audioSourceId => text().nullable()();
|
||||
TextColumn get audioSource =>
|
||||
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
|
||||
TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>()
|
||||
.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 =>
|
||||
boolean().withDefault(const Constant(true))();
|
||||
BoolColumn get endlessPlayback =>
|
||||
@ -94,6 +116,7 @@ class PreferencesTable extends Table {
|
||||
static PreferencesTableData defaults() {
|
||||
return PreferencesTableData(
|
||||
id: 0,
|
||||
audioQuality: SourceQualities.high,
|
||||
albumColorSync: true,
|
||||
amoledDarkTheme: false,
|
||||
checkUpdate: true,
|
||||
@ -109,11 +132,13 @@ class PreferencesTable extends Table {
|
||||
searchMode: SearchMode.youtube,
|
||||
downloadLocation: "",
|
||||
localLibraryLocation: [],
|
||||
pipedInstance: "https://pipedapi.kavin.rocks",
|
||||
invidiousInstance: "https://inv.nadeko.net",
|
||||
themeMode: ThemeMode.system,
|
||||
audioSourceId: null,
|
||||
youtubeClientEngine: kIsIOS
|
||||
? YoutubeClientEngine.youtubeExplode
|
||||
: YoutubeClientEngine.newPipe,
|
||||
audioSource: AudioSource.youtube,
|
||||
youtubeClientEngine: YoutubeClientEngine.youtubeExplode,
|
||||
streamMusicCodec: SourceCodecs.m4a,
|
||||
downloadMusicCodec: SourceCodecs.m4a,
|
||||
discordPresence: true,
|
||||
endlessPlayback: true,
|
||||
enableConnect: false,
|
||||
|
||||
@ -1,9 +1,26 @@
|
||||
part of '../database.dart';
|
||||
|
||||
enum SourceType {
|
||||
youtube._("YouTube"),
|
||||
youtubeMusic._("YouTube Music"),
|
||||
jiosaavn._("JioSaavn"),
|
||||
dabMusic._("DAB Music");
|
||||
|
||||
final String label;
|
||||
|
||||
const SourceType._(this.label);
|
||||
}
|
||||
|
||||
@TableIndex(
|
||||
name: "uniq_track_match",
|
||||
columns: {#trackId, #sourceId, #sourceType},
|
||||
unique: true,
|
||||
)
|
||||
class SourceMatchTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get trackId => text()();
|
||||
TextColumn get sourceInfo => text().withDefault(const Constant("{}"))();
|
||||
TextColumn get sourceType => text()();
|
||||
TextColumn get sourceId => text()();
|
||||
TextColumn get sourceType =>
|
||||
textEnum<SourceType>().withDefault(Constant(SourceType.youtube.name))();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
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,7 +5,6 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart';
|
||||
@ -16,7 +15,6 @@ import 'package:spotube/utils/primitive_utils.dart';
|
||||
part 'metadata.g.dart';
|
||||
part 'metadata.freezed.dart';
|
||||
|
||||
part 'audio_source.dart';
|
||||
part 'album.dart';
|
||||
part 'artist.dart';
|
||||
part 'browse.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,123 +6,6 @@ part of 'metadata.dart';
|
||||
// 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(
|
||||
id: json['id'] as String,
|
||||
@ -536,6 +419,7 @@ Map<String, dynamic> _$$SpotubeUserObjectImplToJson(
|
||||
|
||||
_$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
|
||||
_$PluginConfigurationImpl(
|
||||
type: $enumDecode(_$PluginTypeEnumMap, json['type']),
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
version: json['version'] as String,
|
||||
@ -556,6 +440,7 @@ _$PluginConfigurationImpl _$$PluginConfigurationImplFromJson(Map json) =>
|
||||
Map<String, dynamic> _$$PluginConfigurationImplToJson(
|
||||
_$PluginConfigurationImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$PluginTypeEnumMap[instance.type]!,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'version': instance.version,
|
||||
@ -568,6 +453,10 @@ Map<String, dynamic> _$$PluginConfigurationImplToJson(
|
||||
'repository': instance.repository,
|
||||
};
|
||||
|
||||
const _$PluginTypeEnumMap = {
|
||||
PluginType.metadata: 'metadata',
|
||||
};
|
||||
|
||||
const _$PluginApisEnumMap = {
|
||||
PluginApis.webview: 'webview',
|
||||
PluginApis.localstorage: 'localstorage',
|
||||
@ -577,8 +466,6 @@ const _$PluginApisEnumMap = {
|
||||
const _$PluginAbilitiesEnumMap = {
|
||||
PluginAbilities.authentication: 'authentication',
|
||||
PluginAbilities.scrobbling: 'scrobbling',
|
||||
PluginAbilities.metadata: 'metadata',
|
||||
PluginAbilities.audioSource: 'audio-source',
|
||||
};
|
||||
|
||||
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
|
||||
@ -603,8 +490,6 @@ _$MetadataPluginRepositoryImpl _$$MetadataPluginRepositoryImplFromJson(
|
||||
owner: json['owner'] as String,
|
||||
description: json['description'] as String,
|
||||
repoUrl: json['repoUrl'] as String,
|
||||
topics:
|
||||
(json['topics'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$MetadataPluginRepositoryImplToJson(
|
||||
@ -614,5 +499,4 @@ Map<String, dynamic> _$$MetadataPluginRepositoryImplToJson(
|
||||
'owner': instance.owner,
|
||||
'description': instance.description,
|
||||
'repoUrl': instance.repoUrl,
|
||||
'topics': instance.topics,
|
||||
};
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
part of 'metadata.dart';
|
||||
|
||||
enum PluginType { metadata }
|
||||
|
||||
enum PluginApis { webview, localstorage, timezone }
|
||||
|
||||
enum PluginAbilities {
|
||||
authentication,
|
||||
scrobbling,
|
||||
metadata,
|
||||
@JsonValue('audio-source')
|
||||
audioSource,
|
||||
}
|
||||
enum PluginAbilities { authentication, scrobbling }
|
||||
|
||||
@freezed
|
||||
class PluginConfiguration with _$PluginConfiguration {
|
||||
const PluginConfiguration._();
|
||||
|
||||
factory PluginConfiguration({
|
||||
required PluginType type,
|
||||
required String name,
|
||||
required String description,
|
||||
required String version,
|
||||
|
||||
@ -7,7 +7,6 @@ class MetadataPluginRepository with _$MetadataPluginRepository {
|
||||
required String owner,
|
||||
required String description,
|
||||
required String repoUrl,
|
||||
required List<String> topics,
|
||||
}) = _MetadataPluginRepository;
|
||||
|
||||
factory MetadataPluginRepository.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@ -1,15 +1,122 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:spotube/models/database/database.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';
|
||||
|
||||
@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,
|
||||
required String qualityLabel,
|
||||
}) = _TrackSource;
|
||||
|
||||
factory TrackSource.fromJson(Map<String, dynamic> json) =>
|
||||
_$TrackSourceFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class BasicSourcedTrack {
|
||||
final SpotubeFullTrackObject query;
|
||||
final SpotubeAudioSourceMatchObject info;
|
||||
final String source;
|
||||
final List<SpotubeAudioSourceStreamObject> sources;
|
||||
final List<SpotubeAudioSourceMatchObject> siblings;
|
||||
final TrackSourceQuery query;
|
||||
final AudioSource source;
|
||||
final TrackSourceInfo info;
|
||||
final List<TrackSource> sources;
|
||||
final List<TrackSourceInfo> siblings;
|
||||
BasicSourcedTrack({
|
||||
required this.query,
|
||||
required this.source,
|
||||
|
||||
803
lib/models/playback/track_sources.freezed.dart
Normal file
803
lib/models/playback/track_sources.freezed.dart
Normal file
@ -0,0 +1,803 @@
|
||||
// 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;
|
||||
|
||||
/// Serializes this TrackSourceQuery to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of TrackSourceQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$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;
|
||||
|
||||
/// Create a copy of TrackSourceQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@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);
|
||||
|
||||
/// Create a copy of TrackSourceQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@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(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
title,
|
||||
const DeepCollectionEquality().hash(_artists),
|
||||
album,
|
||||
durationMs,
|
||||
isrc,
|
||||
explicit);
|
||||
|
||||
/// Create a copy of TrackSourceQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@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;
|
||||
|
||||
/// Create a copy of TrackSourceQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$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;
|
||||
|
||||
/// Serializes this TrackSourceInfo to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of TrackSourceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$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;
|
||||
|
||||
/// Create a copy of TrackSourceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@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);
|
||||
|
||||
/// Create a copy of TrackSourceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@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(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, id, title, artists, thumbnail, pageUrl, durationMs);
|
||||
|
||||
/// Create a copy of TrackSourceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@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;
|
||||
|
||||
/// Create a copy of TrackSourceInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$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;
|
||||
String get qualityLabel => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this TrackSource to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of TrackSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$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,
|
||||
String qualityLabel});
|
||||
}
|
||||
|
||||
/// @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;
|
||||
|
||||
/// Create a copy of TrackSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? url = null,
|
||||
Object? quality = null,
|
||||
Object? codec = null,
|
||||
Object? bitrate = null,
|
||||
Object? qualityLabel = 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,
|
||||
qualityLabel: null == qualityLabel
|
||||
? _value.qualityLabel
|
||||
: qualityLabel // 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,
|
||||
String qualityLabel});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$TrackSourceImplCopyWithImpl<$Res>
|
||||
extends _$TrackSourceCopyWithImpl<$Res, _$TrackSourceImpl>
|
||||
implements _$$TrackSourceImplCopyWith<$Res> {
|
||||
__$$TrackSourceImplCopyWithImpl(
|
||||
_$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of TrackSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? url = null,
|
||||
Object? quality = null,
|
||||
Object? codec = null,
|
||||
Object? bitrate = null,
|
||||
Object? qualityLabel = 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,
|
||||
qualityLabel: null == qualityLabel
|
||||
? _value.qualityLabel
|
||||
: qualityLabel // 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,
|
||||
required this.qualityLabel});
|
||||
|
||||
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
|
||||
final String qualityLabel;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate, qualityLabel: $qualityLabel)';
|
||||
}
|
||||
|
||||
@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) &&
|
||||
(identical(other.qualityLabel, qualityLabel) ||
|
||||
other.qualityLabel == qualityLabel));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, url, quality, codec, bitrate, qualityLabel);
|
||||
|
||||
/// Create a copy of TrackSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@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,
|
||||
required final String qualityLabel}) = _$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;
|
||||
|
||||
/// Create a copy of TrackSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
String get qualityLabel;
|
||||
|
||||
/// Create a copy of TrackSource
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -7,18 +7,17 @@ part of 'track_sources.dart';
|
||||
// **************************************************************************
|
||||
|
||||
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
|
||||
query: SpotubeFullTrackObject.fromJson(
|
||||
query: TrackSourceQuery.fromJson(
|
||||
Map<String, dynamic>.from(json['query'] as Map)),
|
||||
source: json['source'] as String,
|
||||
info: SpotubeAudioSourceMatchObject.fromJson(
|
||||
source: $enumDecode(_$AudioSourceEnumMap, json['source']),
|
||||
info: TrackSourceInfo.fromJson(
|
||||
Map<String, dynamic>.from(json['info'] as Map)),
|
||||
sources: (json['sources'] as List<dynamic>)
|
||||
.map((e) => SpotubeAudioSourceStreamObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.map((e) => TrackSource.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
siblings: (json['siblings'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeAudioSourceMatchObject.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
?.map((e) =>
|
||||
TrackSourceInfo.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
@ -26,8 +25,92 @@ BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
|
||||
Map<String, dynamic> _$BasicSourcedTrackToJson(BasicSourcedTrack instance) =>
|
||||
<String, dynamic>{
|
||||
'query': instance.query.toJson(),
|
||||
'source': _$AudioSourceEnumMap[instance.source]!,
|
||||
'info': instance.info.toJson(),
|
||||
'source': instance.source,
|
||||
'sources': instance.sources.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',
|
||||
AudioSource.dabMusic: 'dabMusic',
|
||||
};
|
||||
|
||||
_$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,
|
||||
qualityLabel: json['qualityLabel'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$TrackSourceImplToJson(_$TrackSourceImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'url': instance.url,
|
||||
'quality': _$SourceQualitiesEnumMap[instance.quality]!,
|
||||
'codec': _$SourceCodecsEnumMap[instance.codec]!,
|
||||
'bitrate': instance.bitrate,
|
||||
'qualityLabel': instance.qualityLabel,
|
||||
};
|
||||
|
||||
const _$SourceQualitiesEnumMap = {
|
||||
SourceQualities.uncompressed: 'uncompressed',
|
||||
SourceQualities.high: 'high',
|
||||
SourceQualities.medium: 'medium',
|
||||
SourceQualities.low: 'low',
|
||||
};
|
||||
|
||||
const _$SourceCodecsEnumMap = {
|
||||
SourceCodecs.m4a: 'm4a',
|
||||
SourceCodecs.weba: 'weba',
|
||||
SourceCodecs.mp3: 'mp3',
|
||||
SourceCodecs.flac: 'flac',
|
||||
};
|
||||
|
||||
@ -6,8 +6,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
|
||||
const containers = ["m4a", "mp3", "mp4", "ogg", "wav", "flac"];
|
||||
final codecs = SourceCodecs.values.map((s) => s.name);
|
||||
|
||||
class LocalFolderCacheExportDialog extends HookConsumerWidget {
|
||||
final Directory exportDir;
|
||||
@ -29,8 +30,7 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
|
||||
final stream = cacheDir.list().where(
|
||||
(event) =>
|
||||
event is File &&
|
||||
containers
|
||||
.contains(path.extension(event.path).replaceAll(".", "")),
|
||||
codecs.contains(path.extension(event.path).replaceAll(".", "")),
|
||||
);
|
||||
|
||||
stream.listen(
|
||||
|
||||
@ -3,7 +3,6 @@ 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/components/markdown/markdown.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart';
|
||||
@ -13,60 +12,29 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/updater/update_checker.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 {
|
||||
final PluginConfiguration plugin;
|
||||
final bool isDefaultMetadata;
|
||||
final bool isDefaultAudioSource;
|
||||
final bool isDefault;
|
||||
const MetadataInstalledPluginItem({
|
||||
super.key,
|
||||
required this.plugin,
|
||||
required this.isDefaultMetadata,
|
||||
required this.isDefaultAudioSource,
|
||||
required this.isDefault,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final mediaQuery = MediaQuery.sizeOf(context);
|
||||
|
||||
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||
final audioSourcePlugin = ref.watch(audioSourcePluginProvider);
|
||||
final pluginSnapshot = switch ((isDefaultMetadata, isDefaultAudioSource)) {
|
||||
(true, _) => metadataPlugin,
|
||||
(false, true) => audioSourcePlugin,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
||||
|
||||
final requiresAuth = (isDefaultMetadata || isDefaultAudioSource) &&
|
||||
plugin.abilities.contains(PluginAbilities.authentication);
|
||||
final supportsScrobbling = isDefaultMetadata &&
|
||||
plugin.abilities.contains(PluginAbilities.scrobbling);
|
||||
|
||||
final isMetadataAuthenticatedSnapshot =
|
||||
final isAuthenticatedSnapshot =
|
||||
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;
|
||||
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
||||
final requiresAuth =
|
||||
isDefault && plugin.abilities.contains(PluginAbilities.authentication);
|
||||
final supportsScrobbling =
|
||||
isDefault && plugin.abilities.contains(PluginAbilities.scrobbling);
|
||||
final isAuthenticated = isAuthenticatedSnapshot.asData?.value == true;
|
||||
final updateAvailable =
|
||||
isDefault ? ref.watch(metadataPluginUpdateCheckerProvider) : null;
|
||||
final hasUpdate = isDefault && updateAvailable?.asData?.value != null;
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
@ -111,18 +79,6 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
spacing: 8,
|
||||
children: [
|
||||
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)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@ -244,73 +200,34 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (plugin.abilities.contains(PluginAbilities.metadata))
|
||||
Button.secondary(
|
||||
enabled: !isDefaultMetadata,
|
||||
onPressed: () async {
|
||||
await pluginsNotifier.setDefaultMetadataPlugin(plugin);
|
||||
},
|
||||
child: Text(
|
||||
isDefaultMetadata
|
||||
? context.l10n.default_metadata_source
|
||||
: context.l10n.set_default_metadata_source,
|
||||
),
|
||||
),
|
||||
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)
|
||||
Button.secondary(
|
||||
enabled: !isDefault,
|
||||
onPressed: () async {
|
||||
await pluginsNotifier.setDefaultPlugin(plugin);
|
||||
},
|
||||
child: Text(
|
||||
isDefault
|
||||
? context.l10n.default_plugin
|
||||
: context.l10n.set_default,
|
||||
),
|
||||
),
|
||||
if (isDefault)
|
||||
Consumer(builder: (context, ref, _) {
|
||||
final metadataSupportTextSnapshot =
|
||||
ref.watch(metadataPluginSupportTextProvider);
|
||||
final audioSourceSupportTextSnapshot =
|
||||
ref.watch(audioSourcePluginSupportTextProvider);
|
||||
|
||||
final supportTextSnapshot =
|
||||
switch ((isDefaultMetadata, isDefaultAudioSource)) {
|
||||
(true, _) => metadataSupportTextSnapshot,
|
||||
(false, true) => audioSourceSupportTextSnapshot,
|
||||
_ => null,
|
||||
};
|
||||
ref.watch(metadataPluginSupportTextProvider);
|
||||
|
||||
if ((supportTextSnapshot?.hasValue ?? false) &&
|
||||
supportTextSnapshot?.value == null) {
|
||||
if (supportTextSnapshot.hasValue &&
|
||||
supportTextSnapshot.value == null) {
|
||||
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)
|
||||
: Colors.pink[600];
|
||||
final textColor =
|
||||
context.theme.brightness == Brightness.dark
|
||||
final textColor = context.theme.brightness == Brightness.dark
|
||||
? Colors.pink[700]
|
||||
: Colors.pink[50];
|
||||
|
||||
@ -341,8 +258,8 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
context.l10n.support_plugin_development),
|
||||
title:
|
||||
Text(context.l10n.support_plugin_development),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: mediaQuery.height * 0.8,
|
||||
@ -352,9 +269,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: AppMarkdown(
|
||||
data: supportTextSnapshot
|
||||
?.asData?.value ??
|
||||
"",
|
||||
data: supportTextSnapshot.value ?? "",
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -373,28 +288,22 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
if ((isDefaultMetadata || isDefaultAudioSource) &&
|
||||
requiresAuth &&
|
||||
!isAuthenticated)
|
||||
const Spacer(),
|
||||
if (isDefault && requiresAuth && !isAuthenticated)
|
||||
Button.primary(
|
||||
onPressed: () async {
|
||||
await pluginSnapshot?.asData?.value?.auth
|
||||
.authenticate();
|
||||
await metadataPlugin.asData?.value?.auth.authenticate();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.login),
|
||||
child: Text(context.l10n.login),
|
||||
)
|
||||
else if ((isDefaultMetadata || isDefaultAudioSource) &&
|
||||
requiresAuth &&
|
||||
isAuthenticated)
|
||||
else if (isDefault && requiresAuth && isAuthenticated)
|
||||
Button.destructive(
|
||||
onPressed: () async {
|
||||
await pluginSnapshot?.asData?.value?.auth.logout();
|
||||
await metadataPlugin.asData?.value?.auth.logout();
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.logout),
|
||||
child: Text(context.l10n.logout),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@ -11,11 +11,6 @@ import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.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 {
|
||||
final MetadataPluginRepository pluginRepo;
|
||||
const MetadataPluginRepositoryItem({
|
||||
@ -213,12 +208,6 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
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)
|
||||
|
||||
@ -21,9 +21,11 @@ import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/modules/root/spotube_navigation_bar.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/volume_provider.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PlayerView extends HookConsumerWidget {
|
||||
final PanelController panelController;
|
||||
@ -43,7 +45,14 @@ class PlayerView extends HookConsumerWidget {
|
||||
final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source;
|
||||
final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
|
||||
final mediaQuery = MediaQuery.sizeOf(context);
|
||||
final qualityLabel = ref.watch(audioSourceQualityLabelProvider);
|
||||
|
||||
final activeSourceCodec = useMemoized(
|
||||
() {
|
||||
return currentActiveTrackSource
|
||||
?.getSourceOfCodec(currentActiveTrackSource.codec);
|
||||
},
|
||||
[currentActiveTrackSource?.sources, currentActiveTrackSource?.codec],
|
||||
);
|
||||
|
||||
final shouldHide = useState(true);
|
||||
|
||||
@ -108,6 +117,22 @@ class PlayerView extends HookConsumerWidget {
|
||||
)
|
||||
],
|
||||
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)
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(
|
||||
@ -251,6 +276,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
}),
|
||||
),
|
||||
const Gap(25),
|
||||
if (activeSourceCodec != null)
|
||||
OutlineBadge(
|
||||
style: const ButtonStyle.outline(
|
||||
size: ButtonSize.normal,
|
||||
@ -262,7 +288,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
leading: const Icon(SpotubeIcons.lightningOutlined),
|
||||
child: Text(qualityLabel),
|
||||
child: Text(activeSourceCodec.qualityLabel),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
@ -1,16 +1,60 @@
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/ui/button_tile.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.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/playback/track_sources.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/audio_player/querying_track_info.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 {
|
||||
final bool floating;
|
||||
@ -21,21 +65,94 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final controller = useScrollController();
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
||||
|
||||
final isLoading = useState(false);
|
||||
final isSearching = useState(false);
|
||||
final searchMode = useState(preferences.searchMode);
|
||||
final activeTrackSources = ref.watch(activeTrackSourcesProvider);
|
||||
final activeTrackNotifier = activeTrackSources.asData?.value?.notifier;
|
||||
final activeTrack = activeTrackSources.asData?.value?.track;
|
||||
final activeTrackSource = activeTrackSources.asData?.value?.source;
|
||||
|
||||
final siblings = useMemoized<List<SpotubeAudioSourceMatchObject>>(
|
||||
final title = ServiceUtils.getTitle(
|
||||
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
|
||||
? [
|
||||
if (activeTrackSource != null) activeTrackSource.info,
|
||||
...?activeTrackSource?.siblings,
|
||||
]
|
||||
: <SpotubeAudioSourceMatchObject>[],
|
||||
: <TrackSourceInfo>[],
|
||||
[activeTrackSource, isFetchingActiveTrack],
|
||||
);
|
||||
|
||||
@ -49,6 +166,74 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [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(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -60,15 +245,71 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
child: !isSearching.value
|
||||
? Text(
|
||||
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(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: activeTrackSources.isLoading
|
||||
child: isLoading.value
|
||||
? const SizedBox(
|
||||
width: double.infinity,
|
||||
child: LinearProgressIndicator(),
|
||||
@ -82,62 +323,42 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: InterScrollbar(
|
||||
controller: controller,
|
||||
child: ListView.separated(
|
||||
child: switch (isSearching.value) {
|
||||
false => ListView.separated(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
controller: controller,
|
||||
itemCount: siblings.length,
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
itemBuilder: (context, index) {
|
||||
final sourceInfo = siblings[index];
|
||||
itemBuilder: (context, index) => itemBuilder(
|
||||
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 ButtonTile(
|
||||
style: ButtonVariance.ghost,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
title: Text(
|
||||
sourceInfo.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
controller: controller,
|
||||
itemCount: snapshot.data!.length,
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
itemBuilder: (context, index) => itemBuilder(
|
||||
snapshot.data![index],
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
final mediaQuery = MediaQuery.sizeOf(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final layoutMode =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||
|
||||
@ -31,7 +31,7 @@ void useGlobalSubscriptions(WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => MetadataPluginUpdateAvailableDialog(
|
||||
plugin: pluginConfig.defaultMetadataPluginConfig!,
|
||||
plugin: pluginConfig.defaultPluginConfig!,
|
||||
update: pluginUpdate,
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,11 +1,32 @@
|
||||
import 'package:flutter/material.dart' show Badge;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/components/ui/button_tile.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/modules/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.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),
|
||||
AudioSource.dabMusic:
|
||||
Assets.images.logos.dabMusic.image(width: 20, height: 20),
|
||||
};
|
||||
|
||||
class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
||||
final VoidCallback onNext;
|
||||
final VoidCallback onPrevious;
|
||||
@ -21,19 +42,19 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
final preferencesNotifier = ref.read(userPreferencesProvider.notifier);
|
||||
|
||||
// final audioSourceToDescription = useMemoized(
|
||||
// () => {
|
||||
// AudioSource.youtube: "${context.l10n.youtube_source_description}\n"
|
||||
// "${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}",
|
||||
// AudioSource.piped: context.l10n.piped_source_description,
|
||||
// AudioSource.jiosaavn:
|
||||
// "${context.l10n.jiosaavn_source_description}\n"
|
||||
// "${context.l10n.highest_quality("320kbps mp4")}",
|
||||
// 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")}",
|
||||
// },
|
||||
// []);
|
||||
final audioSourceToDescription = useMemoized(
|
||||
() => {
|
||||
AudioSource.youtube: "${context.l10n.youtube_source_description}\n"
|
||||
"${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}",
|
||||
AudioSource.piped: context.l10n.piped_source_description,
|
||||
AudioSource.jiosaavn:
|
||||
"${context.l10n.jiosaavn_source_description}\n"
|
||||
"${context.l10n.highest_quality("320kbps mp4")}",
|
||||
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(
|
||||
child: BlurCard(
|
||||
@ -48,44 +69,44 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
// Align(
|
||||
// alignment: Alignment.centerLeft,
|
||||
// child: Text(context.l10n.select_audio_source).semiBold().large(),
|
||||
// ),
|
||||
// const Gap(16),
|
||||
// RadioGroup<AudioSource>(
|
||||
// value: preferences.audioSource,
|
||||
// onChanged: (value) {
|
||||
// preferencesNotifier.setAudioSource(value);
|
||||
// },
|
||||
// child: Wrap(
|
||||
// spacing: 6,
|
||||
// runSpacing: 6,
|
||||
// children: [
|
||||
// for (final source in AudioSource.values)
|
||||
// Badge(
|
||||
// isLabelVisible: source == AudioSource.dabMusic,
|
||||
// label: const Text("NEW"),
|
||||
// backgroundColor: Colors.lime[300],
|
||||
// textColor: Colors.black,
|
||||
// child: RadioCard(
|
||||
// value: source,
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// audioSourceToIconMap[source]!,
|
||||
// Text(source.label),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const Gap(16),
|
||||
// Text(
|
||||
// audioSourceToDescription[preferences.audioSource]!,
|
||||
// ).small().muted(),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(context.l10n.select_audio_source).semiBold().large(),
|
||||
),
|
||||
const Gap(16),
|
||||
RadioGroup<AudioSource>(
|
||||
value: preferences.audioSource,
|
||||
onChanged: (value) {
|
||||
preferencesNotifier.setAudioSource(value);
|
||||
},
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final source in AudioSource.values)
|
||||
Badge(
|
||||
isLabelVisible: source == AudioSource.dabMusic,
|
||||
label: const Text("NEW"),
|
||||
backgroundColor: Colors.lime[300],
|
||||
textColor: Colors.black,
|
||||
child: RadioCard(
|
||||
value: source,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
audioSourceToIconMap[source]!,
|
||||
Text(source.label),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Text(
|
||||
audioSourceToDescription[preferences.audioSource]!,
|
||||
).small().muted(),
|
||||
const Gap(16),
|
||||
ButtonTile(
|
||||
title: Text(context.l10n.endless_playback),
|
||||
|
||||
@ -16,7 +16,7 @@ class UserDownloadsPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final downloadManager = ref.watch(downloadManagerProvider);
|
||||
|
||||
final history = downloadManager.$history;
|
||||
final history = downloadManager.$backHistory;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -48,7 +48,7 @@ class UserDownloadsPage extends HookConsumerWidget {
|
||||
child: ListView.builder(
|
||||
itemCount: history.length,
|
||||
itemBuilder: (context, index) {
|
||||
return DownloadItem(track: history.elementAt(index).query);
|
||||
return DownloadItem(track: history.elementAt(index));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -346,11 +346,9 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
controller: controller,
|
||||
child: Skeletonizer(
|
||||
enabled: trackSnapshot.isLoading,
|
||||
child: CustomScrollView(
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList.builder(
|
||||
itemCount: trackSnapshot.isLoading
|
||||
? 5
|
||||
: filteredTracks.length,
|
||||
@ -379,9 +377,6 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverGap(200),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -403,7 +398,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
error: (error, stackTrace) =>
|
||||
Text(error.toString() + stackTrace.toString()),
|
||||
);
|
||||
}),
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -30,7 +30,6 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final tabState = useState<int>(0);
|
||||
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||
|
||||
final plugins = ref.watch(metadataPluginsProvider);
|
||||
@ -50,50 +49,19 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
|
||||
final pluginRepos = pluginReposSnapshot.asData?.value.items ?? [];
|
||||
if (installedPluginIds.isEmpty) return pluginRepos;
|
||||
final availablePlugins = pluginRepos
|
||||
return pluginRepos
|
||||
.whereNot((repo) => installedPluginIds.contains(repo.repoUrl))
|
||||
.toList();
|
||||
|
||||
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,
|
||||
],
|
||||
[plugins.asData?.value.plugins, pluginReposSnapshot.asData?.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(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
headers: [
|
||||
TitleBar(
|
||||
title: Text(context.l10n.plugins),
|
||||
title: Text(context.l10n.metadata_provider_plugins),
|
||||
)
|
||||
],
|
||||
child: Padding(
|
||||
@ -225,20 +193,6 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
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)
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
@ -253,20 +207,15 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
),
|
||||
const SliverGap(20),
|
||||
SliverList.separated(
|
||||
itemCount: installedPlugins?.length ?? 0,
|
||||
itemCount: plugins.asData?.value.plugins.length ?? 0,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
final plugin = installedPlugins![index];
|
||||
final isDefaultMetadata =
|
||||
plugins.asData!.value.defaultMetadataPluginConfig?.slug ==
|
||||
plugin.slug;
|
||||
final isDefaultAudioSource = plugins
|
||||
.asData!.value.defaultAudioSourcePluginConfig?.slug ==
|
||||
plugin.slug;
|
||||
final plugin = plugins.asData!.value.plugins[index];
|
||||
final isDefault =
|
||||
plugins.asData!.value.defaultPlugin == index;
|
||||
return MetadataInstalledPluginItem(
|
||||
plugin: plugin,
|
||||
isDefaultMetadata: isDefaultMetadata,
|
||||
isDefaultAudioSource: isDefaultAudioSource,
|
||||
isDefault: isDefault,
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -300,7 +249,6 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
description: "Loading...",
|
||||
repoUrl: "",
|
||||
owner: "",
|
||||
topics: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -21,8 +21,8 @@ class SettingsAccountSection extends HookConsumerWidget {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(SpotubeIcons.extensions),
|
||||
title: Text(context.l10n.plugins),
|
||||
subtitle: Text(context.l10n.configure_plugins),
|
||||
title: Text(context.l10n.metadata_provider_plugins),
|
||||
subtitle: Text(context.l10n.configure_your_own_metadata_plugin),
|
||||
onTap: () {
|
||||
context.pushRoute(const SettingsMetadataProviderRoute());
|
||||
},
|
||||
|
||||
@ -1,24 +1,30 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' show ListTile;
|
||||
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/routes.gr.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/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/components/adaptive/adaptive_select_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
|
||||
import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.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/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
|
||||
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
@ -28,15 +34,264 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
|
||||
final sourcePresets = ref.watch(audioSourcePresetsProvider);
|
||||
final sourcePresetsNotifier =
|
||||
ref.watch(audioSourcePresetsProvider.notifier);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SectionCardWithHeading(
|
||||
heading: context.l10n.playback,
|
||||
children: [
|
||||
AdaptiveSelectTile<YoutubeClientEngine>(
|
||||
AdaptiveSelectTile<SourceQualities>(
|
||||
secondary: const Icon(SpotubeIcons.audioQuality),
|
||||
title: Text(context.l10n.audio_quality),
|
||||
value: preferences.audioQuality,
|
||||
options: [
|
||||
if (preferences.audioSource == AudioSource.dabMusic)
|
||||
SelectItemButton(
|
||||
value: SourceQualities.uncompressed,
|
||||
child: Text(context.l10n.uncompressed),
|
||||
),
|
||||
SelectItemButton(
|
||||
value: SourceQualities.high,
|
||||
child: Text(context.l10n.high),
|
||||
),
|
||||
if (preferences.audioSource != AudioSource.dabMusic) ...[
|
||||
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),
|
||||
title: Text(context.l10n.youtube_engine),
|
||||
value: preferences.youtubeClientEngine,
|
||||
@ -52,7 +307,8 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
if (value == YoutubeClientEngine.ytDlp) {
|
||||
final customPath = KVStoreService.getYoutubeEnginePath(value);
|
||||
if (!await YtDlpEngine.isInstalled() &&
|
||||
(customPath == null || !await File(customPath).exists()) &&
|
||||
(customPath == null ||
|
||||
!await File(customPath).exists()) &&
|
||||
context.mounted) {
|
||||
final hasInstalled = await showDialog<bool>(
|
||||
context: context,
|
||||
@ -65,70 +321,45 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
preferencesNotifier.setYoutubeClientEngine(value);
|
||||
},
|
||||
),
|
||||
if (sourcePresets.presets.isNotEmpty) ...[
|
||||
AdaptiveSelectTile(
|
||||
secondary: const Icon(SpotubeIcons.api),
|
||||
title: Text(context.l10n.streaming_music_codec),
|
||||
value: sourcePresets.selectedStreamingContainerIndex,
|
||||
options: [
|
||||
for (final MapEntry(:key, value: preset)
|
||||
in sourcePresets.presets.asMap().entries)
|
||||
SelectItemButton(value: key, child: Text(preset.name)),
|
||||
],
|
||||
AudioSource.piped ||
|
||||
AudioSource.invidious =>
|
||||
AdaptiveSelectTile<SearchMode>(
|
||||
secondary: const Icon(SpotubeIcons.search),
|
||||
title: Text(context.l10n.search_mode),
|
||||
value: preferences.searchMode,
|
||||
options: SearchMode.values
|
||||
.map((e) => SelectItemButton(
|
||||
value: e,
|
||||
child: Text(e.label),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
sourcePresetsNotifier.setSelectedStreamingContainerIndex(value);
|
||||
preferencesNotifier.setSearchMode(value);
|
||||
},
|
||||
),
|
||||
AdaptiveSelectTile(
|
||||
secondary: const Icon(SpotubeIcons.api),
|
||||
title: const Text("Streaming music quality"),
|
||||
value: sourcePresets.selectedStreamingQualityIndex,
|
||||
options: [
|
||||
for (final MapEntry(:key, value: quality) in sourcePresets
|
||||
.presets[sourcePresets.selectedStreamingContainerIndex]
|
||||
.qualities
|
||||
.asMap()
|
||||
.entries)
|
||||
SelectItemButton(value: key, child: Text(quality.toString())),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
sourcePresetsNotifier.setSelectedStreamingQualityIndex(value);
|
||||
_ => const SizedBox.shrink(),
|
||||
},
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
crossFadeState: preferences.searchMode == SearchMode.youtube &&
|
||||
(preferences.audioSource == AudioSource.piped ||
|
||||
preferences.audioSource == AudioSource.youtube ||
|
||||
preferences.audioSource == AudioSource.invidious)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: ListTile(
|
||||
leading: const Icon(SpotubeIcons.skip),
|
||||
title: Text(context.l10n.skip_non_music),
|
||||
trailing: Switch(
|
||||
value: preferences.skipNonMusic,
|
||||
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);
|
||||
},
|
||||
),
|
||||
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);
|
||||
},
|
||||
secondChild: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
title: Text(context.l10n.cache_music),
|
||||
subtitle: kIsMobile
|
||||
@ -172,6 +403,50 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
onChanged: preferencesNotifier.setNormalizeAudio,
|
||||
),
|
||||
),
|
||||
if (const [AudioSource.jiosaavn, AudioSource.dabMusic]
|
||||
.contains(preferences.audioSource) ==
|
||||
false) ...[
|
||||
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(
|
||||
leading: const Icon(SpotubeIcons.repeat),
|
||||
title: Text(context.l10n.endless_playback),
|
||||
|
||||
@ -7,11 +7,12 @@ import 'package:media_kit/media_kit.dart';
|
||||
import 'package:spotube/extensions/list.dart';
|
||||
import 'package:spotube/models/database/database.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/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/discord_provider.dart';
|
||||
import 'package:spotube/provider/server/sourced_track_provider.dart';
|
||||
import 'package:spotube/provider/server/track_sources.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
@ -163,8 +164,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
final tracks = <SpotubeTrackObject>[];
|
||||
|
||||
for (final media in playlist.medias) {
|
||||
final track = trackGroupedById[SpotubeMedia.media(media).track.id]
|
||||
?.firstOrNull;
|
||||
final trackQuery = TrackSourceQuery.parseUri(media.uri);
|
||||
final track = trackGroupedById[trackQuery.id]?.firstOrNull;
|
||||
if (track != null) {
|
||||
tracks.add(track);
|
||||
}
|
||||
@ -399,9 +400,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
// because of timeout
|
||||
final intendedActiveTrack = medias.elementAt(initialIndex);
|
||||
if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
|
||||
ref.read(
|
||||
sourcedTrackProvider(
|
||||
intendedActiveTrack.track as SpotubeFullTrackObject,
|
||||
await ref.read(
|
||||
trackSourcesProvider(
|
||||
TrackSourceQuery.fromTrack(
|
||||
intendedActiveTrack.track as SpotubeFullTrackObject),
|
||||
).future,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,13 +3,14 @@ import 'dart:math';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/state.dart';
|
||||
import 'package:spotube/provider/discord_provider.dart';
|
||||
import 'package:spotube/provider/history/history.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/scrobble.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/server/sourced_track_provider.dart';
|
||||
import 'package:spotube/provider/server/track_sources.dart';
|
||||
import 'package:spotube/provider/skip_segments/skip_segments.dart';
|
||||
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
@ -155,7 +156,9 @@ class AudioPlayerStreamListeners {
|
||||
|
||||
try {
|
||||
await ref.read(
|
||||
sourcedTrackProvider(nextTrack as SpotubeFullTrackObject).future,
|
||||
trackSourcesProvider(
|
||||
TrackSourceQuery.fromTrack(nextTrack as SpotubeFullTrackObject),
|
||||
).future,
|
||||
);
|
||||
} finally {
|
||||
lastTrack = nextTrack.id;
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/server/sourced_track_provider.dart';
|
||||
import 'package:spotube/provider/server/track_sources.dart';
|
||||
|
||||
final queryingTrackInfoProvider = Provider<bool>((ref) {
|
||||
final audioPlayer = ref.watch(audioPlayerProvider);
|
||||
@ -15,9 +16,10 @@ final queryingTrackInfoProvider = Provider<bool>((ref) {
|
||||
}
|
||||
|
||||
return ref
|
||||
.watch(
|
||||
sourcedTrackProvider(
|
||||
audioPlayer.activeTrack! as SpotubeFullTrackObject),
|
||||
)
|
||||
.watch(trackSourcesProvider(
|
||||
TrackSourceQuery.fromTrack(
|
||||
audioPlayer.activeTrack! as SpotubeFullTrackObject,
|
||||
),
|
||||
))
|
||||
.isLoading;
|
||||
});
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
||||
|
||||
final invidiousInstancesProvider = FutureProvider((ref) async {
|
||||
final invidious = ref.watch(invidiousProvider);
|
||||
|
||||
final instances = await invidious.instances();
|
||||
|
||||
return instances
|
||||
.where((instance) => instance.details.type == "https")
|
||||
.toList();
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||
|
||||
final pipedInstancesFutureProvider = FutureProvider<List<PipedInstance>>(
|
||||
(ref) async {
|
||||
try {
|
||||
final pipedClient = ref.watch(pipedProvider);
|
||||
|
||||
return await pipedClient.instanceList();
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
return <PipedInstance>[];
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -2,8 +2,8 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
|
||||
import 'package:spotube/provider/server/sourced_track_provider.dart';
|
||||
import 'package:spotube/models/playback/track_sources.dart';
|
||||
import 'package:spotube/provider/server/track_sources.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -12,6 +12,7 @@ import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/download_manager/download_manager.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
@ -19,21 +20,20 @@ import 'package:spotube/utils/service_utils.dart';
|
||||
class DownloadManagerProvider extends ChangeNotifier {
|
||||
DownloadManagerProvider({required this.ref})
|
||||
: $history = <SourcedTrack>{},
|
||||
$backHistory = <SpotubeFullTrackObject>{},
|
||||
dl = DownloadManager() {
|
||||
dl.statusStream.listen((event) async {
|
||||
try {
|
||||
final (:request, :status) = event;
|
||||
|
||||
final sourcedTrack = $history.firstWhereOrNull(
|
||||
(element) =>
|
||||
element.getUrlOfQuality(
|
||||
downloadContainer,
|
||||
downloadQualityIndex,
|
||||
) ==
|
||||
request.url,
|
||||
(element) => element.getUrlOfCodec(downloadCodec) == request.url,
|
||||
);
|
||||
|
||||
if (sourcedTrack == null) return;
|
||||
final track = $backHistory.firstWhereOrNull(
|
||||
(element) => element.id == sourcedTrack.query.id,
|
||||
);
|
||||
if (track == null) return;
|
||||
|
||||
final savePath = getTrackFileUrl(sourcedTrack);
|
||||
// related to onFileExists
|
||||
@ -45,12 +45,11 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
await oldFile.exists()) {
|
||||
await oldFile.rename(savePath);
|
||||
}
|
||||
|
||||
if (status != DownloadStatus.completed ||
|
||||
//? WebA audiotagging is not supported yet
|
||||
//? Although in future by converting weba to opus & then tagging it
|
||||
//? is possible using vorbis comments
|
||||
downloadContainer.getFileExtension() == "weba") {
|
||||
downloadCodec == SourceCodecs.weba) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -61,13 +60,13 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final imageBytes = await ServiceUtils.downloadImage(
|
||||
(sourcedTrack.query.album.images).asUrlString(
|
||||
(track.album.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
index: 1,
|
||||
),
|
||||
);
|
||||
|
||||
final metadata = sourcedTrack.query.toMetadata(
|
||||
final metadata = track.toMetadata(
|
||||
fileLength: await file.length(),
|
||||
imageBytes: imageBytes,
|
||||
);
|
||||
@ -89,13 +88,8 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
|
||||
String get downloadDirectory =>
|
||||
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
|
||||
SpotubeAudioSourceContainerPreset get downloadContainer => ref.read(
|
||||
audioSourcePresetsProvider
|
||||
.select((s) => s.presets[s.selectedDownloadingContainerIndex]),
|
||||
);
|
||||
|
||||
int get downloadQualityIndex => ref.read(audioSourcePresetsProvider
|
||||
.select((s) => s.selectedDownloadingQualityIndex));
|
||||
SourceCodecs get downloadCodec =>
|
||||
ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec));
|
||||
|
||||
int get $downloadCount => dl
|
||||
.getAllDownloads()
|
||||
@ -109,16 +103,17 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
|
||||
final Set<SourcedTrack> $history;
|
||||
// these are the tracks which metadata hasn't been fetched yet
|
||||
final Set<SpotubeFullTrackObject> $backHistory;
|
||||
final DownloadManager dl;
|
||||
|
||||
String getTrackFileUrl(SourcedTrack track) {
|
||||
final name =
|
||||
"${track.query.name} - ${track.query.artists.map((e) => e.name).join(", ")}.${downloadContainer.getFileExtension()}";
|
||||
"${track.query.title} - ${track.query.artists.join(", ")}.${downloadCodec.name}";
|
||||
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
|
||||
}
|
||||
|
||||
bool isActive(SpotubeFullTrackObject track) {
|
||||
if ($history.any((e) => e.query.id == track.id)) return true;
|
||||
if ($backHistory.contains(track)) return true;
|
||||
|
||||
final sourcedTrack = $history.firstWhereOrNull(
|
||||
(element) => element.query.id == track.id,
|
||||
@ -135,15 +130,14 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
download.status.value == DownloadStatus.queued,
|
||||
)
|
||||
.map((e) => e.request.url)
|
||||
.contains(sourcedTrack.getUrlOfQuality(
|
||||
downloadContainer,
|
||||
downloadQualityIndex,
|
||||
)!);
|
||||
.contains(sourcedTrack.getUrlOfCodec(downloadCodec)!);
|
||||
}
|
||||
|
||||
/// For singular downloads
|
||||
Future<void> addToQueue(SpotubeFullTrackObject track) async {
|
||||
final sourcedTrack = await ref.read(sourcedTrackProvider(track).future);
|
||||
final sourcedTrack = await ref.read(
|
||||
trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future,
|
||||
);
|
||||
|
||||
final savePath = getTrackFileUrl(sourcedTrack);
|
||||
|
||||
@ -156,17 +150,40 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
await oldFile.rename("$savePath.old");
|
||||
}
|
||||
|
||||
if (sourcedTrack.codec == downloadCodec) {
|
||||
final downloadTask = await dl.addDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!,
|
||||
sourcedTrack.getUrlOfCodec(downloadCodec)!,
|
||||
savePath,
|
||||
);
|
||||
if (downloadTask != null) {
|
||||
$history.add(sourcedTrack);
|
||||
}
|
||||
} else {
|
||||
$backHistory.add(track);
|
||||
final sourcedTrack = await ref
|
||||
.read(
|
||||
trackSourcesProvider(
|
||||
TrackSourceQuery.fromTrack(track),
|
||||
).future,
|
||||
)
|
||||
.then((d) {
|
||||
$backHistory.remove(track);
|
||||
return d;
|
||||
});
|
||||
final downloadTask = await dl.addDownload(
|
||||
sourcedTrack.getUrlOfCodec(downloadCodec)!,
|
||||
savePath,
|
||||
);
|
||||
if (downloadTask != null) {
|
||||
$history.add(sourcedTrack);
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> batchAddToQueue(List<SpotubeFullTrackObject> tracks) async {
|
||||
$backHistory.addAll(tracks);
|
||||
notifyListeners();
|
||||
for (final track in tracks) {
|
||||
try {
|
||||
@ -187,21 +204,18 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
|
||||
Future<void> removeFromQueue(SpotubeFullTrackObject track) async {
|
||||
final sourcedTrack = await mapToSourcedTrack(track);
|
||||
await dl.removeDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
|
||||
await dl.removeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!);
|
||||
$history.remove(sourcedTrack);
|
||||
}
|
||||
|
||||
Future<void> pause(SpotubeFullTrackObject track) async {
|
||||
final sourcedTrack = await mapToSourcedTrack(track);
|
||||
return dl.pauseDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
|
||||
return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!);
|
||||
}
|
||||
|
||||
Future<void> resume(SpotubeFullTrackObject track) async {
|
||||
final sourcedTrack = await mapToSourcedTrack(track);
|
||||
return dl.resumeDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
|
||||
return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!);
|
||||
}
|
||||
|
||||
Future<void> retry(SpotubeFullTrackObject track) {
|
||||
@ -210,8 +224,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
|
||||
void cancel(SpotubeFullTrackObject track) async {
|
||||
final sourcedTrack = await mapToSourcedTrack(track);
|
||||
return dl.cancelDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
|
||||
return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!);
|
||||
}
|
||||
|
||||
void cancelAll() {
|
||||
@ -229,7 +242,9 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
return historicTrack;
|
||||
}
|
||||
|
||||
final sourcedTrack = await ref.read(sourcedTrackProvider(track).future);
|
||||
final sourcedTrack = await ref.read(
|
||||
trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future,
|
||||
);
|
||||
|
||||
return sourcedTrack;
|
||||
}
|
||||
@ -243,10 +258,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
if (sourcedTrack == null) {
|
||||
return null;
|
||||
}
|
||||
return dl
|
||||
.getDownload(sourcedTrack.getUrlOfQuality(
|
||||
downloadContainer, downloadQualityIndex)!)
|
||||
?.status;
|
||||
return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.status;
|
||||
}
|
||||
|
||||
ValueNotifier<double>? getProgressNotifier(SpotubeFullTrackObject track) {
|
||||
@ -256,10 +268,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
if (sourcedTrack == null) {
|
||||
return null;
|
||||
}
|
||||
return dl
|
||||
.getDownload(sourcedTrack.getUrlOfQuality(
|
||||
downloadContainer, downloadQualityIndex)!)
|
||||
?.progress;
|
||||
return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.progress;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
|
||||
|
||||
final audioSourceQualityLabelProvider = Provider<String>((ref) {
|
||||
final sourceQuality = ref.watch(audioSourcePresetsProvider);
|
||||
final sourceContainer = sourceQuality.presets
|
||||
.elementAtOrNull(sourceQuality.selectedStreamingContainerIndex);
|
||||
final quality = sourceContainer?.qualities
|
||||
.elementAtOrNull(sourceQuality.selectedStreamingQualityIndex);
|
||||
|
||||
return "${sourceContainer?.name ?? "Unknown"} • ${quality?.toString() ?? "Unknown"}";
|
||||
});
|
||||
@ -1,131 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
|
||||
part 'quality_presets.g.dart';
|
||||
part 'quality_presets.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class AudioSourcePresetsState with _$AudioSourcePresetsState {
|
||||
factory AudioSourcePresetsState({
|
||||
@Default([]) final List<SpotubeAudioSourceContainerPreset> presets,
|
||||
@Default(0) final int selectedStreamingQualityIndex,
|
||||
@Default(0) final int selectedStreamingContainerIndex,
|
||||
@Default(0) final int selectedDownloadingQualityIndex,
|
||||
@Default(0) final int selectedDownloadingContainerIndex,
|
||||
}) = _AudioSourcePresetsState;
|
||||
|
||||
factory AudioSourcePresetsState.fromJson(Map<String, dynamic> json) =>
|
||||
_$AudioSourcePresetsStateFromJson(json);
|
||||
}
|
||||
|
||||
class AudioSourceAvailableQualityPresetsNotifier
|
||||
extends Notifier<AudioSourcePresetsState> {
|
||||
@override
|
||||
build() {
|
||||
final audioSourceSnapshot = ref.watch(audioSourcePluginProvider);
|
||||
final audioSourceConfigSnapshot = ref.watch(
|
||||
metadataPluginsProvider.select((data) =>
|
||||
data.whenData((value) => value.defaultAudioSourcePluginConfig)),
|
||||
);
|
||||
|
||||
_initialize(audioSourceSnapshot, audioSourceConfigSnapshot);
|
||||
|
||||
listenSelf((previous, next) {
|
||||
final isNewLossless =
|
||||
next.presets.elementAtOrNull(next.selectedStreamingContainerIndex)
|
||||
is SpotubeAudioSourceContainerPresetLossless;
|
||||
final isOldLossless = previous?.presets
|
||||
.elementAtOrNull(previous.selectedStreamingContainerIndex)
|
||||
is SpotubeAudioSourceContainerPresetLossless;
|
||||
if (!isOldLossless && isNewLossless) {
|
||||
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
|
||||
} else if (isOldLossless && !isNewLossless) {
|
||||
audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB
|
||||
}
|
||||
});
|
||||
|
||||
return AudioSourcePresetsState();
|
||||
}
|
||||
|
||||
void _initialize(
|
||||
AsyncValue<MetadataPlugin?> audioSourceSnapshot,
|
||||
AsyncValue<PluginConfiguration?> audioSourceConfigSnapshot,
|
||||
) async {
|
||||
audioSourceConfigSnapshot.whenData((audioSourceConfig) {
|
||||
audioSourceSnapshot.whenData((audioSource) async {
|
||||
if (audioSource == null || audioSourceConfig == null) {
|
||||
throw Exception("Dude wat?");
|
||||
}
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
final persistedStateStr =
|
||||
preferences.getString("audioSourceState-${audioSourceConfig.slug}");
|
||||
|
||||
if (persistedStateStr != null) {
|
||||
state =
|
||||
AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr))
|
||||
.copyWith(
|
||||
presets: audioSource.audioSource.supportedPresets,
|
||||
);
|
||||
} else {
|
||||
state = AudioSourcePresetsState(
|
||||
presets: audioSource.audioSource.supportedPresets,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void setSelectedStreamingContainerIndex(int index) {
|
||||
state = state.copyWith(
|
||||
selectedStreamingContainerIndex: index,
|
||||
selectedStreamingQualityIndex:
|
||||
0, // Resetting both because it's a different quality
|
||||
);
|
||||
_updatePreferences();
|
||||
}
|
||||
|
||||
void setSelectedStreamingQualityIndex(int index) {
|
||||
state = state.copyWith(selectedStreamingQualityIndex: index);
|
||||
_updatePreferences();
|
||||
}
|
||||
|
||||
void setSelectedDownloadingContainerIndex(int index) {
|
||||
state = state.copyWith(
|
||||
selectedDownloadingContainerIndex: index,
|
||||
selectedDownloadingQualityIndex:
|
||||
0, // Resetting both because it's a different quality
|
||||
);
|
||||
_updatePreferences();
|
||||
}
|
||||
|
||||
void setSelectedDownloadingQualityIndex(int index) {
|
||||
state = state.copyWith(selectedDownloadingQualityIndex: index);
|
||||
_updatePreferences();
|
||||
}
|
||||
|
||||
void _updatePreferences() async {
|
||||
final audioSourceConfig = await ref.read(metadataPluginsProvider
|
||||
.selectAsync((data) => data.defaultAudioSourcePluginConfig));
|
||||
if (audioSourceConfig == null) {
|
||||
throw Exception("Dude wat?");
|
||||
}
|
||||
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
await preferences.setString(
|
||||
"audioSourceState-${audioSourceConfig.slug}",
|
||||
jsonEncode(state),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final audioSourcePresetsProvider = NotifierProvider<
|
||||
AudioSourceAvailableQualityPresetsNotifier, AudioSourcePresetsState>(
|
||||
() => AudioSourceAvailableQualityPresetsNotifier(),
|
||||
);
|
||||
@ -1,289 +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 'quality_presets.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');
|
||||
|
||||
AudioSourcePresetsState _$AudioSourcePresetsStateFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return _AudioSourcePresetsState.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AudioSourcePresetsState {
|
||||
List<SpotubeAudioSourceContainerPreset> get presets =>
|
||||
throw _privateConstructorUsedError;
|
||||
int get selectedStreamingQualityIndex => throw _privateConstructorUsedError;
|
||||
int get selectedStreamingContainerIndex => throw _privateConstructorUsedError;
|
||||
int get selectedDownloadingQualityIndex => throw _privateConstructorUsedError;
|
||||
int get selectedDownloadingContainerIndex =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this AudioSourcePresetsState to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of AudioSourcePresetsState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$AudioSourcePresetsStateCopyWith<AudioSourcePresetsState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AudioSourcePresetsStateCopyWith<$Res> {
|
||||
factory $AudioSourcePresetsStateCopyWith(AudioSourcePresetsState value,
|
||||
$Res Function(AudioSourcePresetsState) then) =
|
||||
_$AudioSourcePresetsStateCopyWithImpl<$Res, AudioSourcePresetsState>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{List<SpotubeAudioSourceContainerPreset> presets,
|
||||
int selectedStreamingQualityIndex,
|
||||
int selectedStreamingContainerIndex,
|
||||
int selectedDownloadingQualityIndex,
|
||||
int selectedDownloadingContainerIndex});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AudioSourcePresetsStateCopyWithImpl<$Res,
|
||||
$Val extends AudioSourcePresetsState>
|
||||
implements $AudioSourcePresetsStateCopyWith<$Res> {
|
||||
_$AudioSourcePresetsStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of AudioSourcePresetsState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? presets = null,
|
||||
Object? selectedStreamingQualityIndex = null,
|
||||
Object? selectedStreamingContainerIndex = null,
|
||||
Object? selectedDownloadingQualityIndex = null,
|
||||
Object? selectedDownloadingContainerIndex = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
presets: null == presets
|
||||
? _value.presets
|
||||
: presets // ignore: cast_nullable_to_non_nullable
|
||||
as List<SpotubeAudioSourceContainerPreset>,
|
||||
selectedStreamingQualityIndex: null == selectedStreamingQualityIndex
|
||||
? _value.selectedStreamingQualityIndex
|
||||
: selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
selectedStreamingContainerIndex: null == selectedStreamingContainerIndex
|
||||
? _value.selectedStreamingContainerIndex
|
||||
: selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex
|
||||
? _value.selectedDownloadingQualityIndex
|
||||
: selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
selectedDownloadingContainerIndex: null ==
|
||||
selectedDownloadingContainerIndex
|
||||
? _value.selectedDownloadingContainerIndex
|
||||
: selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$AudioSourcePresetsStateImplCopyWith<$Res>
|
||||
implements $AudioSourcePresetsStateCopyWith<$Res> {
|
||||
factory _$$AudioSourcePresetsStateImplCopyWith(
|
||||
_$AudioSourcePresetsStateImpl value,
|
||||
$Res Function(_$AudioSourcePresetsStateImpl) then) =
|
||||
__$$AudioSourcePresetsStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{List<SpotubeAudioSourceContainerPreset> presets,
|
||||
int selectedStreamingQualityIndex,
|
||||
int selectedStreamingContainerIndex,
|
||||
int selectedDownloadingQualityIndex,
|
||||
int selectedDownloadingContainerIndex});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$AudioSourcePresetsStateImplCopyWithImpl<$Res>
|
||||
extends _$AudioSourcePresetsStateCopyWithImpl<$Res,
|
||||
_$AudioSourcePresetsStateImpl>
|
||||
implements _$$AudioSourcePresetsStateImplCopyWith<$Res> {
|
||||
__$$AudioSourcePresetsStateImplCopyWithImpl(
|
||||
_$AudioSourcePresetsStateImpl _value,
|
||||
$Res Function(_$AudioSourcePresetsStateImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of AudioSourcePresetsState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? presets = null,
|
||||
Object? selectedStreamingQualityIndex = null,
|
||||
Object? selectedStreamingContainerIndex = null,
|
||||
Object? selectedDownloadingQualityIndex = null,
|
||||
Object? selectedDownloadingContainerIndex = null,
|
||||
}) {
|
||||
return _then(_$AudioSourcePresetsStateImpl(
|
||||
presets: null == presets
|
||||
? _value._presets
|
||||
: presets // ignore: cast_nullable_to_non_nullable
|
||||
as List<SpotubeAudioSourceContainerPreset>,
|
||||
selectedStreamingQualityIndex: null == selectedStreamingQualityIndex
|
||||
? _value.selectedStreamingQualityIndex
|
||||
: selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
selectedStreamingContainerIndex: null == selectedStreamingContainerIndex
|
||||
? _value.selectedStreamingContainerIndex
|
||||
: selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex
|
||||
? _value.selectedDownloadingQualityIndex
|
||||
: selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
selectedDownloadingContainerIndex: null ==
|
||||
selectedDownloadingContainerIndex
|
||||
? _value.selectedDownloadingContainerIndex
|
||||
: selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$AudioSourcePresetsStateImpl implements _AudioSourcePresetsState {
|
||||
_$AudioSourcePresetsStateImpl(
|
||||
{final List<SpotubeAudioSourceContainerPreset> presets = const [],
|
||||
this.selectedStreamingQualityIndex = 0,
|
||||
this.selectedStreamingContainerIndex = 0,
|
||||
this.selectedDownloadingQualityIndex = 0,
|
||||
this.selectedDownloadingContainerIndex = 0})
|
||||
: _presets = presets;
|
||||
|
||||
factory _$AudioSourcePresetsStateImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$AudioSourcePresetsStateImplFromJson(json);
|
||||
|
||||
final List<SpotubeAudioSourceContainerPreset> _presets;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<SpotubeAudioSourceContainerPreset> get presets {
|
||||
if (_presets is EqualUnmodifiableListView) return _presets;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_presets);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final int selectedStreamingQualityIndex;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int selectedStreamingContainerIndex;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int selectedDownloadingQualityIndex;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int selectedDownloadingContainerIndex;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AudioSourcePresetsState(presets: $presets, selectedStreamingQualityIndex: $selectedStreamingQualityIndex, selectedStreamingContainerIndex: $selectedStreamingContainerIndex, selectedDownloadingQualityIndex: $selectedDownloadingQualityIndex, selectedDownloadingContainerIndex: $selectedDownloadingContainerIndex)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$AudioSourcePresetsStateImpl &&
|
||||
const DeepCollectionEquality().equals(other._presets, _presets) &&
|
||||
(identical(other.selectedStreamingQualityIndex,
|
||||
selectedStreamingQualityIndex) ||
|
||||
other.selectedStreamingQualityIndex ==
|
||||
selectedStreamingQualityIndex) &&
|
||||
(identical(other.selectedStreamingContainerIndex,
|
||||
selectedStreamingContainerIndex) ||
|
||||
other.selectedStreamingContainerIndex ==
|
||||
selectedStreamingContainerIndex) &&
|
||||
(identical(other.selectedDownloadingQualityIndex,
|
||||
selectedDownloadingQualityIndex) ||
|
||||
other.selectedDownloadingQualityIndex ==
|
||||
selectedDownloadingQualityIndex) &&
|
||||
(identical(other.selectedDownloadingContainerIndex,
|
||||
selectedDownloadingContainerIndex) ||
|
||||
other.selectedDownloadingContainerIndex ==
|
||||
selectedDownloadingContainerIndex));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(_presets),
|
||||
selectedStreamingQualityIndex,
|
||||
selectedStreamingContainerIndex,
|
||||
selectedDownloadingQualityIndex,
|
||||
selectedDownloadingContainerIndex);
|
||||
|
||||
/// Create a copy of AudioSourcePresetsState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl>
|
||||
get copyWith => __$$AudioSourcePresetsStateImplCopyWithImpl<
|
||||
_$AudioSourcePresetsStateImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$AudioSourcePresetsStateImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _AudioSourcePresetsState implements AudioSourcePresetsState {
|
||||
factory _AudioSourcePresetsState(
|
||||
{final List<SpotubeAudioSourceContainerPreset> presets,
|
||||
final int selectedStreamingQualityIndex,
|
||||
final int selectedStreamingContainerIndex,
|
||||
final int selectedDownloadingQualityIndex,
|
||||
final int selectedDownloadingContainerIndex}) =
|
||||
_$AudioSourcePresetsStateImpl;
|
||||
|
||||
factory _AudioSourcePresetsState.fromJson(Map<String, dynamic> json) =
|
||||
_$AudioSourcePresetsStateImpl.fromJson;
|
||||
|
||||
@override
|
||||
List<SpotubeAudioSourceContainerPreset> get presets;
|
||||
@override
|
||||
int get selectedStreamingQualityIndex;
|
||||
@override
|
||||
int get selectedStreamingContainerIndex;
|
||||
@override
|
||||
int get selectedDownloadingQualityIndex;
|
||||
@override
|
||||
int get selectedDownloadingContainerIndex;
|
||||
|
||||
/// Create a copy of AudioSourcePresetsState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'quality_presets.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$AudioSourcePresetsStateImpl _$$AudioSourcePresetsStateImplFromJson(
|
||||
Map json) =>
|
||||
_$AudioSourcePresetsStateImpl(
|
||||
presets: (json['presets'] as List<dynamic>?)
|
||||
?.map((e) => SpotubeAudioSourceContainerPreset.fromJson(
|
||||
Map<String, dynamic>.from(e as Map)))
|
||||
.toList() ??
|
||||
const [],
|
||||
selectedStreamingQualityIndex:
|
||||
(json['selectedStreamingQualityIndex'] as num?)?.toInt() ?? 0,
|
||||
selectedStreamingContainerIndex:
|
||||
(json['selectedStreamingContainerIndex'] as num?)?.toInt() ?? 0,
|
||||
selectedDownloadingQualityIndex:
|
||||
(json['selectedDownloadingQualityIndex'] as num?)?.toInt() ?? 0,
|
||||
selectedDownloadingContainerIndex:
|
||||
(json['selectedDownloadingContainerIndex'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$AudioSourcePresetsStateImplToJson(
|
||||
_$AudioSourcePresetsStateImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'presets': instance.presets.map((e) => e.toJson()).toList(),
|
||||
'selectedStreamingQualityIndex': instance.selectedStreamingQualityIndex,
|
||||
'selectedStreamingContainerIndex':
|
||||
instance.selectedStreamingContainerIndex,
|
||||
'selectedDownloadingQualityIndex':
|
||||
instance.selectedDownloadingQualityIndex,
|
||||
'selectedDownloadingContainerIndex':
|
||||
instance.selectedDownloadingContainerIndex,
|
||||
};
|
||||
@ -8,7 +8,7 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
||||
@override
|
||||
FutureOr<bool> build() async {
|
||||
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
||||
if (defaultPluginConfig.asData?.value.defaultMetadataPluginConfig?.abilities
|
||||
if (defaultPluginConfig.asData?.value.defaultPluginConfig?.abilities
|
||||
.contains(PluginAbilities.authentication) !=
|
||||
true) {
|
||||
return false;
|
||||
@ -35,36 +35,3 @@ final metadataPluginAuthenticatedProvider =
|
||||
AsyncNotifierProvider<MetadataPluginAuthenticatedNotifier, bool>(
|
||||
MetadataPluginAuthenticatedNotifier.new,
|
||||
);
|
||||
|
||||
class AudioSourcePluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
||||
@override
|
||||
FutureOr<bool> build() async {
|
||||
final defaultPluginConfig = ref.watch(metadataPluginsProvider);
|
||||
if (defaultPluginConfig
|
||||
.asData?.value.defaultAudioSourcePluginConfig?.abilities
|
||||
.contains(PluginAbilities.authentication) !=
|
||||
true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final defaultPlugin = await ref.watch(audioSourcePluginProvider.future);
|
||||
if (defaultPlugin == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final sub = defaultPlugin.auth.authStateStream.listen((event) {
|
||||
state = AsyncData(defaultPlugin.auth.isAuthenticated());
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
sub.cancel();
|
||||
});
|
||||
|
||||
return defaultPlugin.auth.isAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
final audioSourcePluginAuthenticatedProvider =
|
||||
AsyncNotifierProvider<AudioSourcePluginAuthenticatedNotifier, bool>(
|
||||
AudioSourcePluginAuthenticatedNotifier.new,
|
||||
);
|
||||
|
||||
@ -49,7 +49,6 @@ class MetadataPluginRepositoriesNotifier
|
||||
owner: repo["owner"]["login"] ?? "",
|
||||
description: repo["description"] ?? "",
|
||||
repoUrl: repo["html_url"] ?? "",
|
||||
topics: repo["topics"].cast<String>() ?? [],
|
||||
);
|
||||
}).toList();
|
||||
|
||||
|
||||
@ -10,10 +10,8 @@ class MetadataPluginScrobbleNotifier
|
||||
@override
|
||||
build() {
|
||||
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||
final pluginConfig = ref
|
||||
.watch(metadataPluginsProvider)
|
||||
.valueOrNull
|
||||
?.defaultMetadataPluginConfig;
|
||||
final pluginConfig =
|
||||
ref.watch(metadataPluginsProvider).valueOrNull?.defaultPluginConfig;
|
||||
|
||||
if (metadataPlugin.valueOrNull == null ||
|
||||
pluginConfig == null ||
|
||||
|
||||
@ -9,13 +9,3 @@ final metadataPluginSupportTextProvider = FutureProvider<String>((ref) async {
|
||||
}
|
||||
return await metadataPlugin.core.support;
|
||||
});
|
||||
|
||||
final audioSourcePluginSupportTextProvider =
|
||||
FutureProvider<String>((ref) async {
|
||||
final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future);
|
||||
|
||||
if (audioSourcePlugin == null) {
|
||||
throw 'No metadata plugin available';
|
||||
}
|
||||
return await audioSourcePlugin.core.support;
|
||||
});
|
||||
|
||||
@ -4,14 +4,12 @@ import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||
import 'package:spotube/services/dio/dio.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
@ -26,28 +24,18 @@ final allowedDomainsRegex = RegExp(
|
||||
|
||||
class MetadataPluginState {
|
||||
final List<PluginConfiguration> plugins;
|
||||
final int defaultMetadataPlugin;
|
||||
final int defaultAudioSourcePlugin;
|
||||
final int defaultPlugin;
|
||||
|
||||
const MetadataPluginState({
|
||||
this.plugins = const [],
|
||||
this.defaultMetadataPlugin = -1,
|
||||
this.defaultAudioSourcePlugin = -1,
|
||||
this.defaultPlugin = -1,
|
||||
});
|
||||
|
||||
PluginConfiguration? get defaultMetadataPluginConfig {
|
||||
if (defaultMetadataPlugin < 0 || defaultMetadataPlugin >= plugins.length) {
|
||||
PluginConfiguration? get defaultPluginConfig {
|
||||
if (defaultPlugin < 0 || defaultPlugin >= plugins.length) {
|
||||
return null;
|
||||
}
|
||||
return plugins[defaultMetadataPlugin];
|
||||
}
|
||||
|
||||
PluginConfiguration? get defaultAudioSourcePluginConfig {
|
||||
if (defaultAudioSourcePlugin < 0 ||
|
||||
defaultAudioSourcePlugin >= plugins.length) {
|
||||
return null;
|
||||
}
|
||||
return plugins[defaultAudioSourcePlugin];
|
||||
return plugins[defaultPlugin];
|
||||
}
|
||||
|
||||
factory MetadataPluginState.fromJson(Map<String, dynamic> json) {
|
||||
@ -55,30 +43,24 @@ class MetadataPluginState {
|
||||
plugins: (json["plugins"] as List<dynamic>)
|
||||
.map((e) => PluginConfiguration.fromJson(e))
|
||||
.toList(),
|
||||
defaultMetadataPlugin: json["default_metadata_plugin"] ?? -1,
|
||||
defaultAudioSourcePlugin: json['default_audio_source_plugin'],
|
||||
defaultPlugin: json["default_plugin"] ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"plugins": plugins.map((e) => e.toJson()).toList(),
|
||||
"default_metadata_plugin": defaultMetadataPlugin,
|
||||
"default_audio_source_plugin": defaultAudioSourcePlugin
|
||||
"default_plugin": defaultPlugin,
|
||||
};
|
||||
}
|
||||
|
||||
MetadataPluginState copyWith({
|
||||
List<PluginConfiguration>? plugins,
|
||||
int? defaultMetadataPlugin,
|
||||
int? defaultAudioSourcePlugin,
|
||||
int? defaultPlugin,
|
||||
}) {
|
||||
return MetadataPluginState(
|
||||
plugins: plugins ?? this.plugins,
|
||||
defaultMetadataPlugin:
|
||||
defaultMetadataPlugin ?? this.defaultMetadataPlugin,
|
||||
defaultAudioSourcePlugin:
|
||||
defaultAudioSourcePlugin ?? this.defaultAudioSourcePlugin,
|
||||
defaultPlugin: defaultPlugin ?? this.defaultPlugin,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -90,7 +72,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
build() async {
|
||||
final database = ref.watch(databaseProvider);
|
||||
|
||||
final subscription = database.pluginsTable.select().watch().listen(
|
||||
final subscription = database.metadataPluginsTable.select().watch().listen(
|
||||
(event) async {
|
||||
state = AsyncValue.data(await toStatePlugins(event));
|
||||
},
|
||||
@ -100,26 +82,22 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
final plugins = await database.pluginsTable.select().get();
|
||||
final plugins = await database.metadataPluginsTable.select().get();
|
||||
|
||||
final pluginState = await toStatePlugins(plugins);
|
||||
|
||||
await _loadDefaultPlugins(pluginState);
|
||||
|
||||
return pluginState;
|
||||
return await toStatePlugins(plugins);
|
||||
}
|
||||
|
||||
Future<MetadataPluginState> toStatePlugins(
|
||||
List<PluginsTableData> plugins,
|
||||
List<MetadataPluginsTableData> plugins,
|
||||
) async {
|
||||
int defaultMetadataPlugin = -1;
|
||||
int defaultAudioSourcePlugin = -1;
|
||||
int defaultPlugin = -1;
|
||||
final pluginConfigs = <PluginConfiguration>[];
|
||||
|
||||
for (int i = 0; i < plugins.length; i++) {
|
||||
final plugin = plugins[i];
|
||||
|
||||
final pluginConfig = PluginConfiguration(
|
||||
type: PluginType.metadata,
|
||||
name: plugin.name,
|
||||
author: plugin.author,
|
||||
description: plugin.description,
|
||||
@ -155,66 +133,23 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
!await pluginJsonFile.exists() ||
|
||||
!await pluginBinaryFile.exists()) {
|
||||
// Delete the plugin entry from DB if the plugin files are not there.
|
||||
await database.pluginsTable.deleteOne(plugin);
|
||||
await database.metadataPluginsTable.deleteOne(plugin);
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginConfigs.add(pluginConfig);
|
||||
|
||||
if (plugin.selectedForMetadata) {
|
||||
defaultMetadataPlugin = pluginConfigs.length - 1;
|
||||
}
|
||||
if (plugin.selectedForAudioSource) {
|
||||
defaultAudioSourcePlugin = pluginConfigs.length - 1;
|
||||
if (plugin.selected) {
|
||||
defaultPlugin = pluginConfigs.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return MetadataPluginState(
|
||||
plugins: pluginConfigs,
|
||||
defaultMetadataPlugin: defaultMetadataPlugin,
|
||||
defaultAudioSourcePlugin: defaultAudioSourcePlugin,
|
||||
defaultPlugin: defaultPlugin,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadDefaultPlugins(MetadataPluginState pluginState) async {
|
||||
const plugins = [
|
||||
"spotube-plugin-musicbrainz-listenbrainz",
|
||||
"spotube-plugin-youtube-audio",
|
||||
];
|
||||
|
||||
for (final plugin in plugins) {
|
||||
final byteData = await rootBundle.load(
|
||||
"assets/plugins/$plugin/plugin.smplug",
|
||||
);
|
||||
final pluginConfig =
|
||||
await extractPluginArchive(byteData.buffer.asUint8List());
|
||||
try {
|
||||
await addPlugin(pluginConfig);
|
||||
} on MetadataPluginException catch (e) {
|
||||
if (e.errorCode == MetadataPluginErrorCode.duplicatePlugin &&
|
||||
await isPluginUpdate(pluginConfig)) {
|
||||
final oldConfig = pluginState.plugins
|
||||
.firstWhereOrNull((p) => p.slug == pluginConfig.slug);
|
||||
if (oldConfig == null) continue;
|
||||
final isDefaultMetadata =
|
||||
oldConfig == pluginState.defaultMetadataPluginConfig;
|
||||
final isDefaultAudioSource =
|
||||
oldConfig == pluginState.defaultAudioSourcePluginConfig;
|
||||
|
||||
await removePlugin(pluginConfig);
|
||||
await addPlugin(pluginConfig);
|
||||
|
||||
if (isDefaultMetadata) {
|
||||
await setDefaultMetadataPlugin(pluginConfig);
|
||||
}
|
||||
if (isDefaultAudioSource) {
|
||||
await setDefaultAudioSourcePlugin(pluginConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uri _getGithubReleasesUrl(String repoUrl) {
|
||||
final parsedUri = Uri.parse(repoUrl);
|
||||
final uri = parsedUri.replace(
|
||||
@ -392,7 +327,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
Future<void> addPlugin(PluginConfiguration plugin) async {
|
||||
_assertPluginApiCompatibility(plugin);
|
||||
|
||||
final pluginRes = await (database.pluginsTable.select()
|
||||
final pluginRes = await (database.metadataPluginsTable.select()
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author),
|
||||
@ -404,8 +339,8 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
throw MetadataPluginException.duplicatePlugin();
|
||||
}
|
||||
|
||||
await database.pluginsTable.insertOne(
|
||||
PluginsTableCompanion.insert(
|
||||
await database.metadataPluginsTable.insertOne(
|
||||
MetadataPluginsTableCompanion.insert(
|
||||
name: plugin.name,
|
||||
author: plugin.author,
|
||||
description: plugin.description,
|
||||
@ -416,22 +351,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
pluginApiVersion: Value(plugin.pluginApiVersion),
|
||||
repository: Value(plugin.repository),
|
||||
// Setting the very first plugin as the default plugin
|
||||
selectedForMetadata: Value(
|
||||
(state.valueOrNull?.plugins
|
||||
.where(
|
||||
(d) => d.abilities.contains(PluginAbilities.metadata))
|
||||
.isEmpty ??
|
||||
true) &&
|
||||
plugin.abilities.contains(PluginAbilities.metadata),
|
||||
),
|
||||
selectedForAudioSource: Value(
|
||||
(state.valueOrNull?.plugins
|
||||
.where((d) =>
|
||||
d.abilities.contains(PluginAbilities.audioSource))
|
||||
.isEmpty ??
|
||||
true) &&
|
||||
plugin.abilities.contains(PluginAbilities.audioSource),
|
||||
),
|
||||
selected: Value(state.valueOrNull?.plugins.isEmpty ?? true),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -442,65 +362,26 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
if (pluginExtractionDir.existsSync()) {
|
||||
await pluginExtractionDir.delete(recursive: true);
|
||||
}
|
||||
await database.pluginsTable.deleteWhere((tbl) =>
|
||||
await database.metadataPluginsTable.deleteWhere((tbl) =>
|
||||
tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author));
|
||||
|
||||
// Same here, if the removed plugin is the default plugin
|
||||
// set the first available plugin as the default plugin
|
||||
// only when there is 1 remaining plugin
|
||||
if (state.valueOrNull?.defaultMetadataPluginConfig == plugin) {
|
||||
final remainingPlugins = state.valueOrNull?.plugins.where(
|
||||
(p) =>
|
||||
p != plugin && p.abilities.contains(PluginAbilities.metadata),
|
||||
) ??
|
||||
[];
|
||||
if (state.valueOrNull?.defaultPluginConfig == plugin) {
|
||||
final remainingPlugins =
|
||||
state.valueOrNull?.plugins.where((p) => p != plugin) ?? [];
|
||||
if (remainingPlugins.length == 1) {
|
||||
await setDefaultMetadataPlugin(remainingPlugins.first);
|
||||
await setDefaultPlugin(remainingPlugins.first);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.valueOrNull?.defaultAudioSourcePluginConfig == plugin) {
|
||||
final remainingPlugins = state.valueOrNull?.plugins.where(
|
||||
(p) =>
|
||||
p != plugin &&
|
||||
p.abilities.contains(PluginAbilities.audioSource),
|
||||
) ??
|
||||
[];
|
||||
if (remainingPlugins.length == 1) {
|
||||
await setDefaultAudioSourcePlugin(remainingPlugins.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isPluginUpdate(PluginConfiguration newPlugin) async {
|
||||
final pluginRes = await (database.pluginsTable.select()
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.name.equals(newPlugin.name) &
|
||||
tbl.author.equals(newPlugin.author),
|
||||
)
|
||||
..limit(1))
|
||||
.get();
|
||||
|
||||
if (pluginRes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final oldPlugin = pluginRes.first;
|
||||
final oldPluginApiVersion = Version.parse(oldPlugin.pluginApiVersion);
|
||||
final newPluginApiVersion = Version.parse(newPlugin.pluginApiVersion);
|
||||
|
||||
return newPluginApiVersion > oldPluginApiVersion;
|
||||
}
|
||||
|
||||
Future<void> updatePlugin(
|
||||
PluginConfiguration plugin,
|
||||
PluginUpdateAvailable update,
|
||||
) async {
|
||||
final isDefaultMetadata =
|
||||
plugin == state.valueOrNull?.defaultMetadataPluginConfig;
|
||||
final isDefaultAudioSource =
|
||||
plugin == state.valueOrNull?.defaultAudioSourcePluginConfig;
|
||||
final isDefault = plugin == state.valueOrNull?.defaultPluginConfig;
|
||||
final pluginUpdatedConfig =
|
||||
await downloadAndCachePlugin(update.downloadUrl);
|
||||
|
||||
@ -513,46 +394,21 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||
await removePlugin(plugin);
|
||||
await addPlugin(pluginUpdatedConfig);
|
||||
|
||||
if (isDefaultMetadata) {
|
||||
await setDefaultMetadataPlugin(pluginUpdatedConfig);
|
||||
}
|
||||
if (isDefaultAudioSource) {
|
||||
await setDefaultAudioSourcePlugin(pluginUpdatedConfig);
|
||||
if (isDefault) {
|
||||
await setDefaultPlugin(pluginUpdatedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setDefaultMetadataPlugin(PluginConfiguration plugin) async {
|
||||
assert(
|
||||
plugin.abilities.contains(PluginAbilities.metadata),
|
||||
"Must be a metadata plugin",
|
||||
);
|
||||
|
||||
await database.pluginsTable
|
||||
Future<void> setDefaultPlugin(PluginConfiguration plugin) async {
|
||||
await database.metadataPluginsTable
|
||||
.update()
|
||||
.write(const PluginsTableCompanion(selectedForMetadata: Value(false)));
|
||||
.write(const MetadataPluginsTableCompanion(selected: Value(false)));
|
||||
|
||||
await (database.pluginsTable.update()
|
||||
await (database.metadataPluginsTable.update()
|
||||
..where((tbl) =>
|
||||
tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author)))
|
||||
.write(
|
||||
const PluginsTableCompanion(selectedForMetadata: Value(true)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setDefaultAudioSourcePlugin(PluginConfiguration plugin) async {
|
||||
assert(
|
||||
plugin.abilities.contains(PluginAbilities.audioSource),
|
||||
"Must be an audio-source plugin",
|
||||
);
|
||||
|
||||
await database.pluginsTable.update().write(
|
||||
const PluginsTableCompanion(selectedForAudioSource: Value(false)));
|
||||
|
||||
await (database.pluginsTable.update()
|
||||
..where((tbl) =>
|
||||
tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author)))
|
||||
.write(
|
||||
const PluginsTableCompanion(selectedForAudioSource: Value(true)),
|
||||
const MetadataPluginsTableCompanion(selected: Value(true)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -589,10 +445,8 @@ final metadataPluginsProvider =
|
||||
final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
(ref) async {
|
||||
final defaultPlugin = await ref.watch(
|
||||
metadataPluginsProvider
|
||||
.selectAsync((data) => data.defaultMetadataPluginConfig),
|
||||
metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig),
|
||||
);
|
||||
final youtubeEngine = ref.read(youtubeEngineProvider);
|
||||
|
||||
if (defaultPlugin == null) {
|
||||
return null;
|
||||
@ -602,34 +456,6 @@ final metadataPluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
final pluginByteCode =
|
||||
await pluginsNotifier.getPluginByteCode(defaultPlugin);
|
||||
|
||||
return await MetadataPlugin.create(
|
||||
youtubeEngine,
|
||||
defaultPlugin,
|
||||
pluginByteCode,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final audioSourcePluginProvider = FutureProvider<MetadataPlugin?>(
|
||||
(ref) async {
|
||||
final defaultPlugin = await ref.watch(
|
||||
metadataPluginsProvider
|
||||
.selectAsync((data) => data.defaultAudioSourcePluginConfig),
|
||||
);
|
||||
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
||||
|
||||
if (defaultPlugin == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||
final pluginByteCode =
|
||||
await pluginsNotifier.getPluginByteCode(defaultPlugin);
|
||||
|
||||
return await MetadataPlugin.create(
|
||||
youtubeEngine,
|
||||
defaultPlugin,
|
||||
pluginByteCode,
|
||||
);
|
||||
return await MetadataPlugin.create(defaultPlugin, pluginByteCode);
|
||||
},
|
||||
);
|
||||
|
||||
@ -8,25 +8,10 @@ final metadataPluginUpdateCheckerProvider =
|
||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (metadataPlugin == null ||
|
||||
metadataPluginConfigs.defaultMetadataPluginConfig == null) {
|
||||
metadataPluginConfigs.defaultPluginConfig == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return metadataPlugin.core
|
||||
.checkUpdate(metadataPluginConfigs.defaultMetadataPluginConfig!);
|
||||
});
|
||||
|
||||
final audioSourcePluginUpdateCheckerProvider =
|
||||
FutureProvider<PluginUpdateAvailable?>((ref) async {
|
||||
final audioSourcePluginConfigs =
|
||||
await ref.watch(metadataPluginsProvider.future);
|
||||
final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future);
|
||||
|
||||
if (audioSourcePlugin == null ||
|
||||
audioSourcePluginConfigs.defaultAudioSourcePluginConfig == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return audioSourcePlugin.core
|
||||
.checkUpdate(audioSourcePluginConfigs.defaultAudioSourcePluginConfig!);
|
||||
.checkUpdate(metadataPluginConfigs.defaultPluginConfig!);
|
||||
});
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/server/sourced_track_provider.dart';
|
||||
import 'package:spotube/provider/server/track_sources.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
|
||||
final activeTrackSourcesProvider = FutureProvider<
|
||||
({
|
||||
SourcedTrack? source,
|
||||
SourcedTrackNotifier? notifier,
|
||||
TrackSourcesNotifier? notifier,
|
||||
SpotubeTrackObject track,
|
||||
})?>((ref) async {
|
||||
final audioPlayerState = ref.watch(audioPlayerProvider);
|
||||
@ -24,15 +25,13 @@ final activeTrackSourcesProvider = FutureProvider<
|
||||
);
|
||||
}
|
||||
|
||||
final sourcedTrack = await ref.watch(
|
||||
sourcedTrackProvider(
|
||||
final trackQuery = TrackSourceQuery.fromTrack(
|
||||
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
||||
).future,
|
||||
);
|
||||
|
||||
final sourcedTrack = await ref.watch(trackSourcesProvider(trackQuery).future);
|
||||
final sourcedTrackNotifier = ref.watch(
|
||||
sourcedTrackProvider(
|
||||
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
||||
).notifier,
|
||||
trackSourcesProvider(trackQuery).notifier,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -11,14 +11,16 @@ import 'package:path/path.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/models/parser/range_headers.dart';
|
||||
import 'package:spotube/models/playback/track_sources.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/audio_player/state.dart';
|
||||
|
||||
import 'package:spotube/provider/server/active_track_sources.dart';
|
||||
import 'package:spotube/provider/server/sourced_track_provider.dart';
|
||||
import 'package:spotube/provider/server/track_sources.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.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';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
@ -48,30 +50,26 @@ class ServerPlaybackRoutes {
|
||||
return join(
|
||||
await UserPreferencesNotifier.getMusicCacheDir(),
|
||||
ServiceUtils.sanitizeFilename(
|
||||
'${track.query.name} - ${track.query.artists.map((d) => d.name).join(",")} (${track.info.id}).${track.qualityPreset!.getFileExtension()}',
|
||||
'${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SourcedTrack?> _getSourcedTrack(
|
||||
Request request,
|
||||
String trackId,
|
||||
) async {
|
||||
Request request, String trackId) async {
|
||||
final track =
|
||||
playlist.tracks.firstWhere((element) => element.id == trackId);
|
||||
|
||||
final activeSourcedTrack =
|
||||
await ref.read(activeTrackSourcesProvider.future);
|
||||
|
||||
final media = audioPlayer.playlist.medias
|
||||
.firstWhere((e) => e.uri == request.requestedUri.toString());
|
||||
final spotubeMedia =
|
||||
media is SpotubeMedia ? media : SpotubeMedia.media(media);
|
||||
final sourcedTrack = activeSourcedTrack?.track.id == track.id
|
||||
? activeSourcedTrack?.source
|
||||
: await ref.read(
|
||||
sourcedTrackProvider(spotubeMedia.track as SpotubeFullTrackObject)
|
||||
.future,
|
||||
trackSourcesProvider(
|
||||
//! Use [Request.requestedUri] as it contains full https url.
|
||||
//! [Request.url] will exclude and starts relatively. (streams/<trackId>... basically)
|
||||
TrackSourceQuery.parseUri(request.requestedUri.toString()),
|
||||
).future,
|
||||
);
|
||||
|
||||
return sourcedTrack;
|
||||
@ -82,7 +80,7 @@ class ServerPlaybackRoutes {
|
||||
SourcedTrack track,
|
||||
) async {
|
||||
AppLogger.log.i(
|
||||
"HEAD request for track: ${track.query.name}\n"
|
||||
"HEAD request for track: ${track.query.title}\n"
|
||||
"Headers: ${request.headers}",
|
||||
);
|
||||
|
||||
@ -94,7 +92,7 @@ class ServerPlaybackRoutes {
|
||||
return dio_lib.Response(
|
||||
statusCode: 200,
|
||||
headers: Headers.fromMap({
|
||||
"content-type": ["audio/${track.qualityPreset!.name}"],
|
||||
"content-type": ["audio/${track.codec.name}"],
|
||||
"content-length": ["$fileLength"],
|
||||
"accept-ranges": ["bytes"],
|
||||
"content-range": ["bytes 0-$fileLength/$fileLength"],
|
||||
@ -105,7 +103,7 @@ class ServerPlaybackRoutes {
|
||||
|
||||
String url = track.url ??
|
||||
await ref
|
||||
.read(sourcedTrackProvider(track.query).notifier)
|
||||
.read(trackSourcesProvider(track.query).notifier)
|
||||
.swapWithNextSibling()
|
||||
.then((track) => track.url!);
|
||||
|
||||
@ -131,7 +129,7 @@ class ServerPlaybackRoutes {
|
||||
Map<String, dynamic> headers,
|
||||
) async {
|
||||
AppLogger.log.i(
|
||||
"GET request for track: ${track.query.name}\n"
|
||||
"GET request for track: ${track.query.title}\n"
|
||||
"Headers: ${request.headers}",
|
||||
);
|
||||
|
||||
@ -145,7 +143,7 @@ class ServerPlaybackRoutes {
|
||||
response: dio_lib.Response<Uint8List>(
|
||||
statusCode: 200,
|
||||
headers: Headers.fromMap({
|
||||
"content-type": ["audio/${track.qualityPreset!.name}"],
|
||||
"content-type": ["audio/${track.codec.name}"],
|
||||
"content-length": ["$cachedFileLength"],
|
||||
"accept-ranges": ["bytes"],
|
||||
"content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"],
|
||||
@ -160,7 +158,7 @@ class ServerPlaybackRoutes {
|
||||
|
||||
String url = track.url ??
|
||||
await ref
|
||||
.read(sourcedTrackProvider(track.query).notifier)
|
||||
.read(trackSourcesProvider(track.query).notifier)
|
||||
.swapWithNextSibling()
|
||||
.then((track) => track.url!);
|
||||
|
||||
@ -182,7 +180,7 @@ class ServerPlaybackRoutes {
|
||||
AppLogger.reportError(e, stack);
|
||||
|
||||
final sourcedTrack = await ref
|
||||
.read(sourcedTrackProvider(track.query).notifier)
|
||||
.read(trackSourcesProvider(track.query).notifier)
|
||||
.refreshStreamingUrl();
|
||||
|
||||
url = sourcedTrack.url!;
|
||||
@ -208,9 +206,11 @@ class ServerPlaybackRoutes {
|
||||
);
|
||||
}
|
||||
|
||||
if (headers["range"] == "bytes=0-" &&
|
||||
track.qualityPreset is SpotubeAudioSourceContainerPresetLossless) {
|
||||
const bufferSize = 6 * 1024 * 1024; // 6MB for lossless
|
||||
if (headers["range"] == "bytes=0-" && track.codec == SourceCodecs.flac) {
|
||||
final bufferSize =
|
||||
userPreferences.audioQuality == SourceQualities.uncompressed
|
||||
? 6 * 1024 * 1024 // 6MB for lossless
|
||||
: 4 * 1024 * 1024; // 4MB for lossy
|
||||
|
||||
final endRange = min(
|
||||
bufferSize,
|
||||
@ -228,7 +228,7 @@ class ServerPlaybackRoutes {
|
||||
final res = await dio.get<Uint8List>(url, options: options);
|
||||
|
||||
AppLogger.log.i(
|
||||
"Response for track: ${track.query.name}\n"
|
||||
"Response for track: ${track.query.title}\n"
|
||||
"Status Code: ${res.statusCode}\n"
|
||||
"Headers: ${res.headers.map}",
|
||||
);
|
||||
@ -262,8 +262,7 @@ class ServerPlaybackRoutes {
|
||||
await trackPartialCacheFile.rename(trackCacheFile.path);
|
||||
}
|
||||
|
||||
if (contentRange.total == fileLength &&
|
||||
track.qualityPreset!.getFileExtension() != "weba") {
|
||||
if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) {
|
||||
final playlistTrack = playlist.tracks.firstWhereOrNull(
|
||||
(element) => element.id == track.query.id,
|
||||
);
|
||||
@ -287,9 +286,7 @@ class ServerPlaybackRoutes {
|
||||
imageBytes: imageBytes,
|
||||
fileLength: fileLength,
|
||||
),
|
||||
).catchError((e, stackTrace) {
|
||||
AppLogger.reportError(e, stackTrace);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
return (bytes: bytes, response: res);
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
|
||||
class SourcedTrackNotifier
|
||||
extends FamilyAsyncNotifier<SourcedTrack, SpotubeFullTrackObject> {
|
||||
@override
|
||||
FutureOr<SourcedTrack> build(query) {
|
||||
ref.watch(audioSourcePluginProvider);
|
||||
ref.watch(audioSourcePresetsProvider);
|
||||
|
||||
return SourcedTrack.fetchFromTrack(query: query, ref: ref);
|
||||
}
|
||||
|
||||
Future<SourcedTrack> refreshStreamingUrl() async {
|
||||
return await update((prev) async {
|
||||
return await prev.refreshStream();
|
||||
});
|
||||
}
|
||||
|
||||
Future<SourcedTrack> copyWithSibling() async {
|
||||
return await update((prev) async {
|
||||
return prev.copyWithSibling();
|
||||
});
|
||||
}
|
||||
|
||||
Future<SourcedTrack> swapWithSibling(
|
||||
SpotubeAudioSourceMatchObject sibling,
|
||||
) async {
|
||||
return await update((prev) async {
|
||||
return await prev.swapWithSibling(sibling) ?? prev;
|
||||
});
|
||||
}
|
||||
|
||||
Future<SourcedTrack> swapWithNextSibling() async {
|
||||
return await update((prev) async {
|
||||
return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final sourcedTrackProvider = AsyncNotifierProviderFamily<SourcedTrackNotifier,
|
||||
SourcedTrack, SpotubeFullTrackObject>(
|
||||
() => SourcedTrackNotifier(),
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user