Compare commits

...

12 Commits

Author SHA1 Message Date
dependabot[bot]
87178c6cd2
Merge 7571f880ec into 66848c78c7 2025-09-20 10:15:30 +07:00
Kingkor Roy Tirtho
66848c78c7 chore: add dab music option on getting started page and audio source quality label 2025-09-19 23:55:38 +06:00
Kingkor Roy Tirtho
3e34bc4be6 chore: streaming issue for mp3 2025-09-19 21:40:26 +06:00
Kingkor Roy Tirtho
cecb687592 feat(playback): add uncompressed flac playback support 2025-09-19 11:53:36 +06:00
Kingkor Roy Tirtho
e8a54d3209 feat(playback): add dab music source 2025-09-19 10:31:49 +06:00
Kingkor Roy Tirtho
ca6924f5a9 feat: show plugin source and set the only plugin as default if no plugin is there 2025-09-18 23:28:56 +06:00
Kingkor Roy Tirtho
0e48b7a337 website: update logo 2025-09-12 00:38:35 +06:00
Kingkor Roy Tirtho
60fbf66639 website: use locked version of astro-pagefine 2025-09-12 00:26:56 +06:00
Kingkor Roy Tirtho
97370712bc website: add back download buttons 2025-09-12 00:19:00 +06:00
Kingkor Roy Tirtho
c36e819ba3 Merge branch 'dev' into website 2025-09-12 00:11:03 +06:00
Kingkor Roy Tirtho
61d34963fa website: ads not showing up 2025-08-15 21:39:05 +06:00
dependabot[bot]
7571f880ec
chore(deps): bump KSXGitHub/github-actions-deploy-aur
Bumps [KSXGitHub/github-actions-deploy-aur](https://github.com/ksxgithub/github-actions-deploy-aur) from 2.7.1 to 2.7.2.
- [Release notes](https://github.com/ksxgithub/github-actions-deploy-aur/releases)
- [Commits](https://github.com/ksxgithub/github-actions-deploy-aur/compare/v2.7.1...v2.7.2)

---
updated-dependencies:
- dependency-name: KSXGitHub/github-actions-deploy-aur
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-03 00:00:09 +00:00
85 changed files with 2000 additions and 1200 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

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

View File

@ -1,3 +1,5 @@
// dart format width=80
/// GENERATED CODE - DO NOT MODIFY BY HAND /// GENERATED CODE - DO NOT MODIFY BY HAND
/// ***************************************************** /// *****************************************************
/// FlutterGen /// FlutterGen
@ -5,7 +7,7 @@
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use // ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -67,6 +69,10 @@ class $AssetsImagesGen {
class $AssetsImagesLogosGen { class $AssetsImagesLogosGen {
const $AssetsImagesLogosGen(); const $AssetsImagesLogosGen();
/// File path: assets/images/logos/dab-music.png
AssetGenImage get dabMusic =>
const AssetGenImage('assets/images/logos/dab-music.png');
/// File path: assets/images/logos/invidious.jpg /// File path: assets/images/logos/invidious.jpg
AssetGenImage get invidious => AssetGenImage get invidious =>
const AssetGenImage('assets/images/logos/invidious.jpg'); const AssetGenImage('assets/images/logos/invidious.jpg');
@ -80,11 +86,12 @@ class $AssetsImagesLogosGen {
const AssetGenImage('assets/images/logos/songlink-transparent.png'); const AssetGenImage('assets/images/logos/songlink-transparent.png');
/// List of all assets /// List of all assets
List<AssetGenImage> get values => [invidious, jiosaavn, songlinkTransparent]; List<AssetGenImage> get values =>
[dabMusic, invidious, jiosaavn, songlinkTransparent];
} }
class Assets { class Assets {
Assets._(); const Assets._();
static const String license = 'LICENSE'; static const String license = 'LICENSE';
static const $AssetsBrandingGen branding = $AssetsBrandingGen(); static const $AssetsBrandingGen branding = $AssetsBrandingGen();
@ -99,12 +106,14 @@ class AssetGenImage {
this._assetName, { this._assetName, {
this.size, this.size,
this.flavors = const {}, this.flavors = const {},
this.animation,
}); });
final String _assetName; final String _assetName;
final Size? size; final Size? size;
final Set<String> flavors; final Set<String> flavors;
final AssetGenImageAnimation? animation;
Image image({ Image image({
Key? key, Key? key,
@ -127,7 +136,7 @@ class AssetGenImage {
bool gaplessPlayback = true, bool gaplessPlayback = true,
bool isAntiAlias = false, bool isAntiAlias = false,
String? package, String? package,
FilterQuality filterQuality = FilterQuality.low, FilterQuality filterQuality = FilterQuality.medium,
int? cacheWidth, int? cacheWidth,
int? cacheHeight, int? cacheHeight,
}) { }) {
@ -174,3 +183,15 @@ class AssetGenImage {
String get keyName => _assetName; String get keyName => _assetName;
} }
class AssetGenImageAnimation {
const AssetGenImageAnimation({
required this.isAnimation,
required this.duration,
required this.frames,
});
final bool isAnimation;
final Duration duration;
final int frames;
}

View File

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

View File

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

View File

@ -80,6 +80,7 @@ abstract class SpotubeIcons {
static const hoverOff = Icons.back_hand_outlined; static const hoverOff = Icons.back_hand_outlined;
static const dragHandle = Icons.drag_indicator; static const dragHandle = Icons.drag_indicator;
static const lightning = Icons.flash_on_rounded; static const lightning = Icons.flash_on_rounded;
static const lightningOutlined = FeatherIcons.zap;
static const colorSync = FeatherIcons.activity; static const colorSync = FeatherIcons.activity;
static const language = FeatherIcons.globe; static const language = FeatherIcons.globe;
static const error = FeatherIcons.alertTriangle; static const error = FeatherIcons.alertTriangle;

View File

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

View File

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

View File

@ -461,5 +461,8 @@
"available_plugins": "Available plugins", "available_plugins": "Available plugins",
"configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider", "configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider",
"audio_scrobblers": "Audio Scrobblers", "audio_scrobblers": "Audio Scrobblers",
"scrobbling": "Scrobbling" "scrobbling": "Scrobbling",
"source": "Source: ",
"uncompressed": "Uncompressed",
"dab_music_source_description": "For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching."
} }

View File

@ -2930,6 +2930,24 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Scrobbling'** /// **'Scrobbling'**
String get scrobbling; String get scrobbling;
/// No description provided for @source.
///
/// In en, this message translates to:
/// **'Source: '**
String get source;
/// No description provided for @uncompressed.
///
/// In en, this message translates to:
/// **'Uncompressed'**
String get uncompressed;
/// No description provided for @dab_music_source_description.
///
/// In en, this message translates to:
/// **'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.'**
String get dab_music_source_description;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@ -1537,4 +1537,14 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get scrobbling => 'التتبع'; String get scrobbling => 'التتبع';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1538,4 +1538,14 @@ class AppLocalizationsBn extends AppLocalizations {
@override @override
String get scrobbling => 'স্ক্রোব্বলিং'; String get scrobbling => 'স্ক্রোব্বলিং';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1548,4 +1548,14 @@ class AppLocalizationsCa extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1538,4 +1538,14 @@ class AppLocalizationsCs extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1550,4 +1550,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1536,4 +1536,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1551,4 +1551,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1548,4 +1548,14 @@ class AppLocalizationsEu extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1536,4 +1536,14 @@ class AppLocalizationsFa extends AppLocalizations {
@override @override
String get scrobbling => 'اسکراب‌بلینگ'; String get scrobbling => 'اسکراب‌بلینگ';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1536,4 +1536,14 @@ class AppLocalizationsFi extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1556,4 +1556,14 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1542,4 +1542,14 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get scrobbling => 'स्क्रॉबलिंग'; String get scrobbling => 'स्क्रॉबलिंग';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1544,4 +1544,14 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1543,4 +1543,14 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1507,4 +1507,14 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1545,4 +1545,14 @@ class AppLocalizationsKa extends AppLocalizations {
@override @override
String get scrobbling => 'სქრობლინგი'; String get scrobbling => 'სქრობლინგი';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1511,4 +1511,14 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get scrobbling => '스크로블링'; String get scrobbling => '스크로블링';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1548,4 +1548,14 @@ class AppLocalizationsNe extends AppLocalizations {
@override @override
String get scrobbling => 'स्क्रब्बलिंग'; String get scrobbling => 'स्क्रब्बलिंग';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1542,4 +1542,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1544,4 +1544,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1541,4 +1541,14 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1544,4 +1544,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get scrobbling => 'Скробблинг'; String get scrobbling => 'Скробблинг';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1550,4 +1550,14 @@ class AppLocalizationsTa extends AppLocalizations {
@override @override
String get scrobbling => 'ஸ்க்ரோப்ளிங்'; String get scrobbling => 'ஸ்க்ரோப்ளிங்';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1533,4 +1533,14 @@ class AppLocalizationsTh extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1551,4 +1551,14 @@ class AppLocalizationsTl extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1544,4 +1544,14 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1540,4 +1540,14 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get scrobbling => 'Скроблінг'; String get scrobbling => 'Скроблінг';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1546,4 +1546,14 @@ class AppLocalizationsVi extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }

View File

@ -1500,6 +1500,16 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
@override
String get uncompressed => 'Uncompressed';
@override
String get dab_music_source_description =>
'For audiophiles. Provides high-quality/lossless audio streams. Accurate ISRC based track matching.';
} }
/// The translations for Chinese, as used in Taiwan (`zh_TW`). /// The translations for Chinese, as used in Taiwan (`zh_TW`).

View File

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

View File

@ -18,15 +18,12 @@ class $AuthenticationTableTable extends AuthenticationTable
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _cookieMeta = const VerificationMeta('cookie');
@override @override
late final GeneratedColumnWithTypeConverter<DecryptedText, String> cookie = late final GeneratedColumnWithTypeConverter<DecryptedText, String> cookie =
GeneratedColumn<String>('cookie', aliasedName, false, GeneratedColumn<String>('cookie', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true) type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<DecryptedText>( .withConverter<DecryptedText>(
$AuthenticationTableTable.$convertercookie); $AuthenticationTableTable.$convertercookie);
static const VerificationMeta _accessTokenMeta =
const VerificationMeta('accessToken');
@override @override
late final GeneratedColumnWithTypeConverter<DecryptedText, String> late final GeneratedColumnWithTypeConverter<DecryptedText, String>
accessToken = GeneratedColumn<String>('access_token', aliasedName, false, accessToken = GeneratedColumn<String>('access_token', aliasedName, false,
@ -55,8 +52,6 @@ class $AuthenticationTableTable extends AuthenticationTable
if (data.containsKey('id')) { if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} }
context.handle(_cookieMeta, const VerificationResult.success());
context.handle(_accessTokenMeta, const VerificationResult.success());
if (data.containsKey('expiration')) { if (data.containsKey('expiration')) {
context.handle( context.handle(
_expirationMeta, _expirationMeta,
@ -301,8 +296,6 @@ class $BlacklistTableTable extends BlacklistTable
late final GeneratedColumn<String> name = GeneratedColumn<String>( late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name', aliasedName, false, 'name', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _elementTypeMeta =
const VerificationMeta('elementType');
@override @override
late final GeneratedColumnWithTypeConverter<BlacklistedType, String> late final GeneratedColumnWithTypeConverter<BlacklistedType, String>
elementType = GeneratedColumn<String>('element_type', aliasedName, false, elementType = GeneratedColumn<String>('element_type', aliasedName, false,
@ -336,7 +329,6 @@ class $BlacklistTableTable extends BlacklistTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_nameMeta); context.missing(_nameMeta);
} }
context.handle(_elementTypeMeta, const VerificationResult.success());
if (data.containsKey('element_id')) { if (data.containsKey('element_id')) {
context.handle(_elementIdMeta, context.handle(_elementIdMeta,
elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta));
@ -566,8 +558,6 @@ class $PreferencesTableTable extends PreferencesTable
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _audioQualityMeta =
const VerificationMeta('audioQuality');
@override @override
late final GeneratedColumnWithTypeConverter<SourceQualities, String> late final GeneratedColumnWithTypeConverter<SourceQualities, String>
audioQuality = GeneratedColumn<String>( audioQuality = GeneratedColumn<String>(
@ -647,8 +637,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("skip_non_music" IN (0, 1))'), 'CHECK ("skip_non_music" IN (0, 1))'),
defaultValue: const Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _closeBehaviorMeta =
const VerificationMeta('closeBehavior');
@override @override
late final GeneratedColumnWithTypeConverter<CloseBehavior, String> late final GeneratedColumnWithTypeConverter<CloseBehavior, String>
closeBehavior = GeneratedColumn<String>( closeBehavior = GeneratedColumn<String>(
@ -658,8 +646,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultValue: Constant(CloseBehavior.close.name)) defaultValue: Constant(CloseBehavior.close.name))
.withConverter<CloseBehavior>( .withConverter<CloseBehavior>(
$PreferencesTableTable.$convertercloseBehavior); $PreferencesTableTable.$convertercloseBehavior);
static const VerificationMeta _accentColorSchemeMeta =
const VerificationMeta('accentColorScheme');
@override @override
late final GeneratedColumnWithTypeConverter<SpotubeColor, String> late final GeneratedColumnWithTypeConverter<SpotubeColor, String>
accentColorScheme = GeneratedColumn<String>( accentColorScheme = GeneratedColumn<String>(
@ -669,8 +655,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultValue: const Constant("Slate:0xff64748b")) defaultValue: const Constant("Slate:0xff64748b"))
.withConverter<SpotubeColor>( .withConverter<SpotubeColor>(
$PreferencesTableTable.$converteraccentColorScheme); $PreferencesTableTable.$converteraccentColorScheme);
static const VerificationMeta _layoutModeMeta =
const VerificationMeta('layoutMode');
@override @override
late final GeneratedColumnWithTypeConverter<LayoutMode, String> layoutMode = late final GeneratedColumnWithTypeConverter<LayoutMode, String> layoutMode =
GeneratedColumn<String>('layout_mode', aliasedName, false, GeneratedColumn<String>('layout_mode', aliasedName, false,
@ -679,7 +663,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultValue: Constant(LayoutMode.adaptive.name)) defaultValue: Constant(LayoutMode.adaptive.name))
.withConverter<LayoutMode>( .withConverter<LayoutMode>(
$PreferencesTableTable.$converterlayoutMode); $PreferencesTableTable.$converterlayoutMode);
static const VerificationMeta _localeMeta = const VerificationMeta('locale');
@override @override
late final GeneratedColumnWithTypeConverter<Locale, String> locale = late final GeneratedColumnWithTypeConverter<Locale, String> locale =
GeneratedColumn<String>('locale', aliasedName, false, GeneratedColumn<String>('locale', aliasedName, false,
@ -688,7 +671,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultValue: const Constant( defaultValue: const Constant(
'{"languageCode":"system","countryCode":"system"}')) '{"languageCode":"system","countryCode":"system"}'))
.withConverter<Locale>($PreferencesTableTable.$converterlocale); .withConverter<Locale>($PreferencesTableTable.$converterlocale);
static const VerificationMeta _marketMeta = const VerificationMeta('market');
@override @override
late final GeneratedColumnWithTypeConverter<Market, String> market = late final GeneratedColumnWithTypeConverter<Market, String> market =
GeneratedColumn<String>('market', aliasedName, false, GeneratedColumn<String>('market', aliasedName, false,
@ -696,8 +678,6 @@ class $PreferencesTableTable extends PreferencesTable
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: Constant(Market.US.name)) defaultValue: Constant(Market.US.name))
.withConverter<Market>($PreferencesTableTable.$convertermarket); .withConverter<Market>($PreferencesTableTable.$convertermarket);
static const VerificationMeta _searchModeMeta =
const VerificationMeta('searchMode');
@override @override
late final GeneratedColumnWithTypeConverter<SearchMode, String> searchMode = late final GeneratedColumnWithTypeConverter<SearchMode, String> searchMode =
GeneratedColumn<String>('search_mode', aliasedName, false, GeneratedColumn<String>('search_mode', aliasedName, false,
@ -714,8 +694,6 @@ class $PreferencesTableTable extends PreferencesTable
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: const Constant("")); defaultValue: const Constant(""));
static const VerificationMeta _localLibraryLocationMeta =
const VerificationMeta('localLibraryLocation');
@override @override
late final GeneratedColumnWithTypeConverter<List<String>, String> late final GeneratedColumnWithTypeConverter<List<String>, String>
localLibraryLocation = GeneratedColumn<String>( localLibraryLocation = GeneratedColumn<String>(
@ -741,8 +719,6 @@ class $PreferencesTableTable extends PreferencesTable
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: const Constant("https://inv.nadeko.net")); defaultValue: const Constant("https://inv.nadeko.net"));
static const VerificationMeta _themeModeMeta =
const VerificationMeta('themeMode');
@override @override
late final GeneratedColumnWithTypeConverter<ThemeMode, String> themeMode = late final GeneratedColumnWithTypeConverter<ThemeMode, String> themeMode =
GeneratedColumn<String>('theme_mode', aliasedName, false, GeneratedColumn<String>('theme_mode', aliasedName, false,
@ -750,8 +726,6 @@ class $PreferencesTableTable extends PreferencesTable
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: Constant(ThemeMode.system.name)) defaultValue: Constant(ThemeMode.system.name))
.withConverter<ThemeMode>($PreferencesTableTable.$converterthemeMode); .withConverter<ThemeMode>($PreferencesTableTable.$converterthemeMode);
static const VerificationMeta _audioSourceMeta =
const VerificationMeta('audioSource');
@override @override
late final GeneratedColumnWithTypeConverter<AudioSource, String> audioSource = late final GeneratedColumnWithTypeConverter<AudioSource, String> audioSource =
GeneratedColumn<String>('audio_source', aliasedName, false, GeneratedColumn<String>('audio_source', aliasedName, false,
@ -760,8 +734,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultValue: Constant(AudioSource.youtube.name)) defaultValue: Constant(AudioSource.youtube.name))
.withConverter<AudioSource>( .withConverter<AudioSource>(
$PreferencesTableTable.$converteraudioSource); $PreferencesTableTable.$converteraudioSource);
static const VerificationMeta _youtubeClientEngineMeta =
const VerificationMeta('youtubeClientEngine');
@override @override
late final GeneratedColumnWithTypeConverter<YoutubeClientEngine, String> late final GeneratedColumnWithTypeConverter<YoutubeClientEngine, String>
youtubeClientEngine = GeneratedColumn<String>( youtubeClientEngine = GeneratedColumn<String>(
@ -771,8 +743,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)) defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name))
.withConverter<YoutubeClientEngine>( .withConverter<YoutubeClientEngine>(
$PreferencesTableTable.$converteryoutubeClientEngine); $PreferencesTableTable.$converteryoutubeClientEngine);
static const VerificationMeta _streamMusicCodecMeta =
const VerificationMeta('streamMusicCodec');
@override @override
late final GeneratedColumnWithTypeConverter<SourceCodecs, String> late final GeneratedColumnWithTypeConverter<SourceCodecs, String>
streamMusicCodec = GeneratedColumn<String>( streamMusicCodec = GeneratedColumn<String>(
@ -782,8 +752,6 @@ class $PreferencesTableTable extends PreferencesTable
defaultValue: Constant(SourceCodecs.weba.name)) defaultValue: Constant(SourceCodecs.weba.name))
.withConverter<SourceCodecs>( .withConverter<SourceCodecs>(
$PreferencesTableTable.$converterstreamMusicCodec); $PreferencesTableTable.$converterstreamMusicCodec);
static const VerificationMeta _downloadMusicCodecMeta =
const VerificationMeta('downloadMusicCodec');
@override @override
late final GeneratedColumnWithTypeConverter<SourceCodecs, String> late final GeneratedColumnWithTypeConverter<SourceCodecs, String>
downloadMusicCodec = GeneratedColumn<String>( downloadMusicCodec = GeneratedColumn<String>(
@ -887,7 +855,6 @@ class $PreferencesTableTable extends PreferencesTable
if (data.containsKey('id')) { if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} }
context.handle(_audioQualityMeta, const VerificationResult.success());
if (data.containsKey('album_color_sync')) { if (data.containsKey('album_color_sync')) {
context.handle( context.handle(
_albumColorSyncMeta, _albumColorSyncMeta,
@ -930,20 +897,12 @@ class $PreferencesTableTable extends PreferencesTable
skipNonMusic.isAcceptableOrUnknown( skipNonMusic.isAcceptableOrUnknown(
data['skip_non_music']!, _skipNonMusicMeta)); data['skip_non_music']!, _skipNonMusicMeta));
} }
context.handle(_closeBehaviorMeta, const VerificationResult.success());
context.handle(_accentColorSchemeMeta, const VerificationResult.success());
context.handle(_layoutModeMeta, const VerificationResult.success());
context.handle(_localeMeta, const VerificationResult.success());
context.handle(_marketMeta, const VerificationResult.success());
context.handle(_searchModeMeta, const VerificationResult.success());
if (data.containsKey('download_location')) { if (data.containsKey('download_location')) {
context.handle( context.handle(
_downloadLocationMeta, _downloadLocationMeta,
downloadLocation.isAcceptableOrUnknown( downloadLocation.isAcceptableOrUnknown(
data['download_location']!, _downloadLocationMeta)); data['download_location']!, _downloadLocationMeta));
} }
context.handle(
_localLibraryLocationMeta, const VerificationResult.success());
if (data.containsKey('piped_instance')) { if (data.containsKey('piped_instance')) {
context.handle( context.handle(
_pipedInstanceMeta, _pipedInstanceMeta,
@ -956,12 +915,6 @@ class $PreferencesTableTable extends PreferencesTable
invidiousInstance.isAcceptableOrUnknown( invidiousInstance.isAcceptableOrUnknown(
data['invidious_instance']!, _invidiousInstanceMeta)); data['invidious_instance']!, _invidiousInstanceMeta));
} }
context.handle(_themeModeMeta, const VerificationResult.success());
context.handle(_audioSourceMeta, const VerificationResult.success());
context.handle(
_youtubeClientEngineMeta, const VerificationResult.success());
context.handle(_streamMusicCodecMeta, const VerificationResult.success());
context.handle(_downloadMusicCodecMeta, const VerificationResult.success());
if (data.containsKey('discord_presence')) { if (data.containsKey('discord_presence')) {
context.handle( context.handle(
_discordPresenceMeta, _discordPresenceMeta,
@ -2030,8 +1983,6 @@ class $ScrobblerTableTable extends ScrobblerTable
late final GeneratedColumn<String> username = GeneratedColumn<String>( late final GeneratedColumn<String> username = GeneratedColumn<String>(
'username', aliasedName, false, 'username', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _passwordHashMeta =
const VerificationMeta('passwordHash');
@override @override
late final GeneratedColumnWithTypeConverter<DecryptedText, String> late final GeneratedColumnWithTypeConverter<DecryptedText, String>
passwordHash = GeneratedColumn<String>( passwordHash = GeneratedColumn<String>(
@ -2064,7 +2015,6 @@ class $ScrobblerTableTable extends ScrobblerTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_usernameMeta); context.missing(_usernameMeta);
} }
context.handle(_passwordHashMeta, const VerificationResult.success());
return context; return context;
} }
@ -2595,8 +2545,6 @@ class $SourceMatchTableTable extends SourceMatchTable
late final GeneratedColumn<String> sourceId = GeneratedColumn<String>( late final GeneratedColumn<String> sourceId = GeneratedColumn<String>(
'source_id', aliasedName, false, 'source_id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _sourceTypeMeta =
const VerificationMeta('sourceType');
@override @override
late final GeneratedColumnWithTypeConverter<SourceType, String> sourceType = late final GeneratedColumnWithTypeConverter<SourceType, String> sourceType =
GeneratedColumn<String>('source_type', aliasedName, false, GeneratedColumn<String>('source_type', aliasedName, false,
@ -2642,7 +2590,6 @@ class $SourceMatchTableTable extends SourceMatchTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_sourceIdMeta); context.missing(_sourceIdMeta);
} }
context.handle(_sourceTypeMeta, const VerificationResult.success());
if (data.containsKey('created_at')) { if (data.containsKey('created_at')) {
context.handle(_createdAtMeta, context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
@ -2901,8 +2848,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
requiredDuringInsert: true, requiredDuringInsert: true,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))'));
static const VerificationMeta _loopModeMeta =
const VerificationMeta('loopMode');
@override @override
late final GeneratedColumnWithTypeConverter<PlaylistMode, String> loopMode = late final GeneratedColumnWithTypeConverter<PlaylistMode, String> loopMode =
GeneratedColumn<String>('loop_mode', aliasedName, false, GeneratedColumn<String>('loop_mode', aliasedName, false,
@ -2918,15 +2863,12 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
requiredDuringInsert: true, requiredDuringInsert: true,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))'));
static const VerificationMeta _collectionsMeta =
const VerificationMeta('collections');
@override @override
late final GeneratedColumnWithTypeConverter<List<String>, String> late final GeneratedColumnWithTypeConverter<List<String>, String>
collections = GeneratedColumn<String>('collections', aliasedName, false, collections = GeneratedColumn<String>('collections', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true) type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<List<String>>( .withConverter<List<String>>(
$AudioPlayerStateTableTable.$convertercollections); $AudioPlayerStateTableTable.$convertercollections);
static const VerificationMeta _tracksMeta = const VerificationMeta('tracks');
@override @override
late final GeneratedColumnWithTypeConverter<List<SpotubeTrackObject>, String> late final GeneratedColumnWithTypeConverter<List<SpotubeTrackObject>, String>
tracks = GeneratedColumn<String>('tracks', aliasedName, false, tracks = GeneratedColumn<String>('tracks', aliasedName, false,
@ -2966,15 +2908,12 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_playingMeta); context.missing(_playingMeta);
} }
context.handle(_loopModeMeta, const VerificationResult.success());
if (data.containsKey('shuffled')) { if (data.containsKey('shuffled')) {
context.handle(_shuffledMeta, context.handle(_shuffledMeta,
shuffled.isAcceptableOrUnknown(data['shuffled']!, _shuffledMeta)); shuffled.isAcceptableOrUnknown(data['shuffled']!, _shuffledMeta));
} else if (isInserting) { } else if (isInserting) {
context.missing(_shuffledMeta); context.missing(_shuffledMeta);
} }
context.handle(_collectionsMeta, const VerificationResult.success());
context.handle(_tracksMeta, const VerificationResult.success());
if (data.containsKey('current_index')) { if (data.containsKey('current_index')) {
context.handle( context.handle(
_currentIndexMeta, _currentIndexMeta,
@ -3305,7 +3244,6 @@ class $HistoryTableTable extends HistoryTable
type: DriftSqlType.dateTime, type: DriftSqlType.dateTime,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: currentDateAndTime); defaultValue: currentDateAndTime);
static const VerificationMeta _typeMeta = const VerificationMeta('type');
@override @override
late final GeneratedColumnWithTypeConverter<HistoryEntryType, String> type = late final GeneratedColumnWithTypeConverter<HistoryEntryType, String> type =
GeneratedColumn<String>('type', aliasedName, false, GeneratedColumn<String>('type', aliasedName, false,
@ -3316,7 +3254,6 @@ class $HistoryTableTable extends HistoryTable
late final GeneratedColumn<String> itemId = GeneratedColumn<String>( late final GeneratedColumn<String> itemId = GeneratedColumn<String>(
'item_id', aliasedName, false, 'item_id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _dataMeta = const VerificationMeta('data');
@override @override
late final GeneratedColumnWithTypeConverter<Map<String, dynamic>, String> late final GeneratedColumnWithTypeConverter<Map<String, dynamic>, String>
data = GeneratedColumn<String>('data', aliasedName, false, data = GeneratedColumn<String>('data', aliasedName, false,
@ -3342,14 +3279,12 @@ class $HistoryTableTable extends HistoryTable
context.handle(_createdAtMeta, context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
} }
context.handle(_typeMeta, const VerificationResult.success());
if (data.containsKey('item_id')) { if (data.containsKey('item_id')) {
context.handle(_itemIdMeta, context.handle(_itemIdMeta,
itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta)); itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta));
} else if (isInserting) { } else if (isInserting) {
context.missing(_itemIdMeta); context.missing(_itemIdMeta);
} }
context.handle(_dataMeta, const VerificationResult.success());
return context; return context;
} }
@ -3608,7 +3543,6 @@ class $LyricsTableTable extends LyricsTable
late final GeneratedColumn<String> trackId = GeneratedColumn<String>( late final GeneratedColumn<String> trackId = GeneratedColumn<String>(
'track_id', aliasedName, false, 'track_id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _dataMeta = const VerificationMeta('data');
@override @override
late final GeneratedColumnWithTypeConverter<SubtitleSimple, String> data = late final GeneratedColumnWithTypeConverter<SubtitleSimple, String> data =
GeneratedColumn<String>('data', aliasedName, false, GeneratedColumn<String>('data', aliasedName, false,
@ -3635,7 +3569,6 @@ class $LyricsTableTable extends LyricsTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_trackIdMeta); context.missing(_trackIdMeta);
} }
context.handle(_dataMeta, const VerificationResult.success());
return context; return context;
} }
@ -3853,15 +3786,12 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable
late final GeneratedColumn<String> entryPoint = GeneratedColumn<String>( late final GeneratedColumn<String> entryPoint = GeneratedColumn<String>(
'entry_point', aliasedName, false, 'entry_point', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _apisMeta = const VerificationMeta('apis');
@override @override
late final GeneratedColumnWithTypeConverter<List<String>, String> apis = late final GeneratedColumnWithTypeConverter<List<String>, String> apis =
GeneratedColumn<String>('apis', aliasedName, false, GeneratedColumn<String>('apis', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true) type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<List<String>>( .withConverter<List<String>>(
$MetadataPluginsTableTable.$converterapis); $MetadataPluginsTableTable.$converterapis);
static const VerificationMeta _abilitiesMeta =
const VerificationMeta('abilities');
@override @override
late final GeneratedColumnWithTypeConverter<List<String>, String> abilities = late final GeneratedColumnWithTypeConverter<List<String>, String> abilities =
GeneratedColumn<String>('abilities', aliasedName, false, GeneratedColumn<String>('abilities', aliasedName, false,
@ -3954,8 +3884,6 @@ class $MetadataPluginsTableTable extends MetadataPluginsTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_entryPointMeta); context.missing(_entryPointMeta);
} }
context.handle(_apisMeta, const VerificationResult.success());
context.handle(_abilitiesMeta, const VerificationResult.success());
if (data.containsKey('selected')) { if (data.containsKey('selected')) {
context.handle(_selectedMeta, context.handle(_selectedMeta,
selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta)); selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta));

View File

@ -12,12 +12,14 @@ enum CloseBehavior {
} }
enum AudioSource { enum AudioSource {
youtube, youtube("YouTube"),
piped, piped("Piped"),
jiosaavn, jiosaavn("JioSaavn"),
invidious; invidious("Invidious"),
dabMusic("DAB Music");
String get label => name[0].toUpperCase() + name.substring(1); final String label;
const AudioSource(this.label);
} }
enum YoutubeClientEngine { enum YoutubeClientEngine {
@ -39,14 +41,6 @@ enum YoutubeClientEngine {
} }
} }
enum MusicCodec {
m4a._("M4a (Best for downloaded music)"),
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
final String label;
const MusicCodec._(this.label);
}
enum SearchMode { enum SearchMode {
youtube._("YouTube"), youtube._("YouTube"),
youtubeMusic._("YouTube Music"); youtubeMusic._("YouTube Music");

View File

@ -3,7 +3,8 @@ part of '../database.dart';
enum SourceType { enum SourceType {
youtube._("YouTube"), youtube._("YouTube"),
youtubeMusic._("YouTube Music"), youtubeMusic._("YouTube Music"),
jiosaavn._("JioSaavn"); jiosaavn._("JioSaavn"),
dabMusic._("DAB Music");
final String label; final String label;

File diff suppressed because it is too large Load Diff

View File

@ -103,6 +103,7 @@ class TrackSource with _$TrackSource {
required SourceQualities quality, required SourceQualities quality,
required SourceCodecs codec, required SourceCodecs codec,
required String bitrate, required String bitrate,
required String qualityLabel,
}) = _TrackSource; }) = _TrackSource;
factory TrackSource.fromJson(Map<String, dynamic> json) => factory TrackSource.fromJson(Map<String, dynamic> json) =>

View File

@ -28,8 +28,12 @@ mixin _$TrackSourceQuery {
String get isrc => throw _privateConstructorUsedError; String get isrc => throw _privateConstructorUsedError;
bool get explicit => throw _privateConstructorUsedError; bool get explicit => throw _privateConstructorUsedError;
/// Serializes this TrackSourceQuery to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// 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 => $TrackSourceQueryCopyWith<TrackSourceQuery> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -60,6 +64,8 @@ class _$TrackSourceQueryCopyWithImpl<$Res, $Val extends TrackSourceQuery>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; 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') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -130,6 +136,8 @@ class __$$TrackSourceQueryImplCopyWithImpl<$Res>
$Res Function(_$TrackSourceQueryImpl) _then) $Res Function(_$TrackSourceQueryImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -233,7 +241,7 @@ class _$TrackSourceQueryImpl extends _TrackSourceQuery {
other.explicit == explicit)); other.explicit == explicit));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
@ -245,7 +253,9 @@ class _$TrackSourceQueryImpl extends _TrackSourceQuery {
isrc, isrc,
explicit); explicit);
@JsonKey(ignore: true) /// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
@ -288,8 +298,11 @@ abstract class _TrackSourceQuery extends TrackSourceQuery {
String get isrc; String get isrc;
@override @override
bool get explicit; bool get explicit;
/// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith => _$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -307,8 +320,12 @@ mixin _$TrackSourceInfo {
String get pageUrl => throw _privateConstructorUsedError; String get pageUrl => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError; int get durationMs => throw _privateConstructorUsedError;
/// Serializes this TrackSourceInfo to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// 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 => $TrackSourceInfoCopyWith<TrackSourceInfo> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -338,6 +355,8 @@ class _$TrackSourceInfoCopyWithImpl<$Res, $Val extends TrackSourceInfo>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; 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') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -402,6 +421,8 @@ class __$$TrackSourceInfoImplCopyWithImpl<$Res>
_$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then) _$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -488,12 +509,14 @@ class _$TrackSourceInfoImpl implements _TrackSourceInfo {
other.durationMs == durationMs)); other.durationMs == durationMs));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, id, title, artists, thumbnail, pageUrl, durationMs); runtimeType, id, title, artists, thumbnail, pageUrl, durationMs);
@JsonKey(ignore: true) /// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
@ -532,8 +555,11 @@ abstract class _TrackSourceInfo implements TrackSourceInfo {
String get pageUrl; String get pageUrl;
@override @override
int get durationMs; int get durationMs;
/// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith => _$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -548,9 +574,14 @@ mixin _$TrackSource {
SourceQualities get quality => throw _privateConstructorUsedError; SourceQualities get quality => throw _privateConstructorUsedError;
SourceCodecs get codec => throw _privateConstructorUsedError; SourceCodecs get codec => throw _privateConstructorUsedError;
String get bitrate => 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; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// 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 => $TrackSourceCopyWith<TrackSource> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -565,7 +596,8 @@ abstract class $TrackSourceCopyWith<$Res> {
{String url, {String url,
SourceQualities quality, SourceQualities quality,
SourceCodecs codec, SourceCodecs codec,
String bitrate}); String bitrate,
String qualityLabel});
} }
/// @nodoc /// @nodoc
@ -578,6 +610,8 @@ class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; 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') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -585,6 +619,7 @@ class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource>
Object? quality = null, Object? quality = null,
Object? codec = null, Object? codec = null,
Object? bitrate = null, Object? bitrate = null,
Object? qualityLabel = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
url: null == url url: null == url
@ -603,6 +638,10 @@ class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource>
? _value.bitrate ? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable : bitrate // ignore: cast_nullable_to_non_nullable
as String, as String,
qualityLabel: null == qualityLabel
? _value.qualityLabel
: qualityLabel // ignore: cast_nullable_to_non_nullable
as String,
) as $Val); ) as $Val);
} }
} }
@ -619,7 +658,8 @@ abstract class _$$TrackSourceImplCopyWith<$Res>
{String url, {String url,
SourceQualities quality, SourceQualities quality,
SourceCodecs codec, SourceCodecs codec,
String bitrate}); String bitrate,
String qualityLabel});
} }
/// @nodoc /// @nodoc
@ -630,6 +670,8 @@ class __$$TrackSourceImplCopyWithImpl<$Res>
_$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then) _$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of TrackSource
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -637,6 +679,7 @@ class __$$TrackSourceImplCopyWithImpl<$Res>
Object? quality = null, Object? quality = null,
Object? codec = null, Object? codec = null,
Object? bitrate = null, Object? bitrate = null,
Object? qualityLabel = null,
}) { }) {
return _then(_$TrackSourceImpl( return _then(_$TrackSourceImpl(
url: null == url url: null == url
@ -655,6 +698,10 @@ class __$$TrackSourceImplCopyWithImpl<$Res>
? _value.bitrate ? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable : bitrate // ignore: cast_nullable_to_non_nullable
as String, as String,
qualityLabel: null == qualityLabel
? _value.qualityLabel
: qualityLabel // ignore: cast_nullable_to_non_nullable
as String,
)); ));
} }
} }
@ -666,7 +713,8 @@ class _$TrackSourceImpl implements _TrackSource {
{required this.url, {required this.url,
required this.quality, required this.quality,
required this.codec, required this.codec,
required this.bitrate}); required this.bitrate,
required this.qualityLabel});
factory _$TrackSourceImpl.fromJson(Map<String, dynamic> json) => factory _$TrackSourceImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceImplFromJson(json); _$$TrackSourceImplFromJson(json);
@ -679,10 +727,12 @@ class _$TrackSourceImpl implements _TrackSource {
final SourceCodecs codec; final SourceCodecs codec;
@override @override
final String bitrate; final String bitrate;
@override
final String qualityLabel;
@override @override
String toString() { String toString() {
return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate)'; return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate, qualityLabel: $qualityLabel)';
} }
@override @override
@ -693,14 +743,19 @@ class _$TrackSourceImpl implements _TrackSource {
(identical(other.url, url) || other.url == url) && (identical(other.url, url) || other.url == url) &&
(identical(other.quality, quality) || other.quality == quality) && (identical(other.quality, quality) || other.quality == quality) &&
(identical(other.codec, codec) || other.codec == codec) && (identical(other.codec, codec) || other.codec == codec) &&
(identical(other.bitrate, bitrate) || other.bitrate == bitrate)); (identical(other.bitrate, bitrate) || other.bitrate == bitrate) &&
(identical(other.qualityLabel, qualityLabel) ||
other.qualityLabel == qualityLabel));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, url, quality, codec, bitrate); int get hashCode =>
Object.hash(runtimeType, url, quality, codec, bitrate, qualityLabel);
@JsonKey(ignore: true) /// Create a copy of TrackSource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith => _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
@ -719,7 +774,8 @@ abstract class _TrackSource implements TrackSource {
{required final String url, {required final String url,
required final SourceQualities quality, required final SourceQualities quality,
required final SourceCodecs codec, required final SourceCodecs codec,
required final String bitrate}) = _$TrackSourceImpl; required final String bitrate,
required final String qualityLabel}) = _$TrackSourceImpl;
factory _TrackSource.fromJson(Map<String, dynamic> json) = factory _TrackSource.fromJson(Map<String, dynamic> json) =
_$TrackSourceImpl.fromJson; _$TrackSourceImpl.fromJson;
@ -733,7 +789,12 @@ abstract class _TrackSource implements TrackSource {
@override @override
String get bitrate; String get bitrate;
@override @override
@JsonKey(ignore: true) 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 => _$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View File

@ -36,6 +36,7 @@ const _$AudioSourceEnumMap = {
AudioSource.piped: 'piped', AudioSource.piped: 'piped',
AudioSource.jiosaavn: 'jiosaavn', AudioSource.jiosaavn: 'jiosaavn',
AudioSource.invidious: 'invidious', AudioSource.invidious: 'invidious',
AudioSource.dabMusic: 'dabMusic',
}; };
_$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) => _$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) =>
@ -88,6 +89,7 @@ _$TrackSourceImpl _$$TrackSourceImplFromJson(Map json) => _$TrackSourceImpl(
quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']), quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']),
codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']), codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']),
bitrate: json['bitrate'] as String, bitrate: json['bitrate'] as String,
qualityLabel: json['qualityLabel'] as String,
); );
Map<String, dynamic> _$$TrackSourceImplToJson(_$TrackSourceImpl instance) => Map<String, dynamic> _$$TrackSourceImplToJson(_$TrackSourceImpl instance) =>
@ -96,9 +98,11 @@ Map<String, dynamic> _$$TrackSourceImplToJson(_$TrackSourceImpl instance) =>
'quality': _$SourceQualitiesEnumMap[instance.quality]!, 'quality': _$SourceQualitiesEnumMap[instance.quality]!,
'codec': _$SourceCodecsEnumMap[instance.codec]!, 'codec': _$SourceCodecsEnumMap[instance.codec]!,
'bitrate': instance.bitrate, 'bitrate': instance.bitrate,
'qualityLabel': instance.qualityLabel,
}; };
const _$SourceQualitiesEnumMap = { const _$SourceQualitiesEnumMap = {
SourceQualities.uncompressed: 'uncompressed',
SourceQualities.high: 'high', SourceQualities.high: 'high',
SourceQualities.medium: 'medium', SourceQualities.medium: 'medium',
SourceQualities.low: 'low', SourceQualities.low: 'low',
@ -107,4 +111,6 @@ const _$SourceQualitiesEnumMap = {
const _$SourceCodecsEnumMap = { const _$SourceCodecsEnumMap = {
SourceCodecs.m4a: 'm4a', SourceCodecs.m4a: 'm4a',
SourceCodecs.weba: 'weba', SourceCodecs.weba: 'weba',
SourceCodecs.mp3: 'mp3',
SourceCodecs.flac: 'flac',
}; };

View File

@ -91,10 +91,27 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
) )
else ...[ else ...[
Text(context.l10n.author_name(plugin.author)), Text(context.l10n.author_name(plugin.author)),
DestructiveBadge( Container(
leading: const Icon(SpotubeIcons.warning), padding: const EdgeInsets.symmetric(
child: Text(context.l10n.third_party), horizontal: 6,
) vertical: 2,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
const Icon(SpotubeIcons.warning, size: 14),
Text(
context.l10n.third_party,
style: const TextStyle(color: Colors.white),
).xSmall
],
),
),
], ],
SecondaryBadge( SecondaryBadge(
leading: const Icon(SpotubeIcons.connect), leading: const Icon(SpotubeIcons.connect),

View File

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

View File

@ -46,6 +46,14 @@ class PlayerView extends HookConsumerWidget {
final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
final mediaQuery = MediaQuery.sizeOf(context); final mediaQuery = MediaQuery.sizeOf(context);
final activeSourceCodec = useMemoized(
() {
return currentActiveTrackSource
?.getSourceOfCodec(currentActiveTrackSource.codec);
},
[currentActiveTrackSource?.sources, currentActiveTrackSource?.codec],
);
final shouldHide = useState(true); final shouldHide = useState(true);
ref.listen(navigationPanelHeight, (_, height) { ref.listen(navigationPanelHeight, (_, height) {
@ -267,6 +275,21 @@ class PlayerView extends HookConsumerWidget {
); );
}), }),
), ),
const Gap(25),
if (activeSourceCodec != null)
OutlineBadge(
style: const ButtonStyle.outline(
size: ButtonSize.normal,
density: ButtonDensity.dense,
shape: ButtonShape.rectangle,
).copyWith(
textStyle: (context, states, value) {
return value.copyWith(fontWeight: FontWeight.w500);
},
),
leading: const Icon(SpotubeIcons.lightningOutlined),
child: Text(activeSourceCodec.qualityLabel),
)
], ],
), ),
), ),

View File

@ -7,7 +7,6 @@ import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
final audioSourceToIconMap = { final audioSourceToIconMap = {
@ -23,6 +22,8 @@ final audioSourceToIconMap = {
), ),
AudioSource.jiosaavn: AudioSource.jiosaavn:
Assets.images.logos.jiosaavn.image(width: 20, height: 20), Assets.images.logos.jiosaavn.image(width: 20, height: 20),
AudioSource.dabMusic:
Assets.images.logos.dabMusic.image(width: 20, height: 20),
}; };
class GettingStartedPagePlaybackSection extends HookConsumerWidget { class GettingStartedPagePlaybackSection extends HookConsumerWidget {
@ -47,8 +48,10 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
AudioSource.piped: context.l10n.piped_source_description, AudioSource.piped: context.l10n.piped_source_description,
AudioSource.jiosaavn: AudioSource.jiosaavn:
"${context.l10n.jiosaavn_source_description}\n" "${context.l10n.jiosaavn_source_description}\n"
"${context.l10n.highest_quality("320kbps mp")}", "${context.l10n.highest_quality("320kbps mp4")}",
AudioSource.invidious: context.l10n.invidious_source_description, AudioSource.invidious: context.l10n.invidious_source_description,
AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n"
"${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}",
}, },
[]); []);
@ -70,43 +73,28 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
child: Text(context.l10n.select_audio_source).semiBold().large(), child: Text(context.l10n.select_audio_source).semiBold().large(),
), ),
const Gap(16), const Gap(16),
Select<AudioSource>( RadioGroup<AudioSource>(
value: preferences.audioSource, value: preferences.audioSource,
onChanged: (value) { onChanged: (value) {
if (value == null) return;
preferencesNotifier.setAudioSource(value); preferencesNotifier.setAudioSource(value);
}, },
placeholder: Text(preferences.audioSource.name.capitalize()), child: Wrap(
itemBuilder: (context, value) => Row(
mainAxisSize: MainAxisSize.min,
spacing: 6, spacing: 6,
runSpacing: 6,
children: [ children: [
audioSourceToIconMap[value]!, for (final source in AudioSource.values)
Text(value.name.capitalize()), RadioCard(
],
),
popup: (context) {
return SelectPopup(
items: SelectItemBuilder(
childCount: AudioSource.values.length,
builder: (context, index) {
final source = AudioSource.values[index];
return SelectItemButton(
value: source, value: source,
child: Row( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [ children: [
audioSourceToIconMap[source]!, audioSourceToIconMap[source]!,
Text(source.name.capitalize()), Text(source.label),
], ],
), ),
);
},
), ),
); ],
}, ),
), ),
const Gap(16), const Gap(16),
Text( Text(

View File

@ -8,7 +8,7 @@ import 'package:flutter/material.dart' show ListTile;
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart'; import 'package:piped_client/piped_client.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
@ -44,10 +44,16 @@ class SettingsPlaybackSection extends HookConsumerWidget {
title: Text(context.l10n.audio_quality), title: Text(context.l10n.audio_quality),
value: preferences.audioQuality, value: preferences.audioQuality,
options: [ options: [
if (preferences.audioSource == AudioSource.dabMusic)
SelectItemButton(
value: SourceQualities.uncompressed,
child: Text(context.l10n.uncompressed),
),
SelectItemButton( SelectItemButton(
value: SourceQualities.high, value: SourceQualities.high,
child: Text(context.l10n.high), child: Text(context.l10n.high),
), ),
if (preferences.audioSource != AudioSource.dabMusic) ...[
SelectItemButton( SelectItemButton(
value: SourceQualities.medium, value: SourceQualities.medium,
child: Text(context.l10n.medium), child: Text(context.l10n.medium),
@ -56,6 +62,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
value: SourceQualities.low, value: SourceQualities.low,
child: Text(context.l10n.low), child: Text(context.l10n.low),
), ),
]
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
@ -396,7 +403,9 @@ class SettingsPlaybackSection extends HookConsumerWidget {
onChanged: preferencesNotifier.setNormalizeAudio, onChanged: preferencesNotifier.setNormalizeAudio,
), ),
), ),
if (preferences.audioSource != AudioSource.jiosaavn) ...[ if (const [AudioSource.jiosaavn, AudioSource.dabMusic]
.contains(preferences.audioSource) ==
false) ...[
AdaptiveSelectTile<SourceCodecs>( AdaptiveSelectTile<SourceCodecs>(
popupConstraints: const BoxConstraints(maxWidth: 300), popupConstraints: const BoxConstraints(maxWidth: 300),
secondary: const Icon(SpotubeIcons.stream), secondary: const Icon(SpotubeIcons.stream),

View File

@ -27,8 +27,12 @@ mixin _$AudioPlayerState {
int get currentIndex => throw _privateConstructorUsedError; int get currentIndex => throw _privateConstructorUsedError;
List<SpotubeTrackObject> get tracks => throw _privateConstructorUsedError; List<SpotubeTrackObject> get tracks => throw _privateConstructorUsedError;
/// Serializes this AudioPlayerState to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of AudioPlayerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AudioPlayerStateCopyWith<AudioPlayerState> get copyWith => $AudioPlayerStateCopyWith<AudioPlayerState> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -58,6 +62,8 @@ class _$AudioPlayerStateCopyWithImpl<$Res, $Val extends AudioPlayerState>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of AudioPlayerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -122,6 +128,8 @@ class __$$AudioPlayerStateImplCopyWithImpl<$Res>
$Res Function(_$AudioPlayerStateImpl) _then) $Res Function(_$AudioPlayerStateImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of AudioPlayerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -226,7 +234,7 @@ class _$AudioPlayerStateImpl extends _AudioPlayerState {
const DeepCollectionEquality().equals(other._tracks, _tracks)); const DeepCollectionEquality().equals(other._tracks, _tracks));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
@ -237,7 +245,9 @@ class _$AudioPlayerStateImpl extends _AudioPlayerState {
currentIndex, currentIndex,
const DeepCollectionEquality().hash(_tracks)); const DeepCollectionEquality().hash(_tracks));
@JsonKey(ignore: true) /// Create a copy of AudioPlayerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith => _$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith =>
@ -277,8 +287,11 @@ abstract class _AudioPlayerState extends AudioPlayerState {
int get currentIndex; int get currentIndex;
@override @override
List<SpotubeTrackObject> get tracks; List<SpotubeTrackObject> get tracks;
/// Create a copy of AudioPlayerState
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith => _$$AudioPlayerStateImplCopyWith<_$AudioPlayerStateImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View File

@ -350,6 +350,8 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
abilities: plugin.abilities.map((e) => e.name).toList(), abilities: plugin.abilities.map((e) => e.name).toList(),
pluginApiVersion: Value(plugin.pluginApiVersion), pluginApiVersion: Value(plugin.pluginApiVersion),
repository: Value(plugin.repository), repository: Value(plugin.repository),
// Setting the very first plugin as the default plugin
selected: Value(state.valueOrNull?.plugins.isEmpty ?? true),
), ),
); );
} }
@ -362,6 +364,17 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
} }
await database.metadataPluginsTable.deleteWhere((tbl) => await database.metadataPluginsTable.deleteWhere((tbl) =>
tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author)); 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?.defaultPluginConfig == plugin) {
final remainingPlugins =
state.valueOrNull?.plugins.where((p) => p != plugin) ?? [];
if (remainingPlugins.length == 1) {
await setDefaultPlugin(remainingPlugins.first);
}
}
} }
Future<void> updatePlugin( Future<void> updatePlugin(

View File

@ -12,6 +12,7 @@ final serverRouterProvider = Provider((ref) {
router.get("/ping", (Request request) => Response.ok("pong")); router.get("/ping", (Request request) => Response.ok("pong"));
router.head("/stream/<trackId>", playbackRoutes.headStreamTrackId);
router.get("/stream/<trackId>", playbackRoutes.getStreamTrackId); router.get("/stream/<trackId>", playbackRoutes.getStreamTrackId);
router.get("/playback/toggle-playback", playbackRoutes.togglePlayback); router.get("/playback/toggle-playback", playbackRoutes.togglePlayback);

View File

@ -46,21 +46,95 @@ class ServerPlaybackRoutes {
ServerPlaybackRoutes(this.ref) : dio = Dio(); ServerPlaybackRoutes(this.ref) : dio = Dio();
Future<String> _getTrackCacheFilePath(SourcedTrack track) async {
return join(
await UserPreferencesNotifier.getMusicCacheDir(),
ServiceUtils.sanitizeFilename(
'${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}',
),
);
}
Future<SourcedTrack?> _getSourcedTrack(
Request request, String trackId) async {
final track =
playlist.tracks.firstWhere((element) => element.id == trackId);
final activeSourcedTrack =
await ref.read(activeTrackSourcesProvider.future);
final sourcedTrack = activeSourcedTrack?.track.id == track.id
? activeSourcedTrack?.source
: await ref.read(
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;
}
Future<dio_lib.Response> streamTrackInformation(
Request request,
SourcedTrack track,
) async {
AppLogger.log.i(
"HEAD request for track: ${track.query.title}\n"
"Headers: ${request.headers}",
);
final trackCacheFile = File(await _getTrackCacheFilePath(track));
if (await trackCacheFile.exists() && userPreferences.cacheMusic) {
final fileLength = await trackCacheFile.length();
return dio_lib.Response(
statusCode: 200,
headers: Headers.fromMap({
"content-type": ["audio/${track.codec.name}"],
"content-length": ["$fileLength"],
"accept-ranges": ["bytes"],
"content-range": ["bytes 0-$fileLength/$fileLength"],
}),
requestOptions: RequestOptions(path: request.requestedUri.toString()),
);
}
String url = track.url ??
await ref
.read(trackSourcesProvider(track.query).notifier)
.swapWithNextSibling()
.then((track) => track.url!);
final options = Options(
headers: {
"user-agent": _randomUserAgent,
"Cache-Control": "max-age=3600",
"Connection": "keep-alive",
"host": Uri.parse(url).host,
},
validateStatus: (status) => status! < 400,
);
final res = await dio.head(url, options: options);
return res;
}
Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})> Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})>
streamTrack( streamTrack(
Request request, Request request,
SourcedTrack track, SourcedTrack track,
Map<String, dynamic> headers, Map<String, dynamic> headers,
) async { ) async {
final trackCacheFile = File( AppLogger.log.i(
join( "GET request for track: ${track.query.title}\n"
await UserPreferencesNotifier.getMusicCacheDir(), "Headers: ${request.headers}",
ServiceUtils.sanitizeFilename(
'${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}',
),
),
); );
final trackCacheFile = File(await _getTrackCacheFilePath(track));
if (await trackCacheFile.exists() && userPreferences.cacheMusic) { if (await trackCacheFile.exists() && userPreferences.cacheMusic) {
final bytes = await trackCacheFile.readAsBytes(); final bytes = await trackCacheFile.readAsBytes();
final cachedFileLength = bytes.length; final cachedFileLength = bytes.length;
@ -101,10 +175,7 @@ class ServerPlaybackRoutes {
); );
final contentLengthRes = await Future<dio_lib.Response?>.value( final contentLengthRes = await Future<dio_lib.Response?>.value(
dio.head( dio.head(url, options: options),
url,
options: options,
),
).catchError((e, stack) async { ).catchError((e, stack) async {
AppLogger.reportError(e, stack); AppLogger.reportError(e, stack);
@ -135,25 +206,33 @@ class ServerPlaybackRoutes {
); );
} }
final contentLength = contentLengthRes?.headers.value("content-length"); 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,
int.parse(contentLengthRes?.headers.value("content-length") ?? "0"),
);
/// Forcing partial content range as mpv sometimes greedily wants
/// everything at one go. Slows down overall streaming.
final range = RangeHeader.parse(headers["range"] ?? "");
final contentPartialLength = int.tryParse(contentLength ?? "");
if ((range.end == null) &&
contentPartialLength != null &&
range.start == 0) {
options = options.copyWith( options = options.copyWith(
headers: { headers: {
...?options.headers, ...?options.headers,
"range": "$range${(contentPartialLength * 0.3).ceil()}", "range": "bytes=0-$endRange",
}, },
); );
} }
final res = await dio.get<Uint8List>(url, options: options); final res = await dio.get<Uint8List>(url, options: options);
AppLogger.log.i(
"Response for track: ${track.query.title}\n"
"Status Code: ${res.statusCode}\n"
"Headers: ${res.headers.map}",
);
final bytes = res.data; final bytes = res.data;
if (bytes == null || !userPreferences.cacheMusic) { if (bytes == null || !userPreferences.cacheMusic) {
@ -213,27 +292,42 @@ class ServerPlaybackRoutes {
return (bytes: bytes, response: res); return (bytes: bytes, response: res);
} }
/// @head('/stream/<trackId>')
Future<Response> headStreamTrackId(Request request, String trackId) async {
try {
final sourcedTrack = await _getSourcedTrack(request, trackId);
if (sourcedTrack == null) {
return Response.notFound("Track not found in the current queue");
}
final res = await streamTrackInformation(
request,
sourcedTrack,
);
return Response(
res.statusCode!,
headers: res.headers.map,
);
} catch (e, stack) {
AppLogger.reportError(e, stack);
return Response.internalServerError();
}
}
/// @get('/stream/<trackId>') /// @get('/stream/<trackId>')
Future<Response> getStreamTrackId(Request request, String trackId) async { Future<Response> getStreamTrackId(Request request, String trackId) async {
try { try {
final track = final sourcedTrack = await _getSourcedTrack(request, trackId);
playlist.tracks.firstWhere((element) => element.id == trackId);
final activeSourcedTrack = if (sourcedTrack == null) {
await ref.read(activeTrackSourcesProvider.future); return Response.notFound("Track not found in the current queue");
final sourcedTrack = activeSourcedTrack?.track.id == track.id }
? activeSourcedTrack?.source
: await ref.read(
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,
);
final (bytes: audioBytes, response: res) = await streamTrack( final (bytes: audioBytes, response: res) = await streamTrack(
request, request,
sourcedTrack!, sourcedTrack,
request.headers, request.headers,
); );

View File

@ -54,6 +54,7 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
} }
await audioPlayer.setAudioNormalization(state.normalizeAudio); await audioPlayer.setAudioNormalization(state.normalizeAudio);
await _updatePlayerBufferSize(event.audioQuality, state.audioQuality);
} catch (e, stack) { } catch (e, stack) {
AppLogger.reportError(e, stack); AppLogger.reportError(e, stack);
} }
@ -79,6 +80,24 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
}); });
} }
/// Sets audio player's buffer size based on the selected audio quality
/// Uncompressed quality gets a larger buffer size for smoother playback
/// while other qualities use a standard buffer size.
Future<void> _updatePlayerBufferSize(
SourceQualities newQuality,
SourceQualities oldQuality,
) async {
if (newQuality == SourceQualities.uncompressed) {
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
return;
}
if (oldQuality == SourceQualities.uncompressed &&
newQuality != SourceQualities.uncompressed) {
audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB
}
}
Future<void> setData(PreferencesTableCompanion data) async { Future<void> setData(PreferencesTableCompanion data) async {
final db = ref.read(databaseProvider); final db = ref.read(databaseProvider);
@ -155,6 +174,7 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
void setAudioQuality(SourceQualities quality) { void setAudioQuality(SourceQualities quality) {
setData(PreferencesTableCompanion(audioQuality: Value(quality))); setData(PreferencesTableCompanion(audioQuality: Value(quality)));
_updatePlayerBufferSize(quality, state.audioQuality);
} }
void setDownloadLocation(String downloadDir) { void setDownloadLocation(String downloadDir) {
@ -204,6 +224,23 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
} }
void setAudioSource(AudioSource type) { void setAudioSource(AudioSource type) {
switch ((type, state.audioQuality)) {
// DAB music only supports high quality/uncompressed streams
case (
AudioSource.dabMusic,
SourceQualities.low || SourceQualities.medium
):
setAudioQuality(SourceQualities.high);
break;
// If the user switches from DAB music to other sources and has
// uncompressed quality selected, downgrade to high quality
case (!= AudioSource.dabMusic, SourceQualities.uncompressed):
setAudioQuality(SourceQualities.high);
break;
default:
break;
}
setData(PreferencesTableCompanion(audioSource: Value(type))); setData(PreferencesTableCompanion(audioSource: Value(type)));
} }

View File

@ -56,6 +56,7 @@ abstract class AudioPlayerInterface {
configuration: const mk.PlayerConfiguration( configuration: const mk.PlayerConfiguration(
title: "Spotube", title: "Spotube",
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
bufferSize: 4 * 1024 * 1024, // 4MB buffer
), ),
) { ) {
_mkPlayer.stream.error.listen((event) { _mkPlayer.stream.error.listen((event) {

View File

@ -131,4 +131,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
Future<void> setAudioNormalization(bool normalize) async { Future<void> setAudioNormalization(bool normalize) async {
await _mkPlayer.setAudioNormalization(normalize); await _mkPlayer.setAudioNormalization(normalize);
} }
Future<void> setDemuxerBufferSize(int sizeInBytes) async {
await _mkPlayer.setDemuxerBufferSize(sizeInBytes);
}
} }

View File

@ -133,4 +133,12 @@ class CustomPlayer extends Player {
await nativePlayer.setProperty('af', ''); await nativePlayer.setProperty('af', '');
} }
} }
Future<void> setDemuxerBufferSize(int sizeInBytes) async {
await nativePlayer.setProperty('demuxer-max-bytes', sizeInBytes.toString());
await nativePlayer.setProperty(
'demuxer-max-back-bytes',
sizeInBytes.toString(),
);
}
} }

View File

@ -30,8 +30,12 @@ mixin _$SongLink {
String? get nativeAppUriMobile => throw _privateConstructorUsedError; String? get nativeAppUriMobile => throw _privateConstructorUsedError;
String? get nativeAppUriDesktop => throw _privateConstructorUsedError; String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
/// Serializes this SongLink to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SongLinkCopyWith<SongLink> get copyWith => $SongLinkCopyWith<SongLink> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
@ -63,6 +67,8 @@ class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink>
// ignore: unused_field // ignore: unused_field
final $Res Function($Val) _then; final $Res Function($Val) _then;
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -145,6 +151,8 @@ class __$$SongLinkImplCopyWithImpl<$Res>
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then) _$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
: super(_value, _then); : super(_value, _then);
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
@ -261,12 +269,14 @@ class _$SongLinkImpl implements _SongLink {
other.nativeAppUriDesktop == nativeAppUriDesktop)); other.nativeAppUriDesktop == nativeAppUriDesktop));
} }
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform, int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop); show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
@JsonKey(ignore: true) /// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
@ -313,8 +323,11 @@ abstract class _SongLink implements SongLink {
String? get nativeAppUriMobile; String? get nativeAppUriMobile;
@override @override
String? get nativeAppUriDesktop; String? get nativeAppUriDesktop;
/// Create a copy of SongLink
/// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(ignore: true) @JsonKey(includeFromJson: false, includeToJson: false)
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith => _$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }

View File

@ -2,13 +2,16 @@ import 'package:spotube/models/playback/track_sources.dart';
enum SourceCodecs { enum SourceCodecs {
m4a._("M4a (Best for downloaded music)"), m4a._("M4a (Best for downloaded music)"),
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"),
mp3._("MP3 (Widely supported audio format)"),
flac._("FLAC (Lossless, best quality)\nLarge file size");
final String label; final String label;
const SourceCodecs._(this.label); const SourceCodecs._(this.label);
} }
enum SourceQualities { enum SourceQualities {
uncompressed(3),
high(2), high(2),
medium(1), medium(1),
low(0); low(0);

View File

@ -5,6 +5,7 @@ import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/sources/dab_music.dart';
import 'package:spotube/services/sourced_track/sources/invidious.dart'; import 'package:spotube/services/sourced_track/sources/invidious.dart';
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart';
@ -74,6 +75,14 @@ abstract class SourcedTrack extends BasicSourcedTrack {
query: query, query: query,
sources: sources, sources: sources,
), ),
AudioSource.dabMusic => DABMusicSourcedTrack(
ref: ref,
source: source,
siblings: siblings,
info: info,
query: query,
sources: sources,
),
}; };
} }
@ -104,6 +113,8 @@ abstract class SourcedTrack extends BasicSourcedTrack {
await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref), await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref),
AudioSource.jiosaavn => AudioSource.jiosaavn =>
await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref), await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref),
AudioSource.dabMusic =>
await DABMusicSourcedTrack.fetchFromTrack(query: query, ref: ref),
}; };
} catch (e) { } catch (e) {
if (preferences.audioSource == AudioSource.youtube) { if (preferences.audioSource == AudioSource.youtube) {
@ -129,6 +140,8 @@ abstract class SourcedTrack extends BasicSourcedTrack {
JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref), JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref),
AudioSource.invidious => AudioSource.invidious =>
InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref), InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref),
AudioSource.dabMusic =>
DABMusicSourcedTrack.fetchSiblings(query: query, ref: ref),
}; };
} }
@ -157,7 +170,7 @@ abstract class SourcedTrack extends BasicSourcedTrack {
/// ///
/// If no sources match the codec, it will return the first or last source /// If no sources match the codec, it will return the first or last source
/// based on the user's audio quality preference. /// based on the user's audio quality preference.
String? getUrlOfCodec(SourceCodecs codec) { TrackSource? getSourceOfCodec(SourceCodecs codec) {
final preferences = ref.read(userPreferencesProvider); final preferences = ref.read(userPreferencesProvider);
final exactMatch = sources.firstWhereOrNull( final exactMatch = sources.firstWhereOrNull(
@ -166,7 +179,7 @@ abstract class SourcedTrack extends BasicSourcedTrack {
); );
if (exactMatch != null) { if (exactMatch != null) {
return exactMatch.url; return exactMatch;
} }
final sameCodecSources = sources final sameCodecSources = sources
@ -180,8 +193,8 @@ abstract class SourcedTrack extends BasicSourcedTrack {
if (sameCodecSources.isNotEmpty) { if (sameCodecSources.isNotEmpty) {
return preferences.audioQuality > SourceQualities.low return preferences.audioQuality > SourceQualities.low
? sameCodecSources.first.url ? sameCodecSources.first
: sameCodecSources.last.url; : sameCodecSources.last;
} }
final fallbackSource = sources.sorted((a, b) { final fallbackSource = sources.sorted((a, b) {
@ -191,23 +204,24 @@ abstract class SourcedTrack extends BasicSourcedTrack {
}); });
return preferences.audioQuality > SourceQualities.low return preferences.audioQuality > SourceQualities.low
? fallbackSource.firstOrNull?.url ? fallbackSource.firstOrNull
: fallbackSource.lastOrNull?.url; : fallbackSource.lastOrNull;
}
String? getUrlOfCodec(SourceCodecs codec) {
return getSourceOfCodec(codec)?.url;
} }
SourceCodecs get codec { SourceCodecs get codec {
final preferences = ref.read(userPreferencesProvider); final preferences = ref.read(userPreferencesProvider);
return preferences.audioSource == AudioSource.jiosaavn return switch (preferences.audioSource) {
? SourceCodecs.m4a AudioSource.dabMusic =>
: preferences.streamMusicCodec; preferences.audioQuality == SourceQualities.uncompressed
} ? SourceCodecs.flac
: SourceCodecs.mp3,
TrackSource get activeTrackSource { AudioSource.jiosaavn => SourceCodecs.m4a,
final audioQuality = ref.read(userPreferencesProvider).audioQuality; _ => preferences.streamMusicCodec
return sources.firstWhereOrNull( };
(source) => source.codec == codec && source.quality == audioQuality,
) ??
sources.first;
} }
} }

View File

@ -0,0 +1,225 @@
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:dab_music_api/dab_music_api.dart';
final dabMusicApiClient = DabMusicApiClient(
Dio(),
baseUrl: "https://dab.yeet.su/api",
);
/// Only Music source that can't support database caching due to having no endpoint.
/// But ISRC search is 100% reliable so caching is actually not necessary.
class DABMusicSourcedTrack extends SourcedTrack {
DABMusicSourcedTrack({
required super.ref,
required super.source,
required super.siblings,
required super.info,
required super.query,
required super.sources,
});
static Future<SourcedTrack> fetchFromTrack({
required TrackSourceQuery query,
required Ref ref,
}) async {
try {
final siblings = await fetchSiblings(ref: ref, query: query);
if (siblings.isEmpty) {
throw TrackNotFoundError(query);
}
return DABMusicSourcedTrack(
ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(),
sources: siblings.first.source!,
info: siblings.first.info,
query: query,
source: AudioSource.dabMusic,
);
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
static Future<List<TrackSource>> fetchSources(
String id,
SourceQualities quality,
AudioQuality trackMaximumQuality,
) async {
try {
final isUncompressed = quality == SourceQualities.uncompressed;
final streamResponse = await dabMusicApiClient.music.getStream(
trackId: id,
quality: isUncompressed ? "27" : "5",
);
if (streamResponse.url == null) {
throw Exception("No stream URL found for track ID: $id");
}
// kbps = (bitDepth * sampleRate * channels) / 1000
final uncompressedBitrate = !isUncompressed
? 0
: ((trackMaximumQuality.maximumBitDepth ?? 0) *
((trackMaximumQuality.maximumSamplingRate ?? 0) * 1000) *
2) /
1000;
return [
TrackSource(
url: streamResponse.url!,
quality: isUncompressed
? SourceQualities.uncompressed
: SourceQualities.high,
bitrate:
isUncompressed ? "${uncompressedBitrate.floor()}kbps" : "320kbps",
codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3,
qualityLabel: isUncompressed
? "${trackMaximumQuality.maximumBitDepth}bit • ${trackMaximumQuality.maximumSamplingRate}kHz • FLAC • Stereo"
: "MP3 • 320kbps • mp3 • Stereo",
),
];
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
static Future<SiblingType> toSiblingType(
Ref ref,
int index,
Track result,
) async {
try {
List<TrackSource>? source;
if (index == 0) {
source = await fetchSources(
result.id.toString(),
ref.read(userPreferencesProvider).audioQuality,
result.audioQuality!,
);
}
final SiblingType sibling = (
info: TrackSourceInfo(
artists: result.artist!,
durationMs: Duration(seconds: result.duration!).inMilliseconds,
id: result.id.toString(),
pageUrl: "https://dab.yeet.su/music/${result.id}",
thumbnail: result.albumCover!,
title: result.title!,
),
source: source,
);
return sibling;
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
static Future<List<SiblingType>> fetchSiblings({
required TrackSourceQuery query,
required Ref ref,
}) async {
try {
List<Track> results = [];
if (query.isrc.isNotEmpty) {
final res =
await dabMusicApiClient.music.getSearch(q: query.isrc, limit: 1);
results = res.tracks ?? <Track>[];
}
if (results.isEmpty) {
final res = await dabMusicApiClient.music.getSearch(
q: SourcedTrack.getSearchTerm(query),
limit: 5,
);
results = res.tracks ?? <Track>[];
}
if (results.isEmpty) {
return [];
}
final matchedResults =
results.mapIndexed((index, d) => toSiblingType(ref, index, d));
return Future.wait(matchedResults);
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
@override
Future<DABMusicSourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
return DABMusicSourcedTrack(
ref: ref,
siblings: fetchedSiblings
.where((s) => s.info.id != info.id)
.map((s) => s.info)
.toList(),
source: source,
info: info,
query: query,
sources: sources,
);
}
@override
Future<DABMusicSourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
if (sibling.id == this.info.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, this.info);
final source = await fetchSources(
sibling.id,
ref.read(userPreferencesProvider).audioQuality,
const AudioQuality(
isHiRes: true,
maximumBitDepth: 16,
maximumSamplingRate: 44.1,
),
);
return DABMusicSourcedTrack(
ref: ref,
siblings: newSiblings,
sources: source,
info: newSourceInfo,
query: query,
source: AudioSource.dabMusic,
);
}
@override
Future<SourcedTrack> refreshStream() async {
// There's no need to refresh the stream for DABMusicSourcedTrack
return this;
}
}

View File

@ -94,6 +94,7 @@ class InvidiousSourcedTrack extends SourcedTrack {
static List<TrackSource> toSources(InvidiousVideoResponse manifest) { static List<TrackSource> toSources(InvidiousVideoResponse manifest) {
return manifest.adaptiveFormats.map((stream) { return manifest.adaptiveFormats.map((stream) {
var isWebm = stream.type.contains("audio/webm");
return TrackSource( return TrackSource(
url: stream.url.toString(), url: stream.url.toString(),
quality: switch (stream.qualityLabel) { quality: switch (stream.qualityLabel) {
@ -101,10 +102,11 @@ class InvidiousSourcedTrack extends SourcedTrack {
"medium" => SourceQualities.medium, "medium" => SourceQualities.medium,
_ => SourceQualities.low, _ => SourceQualities.low,
}, },
codec: stream.type.contains("audio/webm") codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a,
? SourceCodecs.weba
: SourceCodecs.m4a,
bitrate: stream.bitrate, bitrate: stream.bitrate,
qualityLabel:
"${isWebm ? "Opus" : "AAC"}${stream.bitrate.replaceAll("kbps", "")}kbps "
"${isWebm ? "weba" : "m4a"} • Stereo",
); );
}).toList(); }).toList();
} }

View File

@ -104,6 +104,7 @@ class JioSaavnSourcedTrack extends SourcedTrack {
: SourceQualities.low, : SourceQualities.low,
codec: SourceCodecs.m4a, codec: SourceCodecs.m4a,
bitrate: link.quality, bitrate: link.quality,
qualityLabel: "AAC • ${link.quality} • MP4 • Stereo",
); );
}).toList() }).toList()
); );

View File

@ -98,6 +98,7 @@ class PipedSourcedTrack extends SourcedTrack {
static List<TrackSource> toSources(PipedStreamResponse manifest) { static List<TrackSource> toSources(PipedStreamResponse manifest) {
return manifest.audioStreams.map((audio) { return manifest.audioStreams.map((audio) {
final isMp4 = audio.format == PipedAudioStreamFormat.m4a;
return TrackSource( return TrackSource(
url: audio.url.toString(), url: audio.url.toString(),
quality: switch (audio.quality) { quality: switch (audio.quality) {
@ -105,10 +106,11 @@ class PipedSourcedTrack extends SourcedTrack {
"medium" => SourceQualities.medium, "medium" => SourceQualities.medium,
_ => SourceQualities.low, _ => SourceQualities.low,
}, },
codec: audio.format == PipedAudioStreamFormat.m4a codec: isMp4 ? SourceCodecs.m4a : SourceCodecs.weba,
? SourceCodecs.m4a
: SourceCodecs.weba,
bitrate: audio.bitrate.toString(), bitrate: audio.bitrate.toString(),
qualityLabel:
"${isMp4 ? "AAC" : "Opus"}${(audio.bitrate / 1000).floor()}kbps "
"${isMp4 ? "m4a" : "weba"} • Stereo",
); );
}).toList(); }).toList();
} }

View File

@ -98,6 +98,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
static List<TrackSource> toTrackSources(StreamManifest manifest) { static List<TrackSource> toTrackSources(StreamManifest manifest) {
return manifest.audioOnly.map((streamInfo) { return manifest.audioOnly.map((streamInfo) {
var isWebm = streamInfo.codec.mimeType == "audio/webm";
return TrackSource( return TrackSource(
url: streamInfo.url.toString(), url: streamInfo.url.toString(),
quality: switch (streamInfo.qualityLabel) { quality: switch (streamInfo.qualityLabel) {
@ -106,10 +107,11 @@ class YoutubeSourcedTrack extends SourcedTrack {
"low" => SourceQualities.low, "low" => SourceQualities.low,
_ => SourceQualities.high, _ => SourceQualities.high,
}, },
codec: streamInfo.codec.mimeType == "audio/webm" codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a,
? SourceCodecs.weba
: SourceCodecs.m4a,
bitrate: streamInfo.bitrate.bitsPerSecond.toString(), bitrate: streamInfo.bitrate.bitsPerSecond.toString(),
qualityLabel:
"${isWebm ? "Opus" : "AAC"}${(streamInfo.bitrate.kiloBitsPerSecond).floor()}kbps "
"${isWebm ? "weba" : "m4a"} • Stereo",
); );
}).toList(); }).toList();
} }

View File

@ -315,7 +315,7 @@ packages:
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
change_case: change_case:
dependency: transitive dependency: "direct main"
description: description:
name: change_case name: change_case
sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157
@ -458,6 +458,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
dab_music_api:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "55f96368b7465eec2e5e81774f9f2a7b18acc4ab"
url: "https://github.com/KRTirtho/dab_music_api.git"
source: git
version: "0.1.0"
dart_des: dart_des:
dependency: transitive dependency: transitive
description: description:
@ -2046,6 +2055,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
retrofit:
dependency: transitive
description:
name: retrofit
sha256: "699cf44ec6c7fc7d248740932eca75d334e36bdafe0a8b3e9ff93100591c8a25"
url: "https://pub.dev"
source: hosted
version: "4.7.2"
riverpod: riverpod:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -24,6 +24,10 @@ dependencies:
bonsoir: ^5.1.10 bonsoir: ^5.1.10
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
connectivity_plus: ^6.1.2 connectivity_plus: ^6.1.2
dab_music_api:
git:
url: https://github.com/KRTirtho/dab_music_api.git
ref: main
desktop_webview_window: desktop_webview_window:
git: git:
path: packages/desktop_webview_window path: packages/desktop_webview_window
@ -163,6 +167,7 @@ dependencies:
get_it: ^8.0.3 get_it: ^8.0.3
flutter_markdown_plus: ^1.0.3 flutter_markdown_plus: ^1.0.3
pub_semver: ^2.2.0 pub_semver: ^2.2.0
change_case: ^1.1.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13

View File

@ -1,5 +1,176 @@
{ {
"ar": [
"source",
"uncompressed",
"dab_music_source_description"
],
"bn": [
"source",
"uncompressed",
"dab_music_source_description"
],
"ca": [
"source",
"uncompressed",
"dab_music_source_description"
],
"cs": [
"source",
"uncompressed",
"dab_music_source_description"
],
"de": [
"source",
"uncompressed",
"dab_music_source_description"
],
"es": [
"source",
"uncompressed",
"dab_music_source_description"
],
"eu": [
"source",
"uncompressed",
"dab_music_source_description"
],
"fa": [
"source",
"uncompressed",
"dab_music_source_description"
],
"fi": [
"source",
"uncompressed",
"dab_music_source_description"
],
"fr": [
"source",
"uncompressed",
"dab_music_source_description"
],
"hi": [
"source",
"uncompressed",
"dab_music_source_description"
],
"id": [
"source",
"uncompressed",
"dab_music_source_description"
],
"it": [
"source",
"uncompressed",
"dab_music_source_description"
],
"ja": [
"source",
"uncompressed",
"dab_music_source_description"
],
"ka": [
"source",
"uncompressed",
"dab_music_source_description"
],
"ko": [
"source",
"uncompressed",
"dab_music_source_description"
],
"ne": [
"source",
"uncompressed",
"dab_music_source_description"
],
"nl": [ "nl": [
"audio_source" "audio_source",
"source",
"uncompressed",
"dab_music_source_description"
],
"pl": [
"source",
"uncompressed",
"dab_music_source_description"
],
"pt": [
"source",
"uncompressed",
"dab_music_source_description"
],
"ru": [
"source",
"uncompressed",
"dab_music_source_description"
],
"ta": [
"source",
"uncompressed",
"dab_music_source_description"
],
"th": [
"source",
"uncompressed",
"dab_music_source_description"
],
"tl": [
"source",
"uncompressed",
"dab_music_source_description"
],
"tr": [
"source",
"uncompressed",
"dab_music_source_description"
],
"uk": [
"source",
"uncompressed",
"dab_music_source_description"
],
"vi": [
"source",
"uncompressed",
"dab_music_source_description"
],
"zh": [
"source",
"uncompressed",
"dab_music_source_description"
],
"zh_TW": [
"source",
"uncompressed",
"dab_music_source_description"
] ]
} }

View File

@ -17,7 +17,7 @@
"@types/react": "^19.1.9", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
"astro": "^5.12.8", "astro": "^5.12.8",
"astro-pagefind": "^1.8.3", "astro-pagefind": "1.8.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"react": "^19.1.1", "react": "^19.1.1",

View File

@ -33,7 +33,7 @@ importers:
specifier: ^5.12.8 specifier: ^5.12.8
version: 5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2) version: 5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2)
astro-pagefind: astro-pagefind:
specifier: ^1.8.3 specifier: 1.8.3
version: 1.8.3(astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2)) version: 1.8.3(astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2))
date-fns: date-fns:
specifier: ^4.1.0 specifier: ^4.1.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -1,349 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 762 762"
version="1.1"
id="svg270"
sodipodi:docname="spotube-logo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xml:space="preserve"
inkscape:export-filename="spotube-logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
width="762"
height="762"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:bx="https://boxy-svg.com"><sodipodi:namedview
id="namedview272"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.76199998"
inkscape:cx="194.22573"
inkscape:cy="314.96064"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg270"
inkscape:lockguides="false"><inkscape:page
x="0"
y="0"
width="762"
height="762"
id="page3136" /><inkscape:page
x="640.44641"
y="132.29141"
width="89.999939"
height="89.999985"
id="page3138" /></sodipodi:namedview><defs
id="defs220"><linearGradient
inkscape:collect="always"
id="linearGradient5535"><stop
style="stop-color:#00063b;stop-opacity:1;"
offset="0.25885531"
id="stop5531" /><stop
style="stop-color:#004256;stop-opacity:1;"
offset="1"
id="stop5533" /></linearGradient><linearGradient
id="linearGradient2809"><stop
offset="0.113"
style="stop-color:#5668ea;stop-opacity:1;"
id="stop2803" /><stop
offset="0.60799998"
style="stop-color:#0093b1;stop-opacity:1;"
id="stop2805" /><stop
offset="0.94400001"
style="stop-color:#00a29f;stop-opacity:1;"
id="stop2807" /></linearGradient><linearGradient
id="linearGradient938"><stop
offset="0.113"
style="stop-color:#5869eb;stop-opacity:1;"
id="stop932" /><stop
offset="0.60799998"
style="stop-color:#0093b1;stop-opacity:1;"
id="stop934" /><stop
offset="0.94400001"
style="stop-color:#02a7a4;stop-opacity:1;"
id="stop936" /></linearGradient><radialGradient
id="gradient-2-0"
gradientUnits="userSpaceOnUse"
cx="251.179"
cy="248.821"
r="241.45"
gradientTransform="translate(-1.768285,0.589104)"
xlink:href="#gradient-2" /><linearGradient
id="gradient-2"><stop
offset="0.841"
style="stop-color: rgb(255, 255, 255);"
id="stop169" /><stop
offset="1"
style="stop-color: rgb(201, 201, 201);"
id="stop171" /></linearGradient><filter
id="drop-shadow-filter-0"
x="-0.050892502"
y="-0.050892502"
width="1.1017849"
height="1.1017849"
bx:preset="drop-shadow 1 0 0 10 0.42 rgba(201,201,201,1)"><feGaussianBlur
in="SourceAlpha"
stdDeviation="10"
id="feGaussianBlur174" /><feOffset
dx="0"
dy="0"
id="feOffset176" /><feComponentTransfer
result="offsetblur"
id="feComponentTransfer179"><feFuncA
id="spread-ctrl"
type="linear"
slope="0.84" /></feComponentTransfer><feFlood
flood-color="rgba(201,201,201,1)"
id="feFlood181" /><feComposite
in2="offsetblur"
operator="in"
id="feComposite183" /><feMerge
id="feMerge189"><feMergeNode
id="feMergeNode185" /><feMergeNode
in="SourceGraphic"
id="feMergeNode187" /></feMerge></filter><linearGradient
id="gradient-4-3"
gradientUnits="userSpaceOnUse"
x1="47.146"
y1="18.044001"
x2="47.146"
y2="75.353996"
xlink:href="#gradient-4" /><linearGradient
id="gradient-4"><stop
offset="0.113"
style="stop-color: rgb(83, 240, 111);"
id="stop193" /><stop
offset="0.608"
style="stop-color: rgb(0, 177, 86);"
id="stop195" /><stop
offset="0.944"
style="stop-color: rgb(2, 167, 156);"
id="stop197" /></linearGradient><filter
id="inner-shadow-filter-0"
x="-0.064836091"
y="-0.071329232"
width="1.1296722"
height="1.108079"
bx:preset="inner-shadow 1 0 0 4 0.5 rgba(0,0,0,0.7)"><feOffset
dx="0"
dy="0"
id="feOffset200" /><feGaussianBlur
stdDeviation="4"
id="feGaussianBlur202"
result="result1" /><feComposite
operator="out"
in="SourceGraphic"
in2="result1"
id="feComposite204" /><feComponentTransfer
result="choke"
id="feComponentTransfer208"><feFuncA
type="linear"
slope="1"
id="feFuncA206" /></feComponentTransfer><feFlood
flood-color="rgba(0,0,0,0.7)"
result="color"
id="feFlood210" /><feComposite
operator="in"
in="color"
in2="choke"
result="shadow"
id="feComposite212" /><feComposite
operator="over"
in="shadow"
in2="SourceGraphic"
id="feComposite214" /></filter><linearGradient
id="gradient-4-1"
gradientUnits="userSpaceOnUse"
x1="82.026001"
y1="144.832"
x2="82.026001"
y2="264.46201"
xlink:href="#linearGradient2809"
gradientTransform="translate(7.2213312)" /><linearGradient
id="gradient-4-2"
gradientUnits="userSpaceOnUse"
x1="143.69299"
y1="22.804001"
x2="143.69299"
y2="264.582"
xlink:href="#linearGradient938" /><linearGradient
id="gradient-4-0"
gradientUnits="userSpaceOnUse"
x1="205.862"
y1="146.28"
x2="205.862"
y2="265.91"
xlink:href="#gradient-4"
gradientTransform="translate(-7.2213312)" /><filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter2000"
x="-0.3425389"
y="-0.3425389"
width="1.6850778"
height="1.6850778"><feFlood
flood-opacity="1"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1990" /><feComposite
in="flood"
in2="SourceGraphic"
operator="out"
result="composite1"
id="feComposite1992" /><feGaussianBlur
in="composite1"
stdDeviation="29.980818"
result="blur"
id="feGaussianBlur1994" /><feOffset
dx="0"
dy="0"
result="offset"
id="feOffset1996" /><feComposite
in="offset"
in2="SourceGraphic"
operator="atop"
result="fbSourceGraphic"
id="feComposite1998" /><feColorMatrix
result="fbSourceGraphicAlpha"
in="fbSourceGraphic"
values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
id="feColorMatrix2062" /><feFlood
id="feFlood2064"
flood-opacity="1"
flood-color="rgb(0,0,0)"
result="flood"
in="fbSourceGraphic" /><feComposite
in2="fbSourceGraphic"
id="feComposite2066"
in="flood"
operator="out"
result="composite1" /><feGaussianBlur
id="feGaussianBlur2068"
in="composite1"
stdDeviation="28.6433"
result="blur" /><feOffset
id="feOffset2070"
dx="0"
dy="0"
result="offset" /><feComposite
in2="fbSourceGraphic"
id="feComposite2072"
in="offset"
operator="atop"
result="fbSourceGraphic" /><feColorMatrix
result="fbSourceGraphicAlpha"
in="fbSourceGraphic"
values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
id="feColorMatrix3393" /><feFlood
id="feFlood3395"
flood-opacity="0.352941"
flood-color="rgb(0,0,0)"
result="flood"
in="fbSourceGraphic" /><feComposite
in2="fbSourceGraphic"
id="feComposite3397"
in="flood"
operator="in"
result="composite1" /><feGaussianBlur
id="feGaussianBlur3399"
in="composite1"
stdDeviation="6.59891"
result="blur" /><feOffset
id="feOffset3401"
dx="0"
dy="0"
result="offset" /><feComposite
in2="offset"
id="feComposite3403"
in="fbSourceGraphic"
operator="over"
result="composite2" /></filter><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2809"
id="linearGradient5506"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(117.34662)"
x1="82.026001"
y1="144.832"
x2="82.026001"
y2="264.46201" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5535"
id="radialGradient5537"
cx="143.6935"
cy="143.69299"
fx="143.6935"
fy="143.69299"
r="152.72653"
gradientTransform="matrix(1,0,0,0.8506841,0,21.45565)"
gradientUnits="userSpaceOnUse" /></defs><circle
style="opacity:1;fill:#242832;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-dasharray:none;stroke-opacity:0.961795;filter:url(#filter2000)"
id="path1157"
cx="381.48901"
cy="381.48901"
inkscape:label="path1157"
r="235.79112"
sodipodi:insensitive="true" /><g
transform="matrix(0.319972,0,0,0.323174,379.08153,437.03375)"
id="g228"><g
style="opacity:1;fill:none;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none"
transform="matrix(3.89,0,0,3.89,-175.05,-175.05)"
id="g226" /></g><g
id="g236"
style="fill:none;filter:url(#inner-shadow-filter-0)"
transform="matrix(1.107829,0,0,1.106267,221.95533,199.03714)"><path
d="m 78.642332,155.437 v 98.42 c 0,5.867 4.741,10.605 10.605,10.605 5.854,0 10.604995,-4.738 10.604995,-10.605 v -98.42 c 0,-5.856 -4.750995,-10.605 -10.604995,-10.605 -5.864,0 -10.605,4.744 -10.605,10.605 z"
style="fill:none;fill-opacity:1;stroke:url(#gradient-4-1);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path230" /><path
d="m 29.456,264.582 h 23.351 v -116.85 c 0.064,-0.56 0.166,-1.119 0.166,-1.693 0,-50.412 40.69,-91.42 90.698,-91.42 50.002,0 90.692,41.008 90.692,91.42 0,0.771 0.113,1.518 0.228,2.263 v 116.28 h 23.354 c 16.254,0 29.442,-13.64 29.442,-30.469 v -60.936 c 0,-13.878 -8.989,-25.57 -21.261,-29.249 C 264.997,76.957 210.518,22.804 143.676,22.804 76.816,22.804 22.329,76.962 21.211,143.954 8.956,147.638 0,159.32 0,173.187 v 60.926 c 0,16.819 13.187,30.469 29.456,30.469 z"
style="fill:url(#radialGradient5537);fill-opacity:1;stroke:url(#gradient-4-2);stroke-width:18.0661;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="path232" /><path
d="M 49.735541,279.35822 C 23.7214,267.48486 38.122112,248.62719 80.85964,237.45225 c 14.400662,-3.49216 25.08508,-5.12184 43.66659,-4.88901 11.61348,0.23282 24.62053,3.49216 24.62053,3.49216 0,-42.13877 -0.46471,-121.7601 -0.46471,-160.872338 4.6454,0 7.89719,-0.232827 14.40071,-0.232827 0,2.328107 0,4.190613 0,6.053093 0,2.095305 0,3.259358 0.46471,4.656212 4.6454,14.66709 11.14893,20.48736 43.66659,38.41381 41.34392,23.04827 53.42195,36.78411 53.42195,55.17616 -0.46471,17.22802 -30.65954,54.01213 -37.16306,52.61528 9.29075,-13.03741 22.2978,-27.00606 25.54958,-38.64661 4.18085,-14.20147 -7.43263,-34.2232 -26.01414,-44.69971 -14.86522,-8.8468 -50.17016,-16.52957 -59.92547,-16.52957 0,0 -0.46472,84.74317 -0.46472,116.87109 0,5.35464 -9.7553,14.89989 -15.32977,18.15925 -25.54958,15.36551 -75.25519,22.34984 -97.553043,12.33896 z"
id="path3079"
style="stroke-width:3.28861" /><path
d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z"
style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path5502" /></g><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g240" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g242" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g244" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g246" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g248" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g250" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g252" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g254" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g256" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g258" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g260" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g262" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g264" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g266" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g268" /></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -51,16 +51,21 @@ export const extendedDownloadLinks: Record<
"exe", "exe",
], ],
macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"], macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"],
"Ubuntu, Debian": [ "Ubuntu, Debian (x64)": [
`${releasesUrl}/Spotube-linux-x86_64.deb`, `${releasesUrl}/Spotube-linux-x86_64.deb`,
[FaUbuntu, FaDebian], [FaUbuntu, FaDebian],
"deb", "deb",
], ],
"Fedora, Redhat, Opensuse": [ "Ubuntu, Debian (arm64)": [
`${releasesUrl}/Spotube-linux-x86_64.rpm`, `${releasesUrl}/Spotube-linux-aarch64.deb`,
[FaFedora, FaRedhat, FaOpensuse], [FaUbuntu, FaDebian],
"rpm", "deb",
], ],
// "Fedora, Redhat, Opensuse": [
// `${releasesUrl}/Spotube-linux-x86_64.rpm`,
// [FaFedora, FaRedhat, FaOpensuse],
// "rpm",
// ],
iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"], iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"],
}; };

View File

@ -12,7 +12,7 @@ const {
adSlot, adSlot,
adFormat, adFormat,
fullWidthResponsive = true, fullWidthResponsive = true,
style, style = "display:block",
adLayout, adLayout,
adLayoutKey, adLayoutKey,
} = Astro.props; } = Astro.props;
@ -22,7 +22,7 @@ const AD_CLIENT = "ca-pub-6419300932495863";
<ins <ins
class="adsbygoogle" class="adsbygoogle"
{style} style={style}
data-ad-layout={adLayout} data-ad-layout={adLayout}
data-ad-client={AD_CLIENT} data-ad-client={AD_CLIENT}
data-ad-slot={adSlot} data-ad-slot={adSlot}

View File

@ -22,34 +22,7 @@ const otherDownloads: [string, string, IconType][] = [
<br /><br /> <br /><br />
<h5 class="h5">Spotube is available for every platform</h5> <h5 class="h5">Spotube is available for every platform</h5>
<br /> <br />
<!-- WARNING! --> <DownloadItems links={extendedDownloadLinks} />
<h3 class="h3 text-red-500" data-svelte-h="svelte-1l4b696">
Versions of Spotube (&lt;=v4.0.2) are ceased to work with Spotify™ API.
<br />
So users can no longer use/download those versions.
<br />
Please wait for the next version that will remedy this issue by not using such
APIs.
</h3>
<p class="text-surface-500 mt-5" data-svelte-h="svelte-1nkw9cu">
Spotube has no affiliation with Spotify™ or any of its subsidiaries.
</p>
<br />
<br />
<!-- <DownloadItems links={extendedDownloadLinks} /> -->
<h6 class="h6 mb-5" data-svelte-h="svelte-1ws2638">
The new Spotube v5 is still under beta. Please use the Nightly version
until stable release.
</h6>
<!-- WARNING! -->
<div class="flex">
<a href="/downloads/nightly" class="flex gap-2 btn btn-lg preset-filled">
<LuDownload />
Download Nightly
</a>
</div>
<br />
<br /> <br />
<Ads adSlot={ADS_SLOTS.downloadPageDisplay} adFormat="auto" /> <Ads adSlot={ADS_SLOTS.downloadPageDisplay} adFormat="auto" />
<br /> <br />

View File

@ -53,11 +53,11 @@ import { ADS_SLOTS } from "~/collections/app";
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<a <a
href="/downloads/nightly" href="/downloads"
class="flex gap-2 btn btn-lg preset-filled" class="flex gap-2 btn btn-lg preset-filled"
> >
<LuDownload /> <LuDownload />
Download Nightly Download
</a> </a>
</div> </div>
</div> </div>