mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 15:02:42 +01:00
reboot
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../state/init.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
part 'app.g.dart';
|
||||
|
||||
class MyApp extends HookConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final init = ref.watch(initProvider);
|
||||
return init.when(
|
||||
data: (_) => const App(),
|
||||
error: (e, s) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
color: Colors.red[900],
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 100),
|
||||
Text(e.toString()),
|
||||
Text(s.toString()),
|
||||
]),
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LastPath extends _$LastPath {
|
||||
@override
|
||||
String build() {
|
||||
return '/settings';
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final lastBottomNav = await db.getLastBottomNavState().getSingleOrNull();
|
||||
final lastLibrary = await db.getLastLibraryState().getSingleOrNull();
|
||||
|
||||
if (lastBottomNav == null || lastLibrary == null) return;
|
||||
|
||||
// TODO: replace this with a proper first-time setup flow
|
||||
final hasActiveSource = ref.read(settingsServiceProvider.select(
|
||||
(value) => value.activeSource != null,
|
||||
));
|
||||
if (!hasActiveSource) return;
|
||||
|
||||
state = lastBottomNav.tab == 'library'
|
||||
? '/library/${lastLibrary.tab}'
|
||||
: '/${lastBottomNav.tab}';
|
||||
}
|
||||
}
|
||||
|
||||
class App extends HookConsumerWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appRouter = ref.watch(routerProvider);
|
||||
final base = ref.watch(baseThemeProvider);
|
||||
final lastPath = ref.watch(lastPathProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
theme: base.theme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerDelegate: appRouter.delegate(
|
||||
initialDeepLink: lastPath,
|
||||
),
|
||||
routeInformationParser: appRouter.defaultRouteParser(),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: [...AppLocalizations.supportedLocales]
|
||||
..moveToTheFront(const Locale('en')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$lastPathHash() => r'25ba5df6bd984fcce011eec40a12fb74627a790a';
|
||||
|
||||
/// See also [LastPath].
|
||||
@ProviderFor(LastPath)
|
||||
final lastPathProvider = NotifierProvider<LastPath, String>.internal(
|
||||
LastPath.new,
|
||||
name: r'lastPathProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$lastPathHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LastPath = Notifier<String>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,140 +0,0 @@
|
||||
// ignore_for_file: use_key_in_widget_constructors
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'pages/artist_page.dart';
|
||||
import 'pages/bottom_nav_page.dart';
|
||||
import 'pages/browse_page.dart';
|
||||
import 'pages/library_albums_page.dart';
|
||||
import 'pages/library_artists_page.dart';
|
||||
import 'pages/library_page.dart';
|
||||
import 'pages/library_playlists_page.dart';
|
||||
import 'pages/library_songs_page.dart';
|
||||
import 'pages/now_playing_page.dart';
|
||||
import 'pages/search_page.dart';
|
||||
import 'pages/settings_page.dart';
|
||||
import 'pages/songs_page.dart';
|
||||
import 'pages/source_page.dart';
|
||||
|
||||
part 'app_router.gr.dart';
|
||||
|
||||
const kCustomTransitionBuilder = TransitionsBuilders.slideRightWithFade;
|
||||
const kCustomTransitionDuration = 160;
|
||||
|
||||
const itemRoutes = [
|
||||
CustomRoute(
|
||||
path: 'album/:id',
|
||||
page: AlbumSongsPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
CustomRoute(
|
||||
path: 'artist/:id',
|
||||
page: ArtistPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
CustomRoute(
|
||||
path: 'playlist/:id',
|
||||
page: PlaylistSongsPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
CustomRoute(
|
||||
path: 'genre/:genre',
|
||||
page: GenreSongsPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
];
|
||||
|
||||
class EmptyRouterPage extends AutoRouter {
|
||||
const EmptyRouterPage({Key? key})
|
||||
: super(
|
||||
key: key,
|
||||
inheritNavigatorObservers: false,
|
||||
);
|
||||
}
|
||||
|
||||
@MaterialAutoRouter(
|
||||
replaceInRouteName: 'Page,Route',
|
||||
routes: <AutoRoute>[
|
||||
AutoRoute(path: '/', name: 'RootRouter', page: EmptyRouterPage, children: [
|
||||
AutoRoute(path: '', page: BottomNavTabsPage, children: [
|
||||
AutoRoute(
|
||||
path: 'library',
|
||||
name: 'LibraryRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: LibraryTabsPage, children: [
|
||||
AutoRoute(path: 'albums', page: LibraryAlbumsPage),
|
||||
AutoRoute(path: 'artists', page: LibraryArtistsPage),
|
||||
AutoRoute(path: 'playlists', page: LibraryPlaylistsPage),
|
||||
AutoRoute(path: 'songs', page: LibrarySongsPage),
|
||||
]),
|
||||
...itemRoutes,
|
||||
]),
|
||||
AutoRoute(
|
||||
path: 'browse',
|
||||
name: 'BrowseRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: BrowsePage),
|
||||
...itemRoutes,
|
||||
]),
|
||||
AutoRoute(
|
||||
path: 'search',
|
||||
name: 'SearchRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: SearchPage),
|
||||
...itemRoutes,
|
||||
]),
|
||||
AutoRoute(
|
||||
path: 'settings',
|
||||
name: 'SettingsRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: SettingsPage),
|
||||
CustomRoute(
|
||||
path: 'source/:id',
|
||||
page: SourcePage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
CustomRoute(
|
||||
path: '/now-playing',
|
||||
page: NowPlayingPage,
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
durationInMilliseconds: 200,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
),
|
||||
],
|
||||
)
|
||||
class AppRouter extends _$AppRouter {}
|
||||
|
||||
class TabObserver extends AutoRouterObserver {
|
||||
final StreamController<String> _controller = StreamController.broadcast();
|
||||
Stream<String> get path => _controller.stream;
|
||||
|
||||
@override
|
||||
void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {
|
||||
_controller.add(route.path);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) {
|
||||
_controller.add(route.path);
|
||||
}
|
||||
}
|
||||
@@ -1,720 +0,0 @@
|
||||
// **************************************************************************
|
||||
// AutoRouteGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
// **************************************************************************
|
||||
// AutoRouteGenerator
|
||||
// **************************************************************************
|
||||
//
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
part of 'app_router.dart';
|
||||
|
||||
class _$AppRouter extends RootStackRouter {
|
||||
_$AppRouter([GlobalKey<NavigatorState>? navigatorKey]) : super(navigatorKey);
|
||||
|
||||
@override
|
||||
final Map<String, PageFactory> pagesMap = {
|
||||
RootRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
NowPlayingRoute.name: (routeData) {
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const NowPlayingPage(),
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
durationInMilliseconds: 200,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
BottomNavTabsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const BottomNavTabsPage(),
|
||||
);
|
||||
},
|
||||
LibraryRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
BrowseRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
SearchRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
SettingsRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
LibraryTabsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryTabsPage(),
|
||||
);
|
||||
},
|
||||
AlbumSongsRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<AlbumSongsRouteArgs>(
|
||||
orElse: () => AlbumSongsRouteArgs(id: pathParams.getString('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: AlbumSongsPage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
ArtistRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<ArtistRouteArgs>(
|
||||
orElse: () => ArtistRouteArgs(id: pathParams.getString('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: ArtistPage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
PlaylistSongsRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<PlaylistSongsRouteArgs>(
|
||||
orElse: () => PlaylistSongsRouteArgs(id: pathParams.getString('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: PlaylistSongsPage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
GenreSongsRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<GenreSongsRouteArgs>(
|
||||
orElse: () =>
|
||||
GenreSongsRouteArgs(genre: pathParams.getString('genre')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: GenreSongsPage(
|
||||
key: args.key,
|
||||
genre: args.genre,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
LibraryAlbumsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryAlbumsPage(),
|
||||
);
|
||||
},
|
||||
LibraryArtistsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryArtistsPage(),
|
||||
);
|
||||
},
|
||||
LibraryPlaylistsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryPlaylistsPage(),
|
||||
);
|
||||
},
|
||||
LibrarySongsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibrarySongsPage(),
|
||||
);
|
||||
},
|
||||
BrowseRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const BrowsePage(),
|
||||
);
|
||||
},
|
||||
SearchRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const SearchPage(),
|
||||
);
|
||||
},
|
||||
SettingsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const SettingsPage(),
|
||||
);
|
||||
},
|
||||
SourceRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<SourceRouteArgs>(
|
||||
orElse: () => SourceRouteArgs(id: pathParams.optInt('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: SourcePage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@override
|
||||
List<RouteConfig> get routes => [
|
||||
RouteConfig(
|
||||
RootRouter.name,
|
||||
path: '/',
|
||||
children: [
|
||||
RouteConfig(
|
||||
BottomNavTabsRoute.name,
|
||||
path: '',
|
||||
parent: RootRouter.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
LibraryRouter.name,
|
||||
path: 'library',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
LibraryTabsRoute.name,
|
||||
path: '',
|
||||
parent: LibraryRouter.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
LibraryAlbumsRoute.name,
|
||||
path: 'albums',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
RouteConfig(
|
||||
LibraryArtistsRoute.name,
|
||||
path: 'artists',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
RouteConfig(
|
||||
LibraryPlaylistsRoute.name,
|
||||
path: 'playlists',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
RouteConfig(
|
||||
LibrarySongsRoute.name,
|
||||
path: 'songs',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
BrowseRouter.name,
|
||||
path: 'browse',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
BrowseRoute.name,
|
||||
path: '',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
SearchRouter.name,
|
||||
path: 'search',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
SearchRoute.name,
|
||||
path: '',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
SettingsRouter.name,
|
||||
path: 'settings',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
SettingsRoute.name,
|
||||
path: '',
|
||||
parent: SettingsRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
SourceRoute.name,
|
||||
path: 'source/:id',
|
||||
parent: SettingsRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
NowPlayingRoute.name,
|
||||
path: '/now-playing',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class RootRouter extends PageRouteInfo<void> {
|
||||
const RootRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
RootRouter.name,
|
||||
path: '/',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'RootRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [NowPlayingPage]
|
||||
class NowPlayingRoute extends PageRouteInfo<void> {
|
||||
const NowPlayingRoute()
|
||||
: super(
|
||||
NowPlayingRoute.name,
|
||||
path: '/now-playing',
|
||||
);
|
||||
|
||||
static const String name = 'NowPlayingRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [BottomNavTabsPage]
|
||||
class BottomNavTabsRoute extends PageRouteInfo<void> {
|
||||
const BottomNavTabsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
BottomNavTabsRoute.name,
|
||||
path: '',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'BottomNavTabsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class LibraryRouter extends PageRouteInfo<void> {
|
||||
const LibraryRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
LibraryRouter.name,
|
||||
path: 'library',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'LibraryRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class BrowseRouter extends PageRouteInfo<void> {
|
||||
const BrowseRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
BrowseRouter.name,
|
||||
path: 'browse',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'BrowseRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class SearchRouter extends PageRouteInfo<void> {
|
||||
const SearchRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
SearchRouter.name,
|
||||
path: 'search',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SearchRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class SettingsRouter extends PageRouteInfo<void> {
|
||||
const SettingsRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
SettingsRouter.name,
|
||||
path: 'settings',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SettingsRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryTabsPage]
|
||||
class LibraryTabsRoute extends PageRouteInfo<void> {
|
||||
const LibraryTabsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
LibraryTabsRoute.name,
|
||||
path: '',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'LibraryTabsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumSongsPage]
|
||||
class AlbumSongsRoute extends PageRouteInfo<AlbumSongsRouteArgs> {
|
||||
AlbumSongsRoute({
|
||||
Key? key,
|
||||
required String id,
|
||||
}) : super(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
args: AlbumSongsRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'AlbumSongsRoute';
|
||||
}
|
||||
|
||||
class AlbumSongsRouteArgs {
|
||||
const AlbumSongsRouteArgs({
|
||||
this.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumSongsRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [ArtistPage]
|
||||
class ArtistRoute extends PageRouteInfo<ArtistRouteArgs> {
|
||||
ArtistRoute({
|
||||
Key? key,
|
||||
required String id,
|
||||
}) : super(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
args: ArtistRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'ArtistRoute';
|
||||
}
|
||||
|
||||
class ArtistRouteArgs {
|
||||
const ArtistRouteArgs({
|
||||
this.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArtistRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PlaylistSongsPage]
|
||||
class PlaylistSongsRoute extends PageRouteInfo<PlaylistSongsRouteArgs> {
|
||||
PlaylistSongsRoute({
|
||||
Key? key,
|
||||
required String id,
|
||||
}) : super(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
args: PlaylistSongsRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'PlaylistSongsRoute';
|
||||
}
|
||||
|
||||
class PlaylistSongsRouteArgs {
|
||||
const PlaylistSongsRouteArgs({
|
||||
this.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaylistSongsRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [GenreSongsPage]
|
||||
class GenreSongsRoute extends PageRouteInfo<GenreSongsRouteArgs> {
|
||||
GenreSongsRoute({
|
||||
Key? key,
|
||||
required String genre,
|
||||
}) : super(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
args: GenreSongsRouteArgs(
|
||||
key: key,
|
||||
genre: genre,
|
||||
),
|
||||
rawPathParams: {'genre': genre},
|
||||
);
|
||||
|
||||
static const String name = 'GenreSongsRoute';
|
||||
}
|
||||
|
||||
class GenreSongsRouteArgs {
|
||||
const GenreSongsRouteArgs({
|
||||
this.key,
|
||||
required this.genre,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String genre;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GenreSongsRouteArgs{key: $key, genre: $genre}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryAlbumsPage]
|
||||
class LibraryAlbumsRoute extends PageRouteInfo<void> {
|
||||
const LibraryAlbumsRoute()
|
||||
: super(
|
||||
LibraryAlbumsRoute.name,
|
||||
path: 'albums',
|
||||
);
|
||||
|
||||
static const String name = 'LibraryAlbumsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryArtistsPage]
|
||||
class LibraryArtistsRoute extends PageRouteInfo<void> {
|
||||
const LibraryArtistsRoute()
|
||||
: super(
|
||||
LibraryArtistsRoute.name,
|
||||
path: 'artists',
|
||||
);
|
||||
|
||||
static const String name = 'LibraryArtistsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryPlaylistsPage]
|
||||
class LibraryPlaylistsRoute extends PageRouteInfo<void> {
|
||||
const LibraryPlaylistsRoute()
|
||||
: super(
|
||||
LibraryPlaylistsRoute.name,
|
||||
path: 'playlists',
|
||||
);
|
||||
|
||||
static const String name = 'LibraryPlaylistsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibrarySongsPage]
|
||||
class LibrarySongsRoute extends PageRouteInfo<void> {
|
||||
const LibrarySongsRoute()
|
||||
: super(
|
||||
LibrarySongsRoute.name,
|
||||
path: 'songs',
|
||||
);
|
||||
|
||||
static const String name = 'LibrarySongsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [BrowsePage]
|
||||
class BrowseRoute extends PageRouteInfo<void> {
|
||||
const BrowseRoute()
|
||||
: super(
|
||||
BrowseRoute.name,
|
||||
path: '',
|
||||
);
|
||||
|
||||
static const String name = 'BrowseRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SearchPage]
|
||||
class SearchRoute extends PageRouteInfo<void> {
|
||||
const SearchRoute()
|
||||
: super(
|
||||
SearchRoute.name,
|
||||
path: '',
|
||||
);
|
||||
|
||||
static const String name = 'SearchRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SettingsPage]
|
||||
class SettingsRoute extends PageRouteInfo<void> {
|
||||
const SettingsRoute()
|
||||
: super(
|
||||
SettingsRoute.name,
|
||||
path: '',
|
||||
);
|
||||
|
||||
static const String name = 'SettingsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SourcePage]
|
||||
class SourceRoute extends PageRouteInfo<SourceRouteArgs> {
|
||||
SourceRoute({
|
||||
Key? key,
|
||||
int? id,
|
||||
}) : super(
|
||||
SourceRoute.name,
|
||||
path: 'source/:id',
|
||||
args: SourceRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'SourceRoute';
|
||||
}
|
||||
|
||||
class SourceRouteArgs {
|
||||
const SourceRouteArgs({
|
||||
this.key,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourceRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ShuffleFab extends StatelessWidget {
|
||||
final void Function()? onPressed;
|
||||
|
||||
const ShuffleFab({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: onPressed,
|
||||
tooltip: l.actionsCancel,
|
||||
child: const Icon(Icons.shuffle_rounded),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RadioPlayFab extends StatelessWidget {
|
||||
final void Function()? onPressed;
|
||||
|
||||
const RadioPlayFab({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: onPressed,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
const Icon(Icons.radio_rounded),
|
||||
Positioned(
|
||||
bottom: -11,
|
||||
right: -10,
|
||||
child: Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
bottom: -6,
|
||||
right: -5,
|
||||
child: Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/music.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import '../state/theme.dart';
|
||||
import 'app_router.dart';
|
||||
import 'hooks/use_download_actions.dart';
|
||||
import 'images.dart';
|
||||
|
||||
enum MenuSize {
|
||||
small,
|
||||
medium,
|
||||
}
|
||||
|
||||
Future<T?> showContextMenu<T>({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required WidgetBuilder builder,
|
||||
}) {
|
||||
return showModalBottomSheet<T>(
|
||||
backgroundColor: ref.read(baseThemeProvider).theme.colorScheme.background,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
|
||||
class BottomSheetMenu extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
final MenuSize size;
|
||||
|
||||
const BottomSheetMenu({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.size = MenuSize.medium,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(baseThemeProvider);
|
||||
final height = size == MenuSize.medium ? 0.4 : 0.25;
|
||||
|
||||
return Theme(
|
||||
data: theme.theme,
|
||||
child: DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: height,
|
||||
maxChildSize: height,
|
||||
minChildSize: height - 0.05,
|
||||
snap: true,
|
||||
snapSizes: [height - 0.05, height],
|
||||
builder: (context, scrollController) {
|
||||
return PrimaryScrollController(
|
||||
controller: scrollController,
|
||||
child: SizedBox(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumContextMenu extends HookConsumerWidget {
|
||||
final Album album;
|
||||
|
||||
const AlbumContextMenu({
|
||||
super.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloadActions = useAlbumDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
album: album,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
_AlbumHeader(album: album),
|
||||
const SizedBox(height: 8),
|
||||
const _Star(),
|
||||
if (album.artistId != null) _ViewArtist(id: album.artistId!),
|
||||
for (var action in downloadActions)
|
||||
_DownloadAction(key: ValueKey(action.type), downloadAction: action),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongContextMenu extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const SongContextMenu({
|
||||
super.key,
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView(
|
||||
children: [
|
||||
_SongHeader(song: song),
|
||||
const SizedBox(height: 8),
|
||||
const _Star(),
|
||||
if (song.artistId != null) _ViewArtist(id: song.artistId!),
|
||||
if (song.albumId != null) _ViewAlbum(id: song.albumId!),
|
||||
// const _DownloadAction(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistContextMenu extends HookConsumerWidget {
|
||||
final Artist artist;
|
||||
|
||||
const ArtistContextMenu({
|
||||
super.key,
|
||||
required this.artist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView(
|
||||
children: [
|
||||
_ArtistHeader(artist: artist),
|
||||
const SizedBox(height: 8),
|
||||
const _Star(),
|
||||
// const _Download(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistContextMenu extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
|
||||
const PlaylistContextMenu({
|
||||
super.key,
|
||||
required this.playlist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloadActions = usePlaylistDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
playlist: playlist,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
_PlaylistHeader(playlist: playlist),
|
||||
const SizedBox(height: 8),
|
||||
for (var action in downloadActions)
|
||||
_DownloadAction(key: ValueKey(action.type), downloadAction: action),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumHeader extends HookConsumerWidget {
|
||||
final Album album;
|
||||
|
||||
const _AlbumHeader({
|
||||
required this.album,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
|
||||
return _Header(
|
||||
title: album.name,
|
||||
subtitle: album.albumArtist,
|
||||
image: CardClip(
|
||||
child: UriCacheInfoImage(
|
||||
cache: cache.albumArt(album, thumbnail: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SongHeader extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const _SongHeader({
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _Header(
|
||||
title: song.title,
|
||||
subtitle: song.artist,
|
||||
image: SongAlbumArt(song: song, square: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArtistHeader extends HookConsumerWidget {
|
||||
final Artist artist;
|
||||
|
||||
const _ArtistHeader({
|
||||
required this.artist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _Header(
|
||||
title: artist.name,
|
||||
subtitle: l.resourcesAlbumCount(artist.albumCount),
|
||||
image: CircleClip(child: ArtistArtImage(artistId: artist.id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaylistHeader extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
|
||||
const _PlaylistHeader({
|
||||
required this.playlist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final l = AppLocalizations.of(context);
|
||||
return _Header(
|
||||
title: playlist.name,
|
||||
subtitle: l.resourcesSongCount(playlist.songCount),
|
||||
image: CardClip(
|
||||
child: UriCacheInfoImage(
|
||||
cache: cache.playlistArt(playlist, thumbnail: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends HookConsumerWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget image;
|
||||
|
||||
const _Header({
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.image,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 80, width: 80, child: image),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.titleLarge),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!, style: theme.textTheme.titleSmall),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Star extends HookConsumerWidget {
|
||||
const _Star();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _MenuItem(
|
||||
title: l.actionsStar,
|
||||
icon: const Icon(Icons.star_outline_rounded),
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadAction extends HookConsumerWidget {
|
||||
final DownloadAction downloadAction;
|
||||
|
||||
const _DownloadAction({
|
||||
super.key,
|
||||
required this.downloadAction,
|
||||
});
|
||||
|
||||
String _actionText(AppLocalizations l) {
|
||||
switch (downloadAction.type) {
|
||||
case DownloadActionType.download:
|
||||
return l.actionsDownload;
|
||||
case DownloadActionType.cancel:
|
||||
return l.actionsDownloadCancel;
|
||||
case DownloadActionType.delete:
|
||||
return l.actionsDownloadDelete;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _MenuItem(
|
||||
title: _actionText(AppLocalizations.of(context)),
|
||||
icon: downloadAction.iconBuilder(context),
|
||||
onTap: downloadAction.action,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewArtist extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const _ViewArtist({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _MenuItem(
|
||||
title: l.resourcesArtistActionsView,
|
||||
icon: const Icon(Icons.person_rounded),
|
||||
onTap: () async {
|
||||
final router = context.router;
|
||||
|
||||
await router.pop();
|
||||
if (router.currentPath == '/now-playing') {
|
||||
await router.pop();
|
||||
await router.navigate(const LibraryRouter());
|
||||
}
|
||||
await router.navigate(ArtistRoute(id: id));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewAlbum extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const _ViewAlbum({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _MenuItem(
|
||||
title: l.resourcesAlbumActionsView,
|
||||
icon: const Icon(Icons.album_rounded),
|
||||
onTap: () async {
|
||||
final router = context.router;
|
||||
|
||||
await router.pop();
|
||||
if (router.currentPath == '/now-playing') {
|
||||
await router.pop();
|
||||
await router.navigate(const LibraryRouter());
|
||||
}
|
||||
await router.navigate(AlbumSongsRoute(id: id));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuItem extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget icon;
|
||||
final FutureOr<void> Function()? onTap;
|
||||
|
||||
const _MenuItem({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(title),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 8),
|
||||
child: icon,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/support.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
class DeleteDialog extends HookConsumerWidget {
|
||||
const DeleteDialog({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(baseThemeProvider);
|
||||
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return Theme(
|
||||
data: theme.theme,
|
||||
child: AlertDialog(
|
||||
title: Text(l.resourcesSongListDeleteAllTitle),
|
||||
content: Text(l.resourcesSongListDeleteAllContent),
|
||||
actions: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(l.actionsCancel),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
label: Text(l.actionsDelete),
|
||||
icon: const Icon(Icons.delete_forever_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleChoiceDialog<T> extends HookConsumerWidget {
|
||||
final String title;
|
||||
final T current;
|
||||
final IList<MultiChoiceOption> options;
|
||||
|
||||
const MultipleChoiceDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.current,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
final state = useState<T>(current);
|
||||
|
||||
List<Widget> choices = [];
|
||||
for (var opt in options) {
|
||||
final value = opt.map(
|
||||
(value) => null,
|
||||
int: (value) => value.option,
|
||||
string: (value) => value.option,
|
||||
) as T;
|
||||
choices.add(RadioListTile<T>(
|
||||
value: value,
|
||||
groupValue: state.value,
|
||||
title: Text(opt.title),
|
||||
onChanged: (value) => state.value = value as T,
|
||||
));
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 20),
|
||||
content: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: choices),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
child: Text(l.actionsCancel),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () => Navigator.pop(context, state.value),
|
||||
label: Text(l.actionsOk),
|
||||
icon: const Icon(Icons.check_rounded),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/support.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
class MediaItemGradient extends ConsumerWidget {
|
||||
const MediaItemGradient({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
||||
return BackgroundGradient(colors: colors);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumArtGradient extends ConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const AlbumArtGradient({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(albumArtThemeProvider(id)).valueOrNull;
|
||||
return BackgroundGradient(colors: colors);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistArtGradient extends ConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const PlaylistArtGradient({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(playlistArtThemeProvider(id)).valueOrNull;
|
||||
return BackgroundGradient(colors: colors);
|
||||
}
|
||||
}
|
||||
|
||||
class BackgroundGradient extends HookConsumerWidget {
|
||||
final ColorTheme? colors;
|
||||
|
||||
const BackgroundGradient({
|
||||
super.key,
|
||||
this.colors,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final base = ref.watch(baseThemeProvider);
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
colors?.gradientHigh ?? base.gradientHigh,
|
||||
colors?.gradientLow ?? base.gradientLow,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../models/music.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/download_service.dart';
|
||||
import '../../state/music.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../dialogs.dart';
|
||||
|
||||
enum DownloadActionType {
|
||||
download,
|
||||
cancel,
|
||||
delete,
|
||||
}
|
||||
|
||||
class DownloadAction {
|
||||
final DownloadActionType type;
|
||||
final WidgetBuilder iconBuilder;
|
||||
final FutureOr<void> Function()? action;
|
||||
|
||||
const DownloadAction({
|
||||
required this.type,
|
||||
required this.iconBuilder,
|
||||
this.action,
|
||||
});
|
||||
}
|
||||
|
||||
List<DownloadAction> useAlbumDownloadActions({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required Album album,
|
||||
}) {
|
||||
final status = ref.watch(albumDownloadStatusProvider(album.id)).valueOrNull;
|
||||
|
||||
return useListDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
list: album,
|
||||
status: status,
|
||||
onDownload: () =>
|
||||
ref.read(downloadServiceProvider.notifier).downloadAlbum(album),
|
||||
onDelete: () =>
|
||||
ref.read(downloadServiceProvider.notifier).deleteAlbum(album),
|
||||
onCancel: () =>
|
||||
ref.read(downloadServiceProvider.notifier).cancelAlbum(album),
|
||||
);
|
||||
}
|
||||
|
||||
List<DownloadAction> usePlaylistDownloadActions({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required Playlist playlist,
|
||||
}) {
|
||||
final status =
|
||||
ref.watch(playlistDownloadStatusProvider(playlist.id)).valueOrNull;
|
||||
|
||||
return useListDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
list: playlist,
|
||||
status: status,
|
||||
onDownload: () =>
|
||||
ref.read(downloadServiceProvider.notifier).downloadPlaylist(playlist),
|
||||
onDelete: () =>
|
||||
ref.read(downloadServiceProvider.notifier).deletePlaylist(playlist),
|
||||
onCancel: () =>
|
||||
ref.read(downloadServiceProvider.notifier).cancelPlaylist(playlist),
|
||||
);
|
||||
}
|
||||
|
||||
List<DownloadAction> useListDownloadActions({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required SourceIdentifiable list,
|
||||
required ListDownloadStatus? status,
|
||||
required FutureOr<void> Function() onDelete,
|
||||
required FutureOr<void> Function() onCancel,
|
||||
required FutureOr<void> Function() onDownload,
|
||||
}) {
|
||||
status ??= const ListDownloadStatus(total: 0, downloaded: 0, downloading: 0);
|
||||
|
||||
final sourceId = SourceId.from(list);
|
||||
final offline = ref.watch(offlineModeProvider);
|
||||
final listDownloadInProgress = ref.watch(downloadServiceProvider
|
||||
.select((value) => value.listDownloads.contains(sourceId)));
|
||||
final listDeleteInProgress = ref.watch(downloadServiceProvider
|
||||
.select((value) => value.deletes.contains(sourceId)));
|
||||
final listCancelInProgress = ref.watch(downloadServiceProvider
|
||||
.select((value) => value.listCancels.contains(sourceId)));
|
||||
|
||||
DownloadAction delete() {
|
||||
return DownloadAction(
|
||||
type: DownloadActionType.delete,
|
||||
iconBuilder: (context) => const Icon(Icons.delete_forever_rounded),
|
||||
action: listDeleteInProgress
|
||||
? null
|
||||
: () async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => const DeleteDialog(),
|
||||
);
|
||||
if (ok == true) {
|
||||
await onDelete();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
DownloadAction cancel() {
|
||||
return DownloadAction(
|
||||
type: DownloadActionType.cancel,
|
||||
iconBuilder: (context) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(Icons.cancel_rounded),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
action: listCancelInProgress ? null : onCancel,
|
||||
);
|
||||
}
|
||||
|
||||
DownloadAction download() {
|
||||
return DownloadAction(
|
||||
type: DownloadActionType.download,
|
||||
iconBuilder: (context) => const Icon(Icons.download_rounded),
|
||||
action: !offline ? onDownload : null,
|
||||
);
|
||||
}
|
||||
|
||||
if (status.total == status.downloaded) {
|
||||
return [delete()];
|
||||
} else if (status.downloading == 0 && status.downloaded > 0) {
|
||||
return [download(), delete()];
|
||||
} else if (listDownloadInProgress || status.downloading > 0) {
|
||||
return [cancel()];
|
||||
} else {
|
||||
return [download()];
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../../models/query.dart';
|
||||
import '../../services/sync_service.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../pages/library_page.dart';
|
||||
import 'use_paging_controller.dart';
|
||||
|
||||
PagingController<int, T> useLibraryPagingController<T>(
|
||||
WidgetRef ref, {
|
||||
required int libraryTabIndex,
|
||||
required FutureOr<List<T>> Function(ListQuery query) getItems,
|
||||
}) {
|
||||
final queryProvider = libraryListQueryProvider(libraryTabIndex).select(
|
||||
(value) => value.query,
|
||||
);
|
||||
final query = useState(ref.read(queryProvider));
|
||||
|
||||
final onPageRequest = useCallback(
|
||||
(int pageKey, PagingController<int, T> pagingController) =>
|
||||
_pageRequest(getItems, query.value, pageKey, pagingController),
|
||||
[query.value],
|
||||
);
|
||||
|
||||
final pagingController = usePagingController<int, T>(
|
||||
firstPageKey: query.value.page.offset,
|
||||
onPageRequest: onPageRequest,
|
||||
);
|
||||
|
||||
ref.listen(queryProvider, (_, next) {
|
||||
query.value = next;
|
||||
pagingController.refresh();
|
||||
});
|
||||
|
||||
ref.listen(syncServiceProvider, (_, __) => pagingController.refresh());
|
||||
ref.listen(sourceIdProvider, (_, __) => pagingController.refresh());
|
||||
ref.listen(offlineModeProvider, (_, __) => pagingController.refresh());
|
||||
|
||||
return pagingController;
|
||||
}
|
||||
|
||||
PagingController<int, T> useListQueryPagingController<T>(
|
||||
WidgetRef ref, {
|
||||
required ListQuery query,
|
||||
required FutureOr<List<T>> Function(ListQuery query) getItems,
|
||||
}) {
|
||||
final onPageRequest = useCallback(
|
||||
(int pageKey, PagingController<int, T> pagingController) =>
|
||||
_pageRequest(getItems, query, pageKey, pagingController),
|
||||
[query],
|
||||
);
|
||||
|
||||
final pagingController = usePagingController<int, T>(
|
||||
firstPageKey: query.page.offset,
|
||||
onPageRequest: onPageRequest,
|
||||
);
|
||||
|
||||
return pagingController;
|
||||
}
|
||||
|
||||
Future<void> _pageRequest<T>(
|
||||
FutureOr<List<T>> Function(ListQuery query) getItems,
|
||||
ListQuery query,
|
||||
int pageKey,
|
||||
PagingController<int, dynamic> pagingController,
|
||||
) async {
|
||||
try {
|
||||
final newItems = await getItems(query.copyWith.page(offset: pageKey));
|
||||
|
||||
final isFirstPage = newItems.isNotEmpty && pageKey == 0;
|
||||
final alreadyHasItems = pagingController.itemList != null &&
|
||||
pagingController.itemList!.isNotEmpty;
|
||||
if (isFirstPage && alreadyHasItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isLastPage = newItems.length < query.page.limit;
|
||||
if (isLastPage) {
|
||||
pagingController.appendLastPage(newItems);
|
||||
} else {
|
||||
final nextPageKey = pageKey + newItems.length;
|
||||
pagingController.appendPage(newItems, nextPageKey);
|
||||
}
|
||||
} catch (error) {
|
||||
pagingController.error = error;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
PagingController<PageKeyType, ItemType>
|
||||
usePagingController<PageKeyType, ItemType>({
|
||||
required final PageKeyType firstPageKey,
|
||||
final int? invisibleItemsThreshold,
|
||||
List<Object?>? keys,
|
||||
FutureOr<void> Function(PageKeyType pageKey,
|
||||
PagingController<PageKeyType, ItemType> pagingController)?
|
||||
onPageRequest,
|
||||
}) {
|
||||
final controller = use(
|
||||
_PagingControllerHook<PageKeyType, ItemType>(
|
||||
firstPageKey: firstPageKey,
|
||||
invisibleItemsThreshold: invisibleItemsThreshold,
|
||||
keys: keys,
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
listener(PageKeyType pageKey) => onPageRequest?.call(pageKey, controller);
|
||||
controller.addPageRequestListener(listener);
|
||||
return () => controller.removePageRequestListener(listener);
|
||||
}, [onPageRequest]);
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
class _PagingControllerHook<PageKeyType, ItemType>
|
||||
extends Hook<PagingController<PageKeyType, ItemType>> {
|
||||
const _PagingControllerHook({
|
||||
required this.firstPageKey,
|
||||
this.invisibleItemsThreshold,
|
||||
List<Object?>? keys,
|
||||
}) : super(keys: keys);
|
||||
|
||||
final PageKeyType firstPageKey;
|
||||
final int? invisibleItemsThreshold;
|
||||
|
||||
@override
|
||||
HookState<PagingController<PageKeyType, ItemType>,
|
||||
Hook<PagingController<PageKeyType, ItemType>>>
|
||||
createState() => _PagingControllerHookState<PageKeyType, ItemType>();
|
||||
}
|
||||
|
||||
class _PagingControllerHookState<PageKeyType, ItemType> extends HookState<
|
||||
PagingController<PageKeyType, ItemType>,
|
||||
_PagingControllerHook<PageKeyType, ItemType>> {
|
||||
late final controller = PagingController<PageKeyType, ItemType>(
|
||||
firstPageKey: hook.firstPageKey,
|
||||
invisibleItemsThreshold: hook.invisibleItemsThreshold);
|
||||
|
||||
@override
|
||||
PagingController<PageKeyType, ItemType> build(BuildContext context) =>
|
||||
controller;
|
||||
|
||||
@override
|
||||
void dispose() => controller.dispose();
|
||||
|
||||
@override
|
||||
String get debugLabel => 'usePagingController';
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../models/music.dart';
|
||||
import '../models/support.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import '../state/music.dart';
|
||||
import '../state/settings.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
part 'images.g.dart';
|
||||
|
||||
@riverpod
|
||||
CacheInfo _artistArtCacheInfo(
|
||||
_ArtistArtCacheInfoRef ref, {
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
return cache.artistArtCacheInfo(artistId, thumbnail: thumbnail);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<String?> _artistArtCachedUrl(
|
||||
_ArtistArtCachedUrlRef ref, {
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) async {
|
||||
final cache = ref.watch(_artistArtCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
));
|
||||
final file = await cache.cacheManager.getFileFromCache(cache.cacheKey);
|
||||
return file?.originalUrl;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<UriCacheInfo> _artistArtUriCacheInfo(
|
||||
_ArtistArtUriCacheInfoRef ref, {
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) async {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final info = ref.watch(_artistArtCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
));
|
||||
final cachedUrl = await ref.watch(_artistArtCachedUrlProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
).future);
|
||||
final offline = ref.watch(offlineModeProvider);
|
||||
|
||||
// already cached, don't try to get the real url again
|
||||
if (cachedUrl != null) {
|
||||
return UriCacheInfo(
|
||||
uri: Uri.parse(cachedUrl),
|
||||
cacheKey: info.cacheKey,
|
||||
cacheManager: info.cacheManager,
|
||||
);
|
||||
}
|
||||
|
||||
if (offline) {
|
||||
final file = await cache.imageCache.getFileFromCache(info.cacheKey);
|
||||
if (file != null) {
|
||||
return UriCacheInfo(
|
||||
uri: Uri.parse(file.originalUrl),
|
||||
cacheKey: info.cacheKey,
|
||||
cacheManager: info.cacheManager,
|
||||
);
|
||||
} else {
|
||||
return cache.placeholder(thumbnail: thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
// assume the url is good or let this fail
|
||||
return UriCacheInfo(
|
||||
uri: (await cache.artistArtUri(artistId, thumbnail: thumbnail))!,
|
||||
cacheKey: info.cacheKey,
|
||||
cacheManager: info.cacheManager,
|
||||
);
|
||||
}
|
||||
|
||||
class ArtistArtImage extends HookConsumerWidget {
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
final BoxFit fit;
|
||||
final PlaceholderStyle placeholderStyle;
|
||||
final double? height;
|
||||
final double? width;
|
||||
|
||||
const ArtistArtImage({
|
||||
super.key,
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholderStyle = PlaceholderStyle.color,
|
||||
this.height,
|
||||
this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(_artistArtUriCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
));
|
||||
|
||||
// TODO: figure out how to animate this without messing up with boxfit/ratio
|
||||
return cache.when(
|
||||
data: (data) => UriCacheInfoImage(
|
||||
cache: data,
|
||||
fit: fit,
|
||||
placeholderStyle: placeholderStyle,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
error: (_, __) => Container(
|
||||
color: Colors.red,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
loading: () => Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongAlbumArt extends HookConsumerWidget {
|
||||
final Song song;
|
||||
final bool square;
|
||||
|
||||
const SongAlbumArt({
|
||||
super.key,
|
||||
required this.song,
|
||||
this.square = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.watch(albumProvider(song.albumId!)).valueOrNull;
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: album != null ? AlbumArt(album: album) : const PlaceholderImage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumArt extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final bool square;
|
||||
|
||||
const AlbumArt({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.square = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// generate the palette used in other views ahead of time
|
||||
ref.watch(albumArtPaletteProvider(album.id));
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
|
||||
Widget image = UriCacheInfoImage(cache: cache.albumArt(album));
|
||||
|
||||
if (square) {
|
||||
image = AspectRatio(aspectRatio: 1.0, child: image);
|
||||
}
|
||||
|
||||
return CardClip(child: image);
|
||||
}
|
||||
}
|
||||
|
||||
class CircleClip extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const CircleClip({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipOval(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardClip extends StatelessWidget {
|
||||
final Widget child;
|
||||
final bool square;
|
||||
|
||||
const CardClip({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.square = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cardShape = Theme.of(context).cardTheme.shape;
|
||||
return ClipRRect(
|
||||
borderRadius:
|
||||
cardShape is RoundedRectangleBorder ? cardShape.borderRadius : null,
|
||||
child: !square
|
||||
? child
|
||||
: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PlaceholderStyle {
|
||||
color,
|
||||
spinner,
|
||||
}
|
||||
|
||||
class UriCacheInfoImage extends StatelessWidget {
|
||||
final UriCacheInfo cache;
|
||||
final BoxFit fit;
|
||||
final PlaceholderStyle placeholderStyle;
|
||||
final double? height;
|
||||
final double? width;
|
||||
|
||||
const UriCacheInfoImage({
|
||||
super.key,
|
||||
required this.cache,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholderStyle = PlaceholderStyle.color,
|
||||
this.height,
|
||||
this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: cache.uri.toString(),
|
||||
cacheKey: cache.cacheKey,
|
||||
cacheManager: cache.cacheManager,
|
||||
fit: fit,
|
||||
height: height,
|
||||
width: width,
|
||||
fadeInDuration: const Duration(milliseconds: 300),
|
||||
fadeOutDuration: const Duration(milliseconds: 500),
|
||||
placeholder: (context, url) =>
|
||||
placeholderStyle == PlaceholderStyle.spinner
|
||||
? Container()
|
||||
: Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
errorWidget: (context, url, error) => PlaceholderImage(
|
||||
fit: fit,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderImage extends HookConsumerWidget {
|
||||
final BoxFit fit;
|
||||
final double? height;
|
||||
final double? width;
|
||||
final bool thumbnail;
|
||||
|
||||
const PlaceholderImage({
|
||||
super.key,
|
||||
this.fit = BoxFit.cover,
|
||||
this.height,
|
||||
this.width,
|
||||
this.thumbnail = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Image.asset(
|
||||
thumbnail ? 'assets/placeholder_thumb.png' : 'assets/placeholder.png',
|
||||
fit: fit,
|
||||
height: height,
|
||||
width: width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpandedRatio extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double aspectRatio;
|
||||
|
||||
const _ExpandedRatio({
|
||||
required this.child,
|
||||
this.aspectRatio = 1.0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiImage extends HookConsumerWidget {
|
||||
final Iterable<UriCacheInfo> cacheInfo;
|
||||
|
||||
const MultiImage({
|
||||
super.key,
|
||||
required this.cacheInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final images = cacheInfo.map((cache) => UriCacheInfoImage(cache: cache));
|
||||
|
||||
final row1 = <Widget>[];
|
||||
final row2 = <Widget>[];
|
||||
|
||||
if (images.length >= 4) {
|
||||
row1.addAll([
|
||||
_ExpandedRatio(child: images.elementAt(0)),
|
||||
_ExpandedRatio(child: images.elementAt(1)),
|
||||
]);
|
||||
row2.addAll([
|
||||
_ExpandedRatio(child: images.elementAt(2)),
|
||||
_ExpandedRatio(child: images.elementAt(3)),
|
||||
]);
|
||||
}
|
||||
if (images.length == 3) {
|
||||
row1.addAll([
|
||||
_ExpandedRatio(child: images.elementAt(0)),
|
||||
_ExpandedRatio(child: images.elementAt(1)),
|
||||
]);
|
||||
row2.addAll([
|
||||
_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(2)),
|
||||
]);
|
||||
}
|
||||
if (images.length == 2) {
|
||||
row1.add(_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(0)));
|
||||
row2.add(_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(1)));
|
||||
}
|
||||
if (images.length == 1) {
|
||||
row1.addAll([_ExpandedRatio(child: images.elementAt(0))]);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(children: row1),
|
||||
Row(children: row2),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'images.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$artistArtCacheInfoHash() =>
|
||||
r'f82d3e91aa1596939e376c6a7ea7d3e974c6f0fc';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
typedef _ArtistArtCacheInfoRef = AutoDisposeProviderRef<CacheInfo>;
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
@ProviderFor(_artistArtCacheInfo)
|
||||
const _artistArtCacheInfoProvider = _ArtistArtCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
class _ArtistArtCacheInfoFamily extends Family<CacheInfo> {
|
||||
/// See also [_artistArtCacheInfo].
|
||||
const _ArtistArtCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
_ArtistArtCacheInfoProvider call({
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
return _ArtistArtCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_ArtistArtCacheInfoProvider getProviderOverride(
|
||||
covariant _ArtistArtCacheInfoProvider provider,
|
||||
) {
|
||||
return call(
|
||||
artistId: provider.artistId,
|
||||
thumbnail: provider.thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'_artistArtCacheInfoProvider';
|
||||
}
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
class _ArtistArtCacheInfoProvider extends AutoDisposeProvider<CacheInfo> {
|
||||
/// See also [_artistArtCacheInfo].
|
||||
_ArtistArtCacheInfoProvider({
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
}) : super.internal(
|
||||
(ref) => _artistArtCacheInfo(
|
||||
ref,
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
),
|
||||
from: _artistArtCacheInfoProvider,
|
||||
name: r'_artistArtCacheInfoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$artistArtCacheInfoHash,
|
||||
dependencies: _ArtistArtCacheInfoFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
_ArtistArtCacheInfoFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _ArtistArtCacheInfoProvider &&
|
||||
other.artistId == artistId &&
|
||||
other.thumbnail == thumbnail;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, artistId.hashCode);
|
||||
hash = _SystemHash.combine(hash, thumbnail.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
String _$artistArtCachedUrlHash() =>
|
||||
r'2a5e0fea614ff12a1d562faccec6cfe98394af42';
|
||||
typedef _ArtistArtCachedUrlRef = AutoDisposeFutureProviderRef<String?>;
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
@ProviderFor(_artistArtCachedUrl)
|
||||
const _artistArtCachedUrlProvider = _ArtistArtCachedUrlFamily();
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
class _ArtistArtCachedUrlFamily extends Family<AsyncValue<String?>> {
|
||||
/// See also [_artistArtCachedUrl].
|
||||
const _ArtistArtCachedUrlFamily();
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
_ArtistArtCachedUrlProvider call({
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
return _ArtistArtCachedUrlProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_ArtistArtCachedUrlProvider getProviderOverride(
|
||||
covariant _ArtistArtCachedUrlProvider provider,
|
||||
) {
|
||||
return call(
|
||||
artistId: provider.artistId,
|
||||
thumbnail: provider.thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'_artistArtCachedUrlProvider';
|
||||
}
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
class _ArtistArtCachedUrlProvider extends AutoDisposeFutureProvider<String?> {
|
||||
/// See also [_artistArtCachedUrl].
|
||||
_ArtistArtCachedUrlProvider({
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
}) : super.internal(
|
||||
(ref) => _artistArtCachedUrl(
|
||||
ref,
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
),
|
||||
from: _artistArtCachedUrlProvider,
|
||||
name: r'_artistArtCachedUrlProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$artistArtCachedUrlHash,
|
||||
dependencies: _ArtistArtCachedUrlFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
_ArtistArtCachedUrlFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _ArtistArtCachedUrlProvider &&
|
||||
other.artistId == artistId &&
|
||||
other.thumbnail == thumbnail;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, artistId.hashCode);
|
||||
hash = _SystemHash.combine(hash, thumbnail.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
String _$artistArtUriCacheInfoHash() =>
|
||||
r'9bdc0f5654882265236ef746ea697a6d107a4b6f';
|
||||
typedef _ArtistArtUriCacheInfoRef = AutoDisposeFutureProviderRef<UriCacheInfo>;
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
@ProviderFor(_artistArtUriCacheInfo)
|
||||
const _artistArtUriCacheInfoProvider = _ArtistArtUriCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
class _ArtistArtUriCacheInfoFamily extends Family<AsyncValue<UriCacheInfo>> {
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
const _ArtistArtUriCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
_ArtistArtUriCacheInfoProvider call({
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
return _ArtistArtUriCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_ArtistArtUriCacheInfoProvider getProviderOverride(
|
||||
covariant _ArtistArtUriCacheInfoProvider provider,
|
||||
) {
|
||||
return call(
|
||||
artistId: provider.artistId,
|
||||
thumbnail: provider.thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'_artistArtUriCacheInfoProvider';
|
||||
}
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
class _ArtistArtUriCacheInfoProvider
|
||||
extends AutoDisposeFutureProvider<UriCacheInfo> {
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
_ArtistArtUriCacheInfoProvider({
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
}) : super.internal(
|
||||
(ref) => _artistArtUriCacheInfo(
|
||||
ref,
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
),
|
||||
from: _artistArtUriCacheInfoProvider,
|
||||
name: r'_artistArtUriCacheInfoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$artistArtUriCacheInfoHash,
|
||||
dependencies: _ArtistArtUriCacheInfoFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
_ArtistArtUriCacheInfoFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _ArtistArtUriCacheInfoProvider &&
|
||||
other.artistId == artistId &&
|
||||
other.thumbnail == thumbnail;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, artistId.hashCode);
|
||||
hash = _SystemHash.combine(hash, thumbnail.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,433 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/music.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../state/audio.dart';
|
||||
import '../state/music.dart';
|
||||
import '../state/theme.dart';
|
||||
import 'context_menus.dart';
|
||||
import 'images.dart';
|
||||
import 'pages/songs_page.dart';
|
||||
|
||||
enum CardStyle {
|
||||
imageOnly,
|
||||
withText,
|
||||
}
|
||||
|
||||
enum AlbumSubtitle {
|
||||
artist,
|
||||
year,
|
||||
}
|
||||
|
||||
class AlbumCard extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final void Function()? onTap;
|
||||
final CardStyle style;
|
||||
final AlbumSubtitle subtitle;
|
||||
|
||||
const AlbumCard({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
this.style = CardStyle.withText,
|
||||
this.subtitle = AlbumSubtitle.artist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// generate the palette used in other views ahead of time
|
||||
ref.watch(albumArtPaletteProvider(album.id));
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final info = cache.albumArt(album);
|
||||
|
||||
final image = CardClip(child: UriCacheInfoImage(cache: info));
|
||||
Widget content;
|
||||
if (style == CardStyle.imageOnly) {
|
||||
content = image;
|
||||
} else {
|
||||
content = Column(
|
||||
children: [
|
||||
image,
|
||||
_AlbumCardText(album: album, subtitle: subtitle),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ImageCard(
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
child: AlbumContextMenu(album: album),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImageCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onLongPress;
|
||||
|
||||
const ImageCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
surfaceTintColor: Colors.transparent,
|
||||
margin: const EdgeInsets.all(0),
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
child,
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumCardText extends StatelessWidget {
|
||||
final Album album;
|
||||
final AlbumSubtitle subtitle;
|
||||
|
||||
const _AlbumCardText({
|
||||
required this.album,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
album.name,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(subtitle == AlbumSubtitle.artist
|
||||
? album.albumArtist
|
||||
: album.year?.toString()) ??
|
||||
'',
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumListTile extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final void Function()? onTap;
|
||||
|
||||
const AlbumListTile({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final artist = ref.watch(albumProvider(album.artistId!)).valueOrNull;
|
||||
|
||||
return ListTile(
|
||||
leading: AlbumArt(album: album),
|
||||
title: Text(album.name),
|
||||
subtitle: Text(album.albumArtist ?? artist!.name),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
size: MenuSize.small,
|
||||
child: AlbumContextMenu(album: album),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistListTile extends HookConsumerWidget {
|
||||
final Artist artist;
|
||||
final void Function()? onTap;
|
||||
|
||||
const ArtistListTile({
|
||||
super.key,
|
||||
required this.artist,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: CircleClip(
|
||||
child: ArtistArtImage(artistId: artist.id),
|
||||
),
|
||||
title: Text(artist.name),
|
||||
subtitle: Text(AppLocalizations.of(context).resourcesAlbumCount(
|
||||
artist.albumCount,
|
||||
)),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
size: MenuSize.small,
|
||||
child: ArtistContextMenu(artist: artist),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistListTile extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
final void Function()? onTap;
|
||||
|
||||
const PlaylistListTile({
|
||||
super.key,
|
||||
required this.playlist,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// generate the palette used in other views ahead of time
|
||||
ref.watch(playlistArtPaletteProvider(playlist.id));
|
||||
final cache = ref.watch(cacheServiceProvider).playlistArt(playlist);
|
||||
|
||||
return ListTile(
|
||||
leading: CardClip(
|
||||
child: UriCacheInfoImage(cache: cache),
|
||||
),
|
||||
title: Text(playlist.name),
|
||||
subtitle: Text(AppLocalizations.of(context).resourcesSongCount(
|
||||
playlist.songCount,
|
||||
)),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
size: MenuSize.small,
|
||||
child: PlaylistContextMenu(playlist: playlist),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongListTile extends HookConsumerWidget {
|
||||
final Song song;
|
||||
final void Function()? onTap;
|
||||
final bool image;
|
||||
|
||||
const SongListTile({
|
||||
super.key,
|
||||
required this.song,
|
||||
this.onTap,
|
||||
this.image = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: ListTile(
|
||||
title: _SongTitle(song: song),
|
||||
subtitle: _SongSubtitle(song: song),
|
||||
leading: image ? SongAlbumArt(song: song) : null,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.star_outline_rounded,
|
||||
size: 36,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
child: SongContextMenu(song: song),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SongSubtitle extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const _SongSubtitle({
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloadTaskId = ref.watch(songProvider(song.id).select(
|
||||
(value) => value.valueOrNull?.downloadTaskId,
|
||||
));
|
||||
final downloadFilePath = ref.watch(songProvider(song.id).select(
|
||||
(value) => value.valueOrNull?.downloadFilePath,
|
||||
));
|
||||
final download = ref.watch(downloadServiceProvider.select(
|
||||
(value) => value.downloads.firstWhereOrNull(
|
||||
(e) => e.taskId == downloadTaskId,
|
||||
),
|
||||
));
|
||||
|
||||
final inheritedStyle = DefaultTextStyle.of(context).style;
|
||||
|
||||
Widget? downloadIndicator;
|
||||
if (downloadFilePath != null) {
|
||||
downloadIndicator = const Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 3),
|
||||
child: Icon(
|
||||
Icons.download_done_rounded,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
} else if (downloadTaskId != null || download != null) {
|
||||
downloadIndicator = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 4, end: 9),
|
||||
child: SizedBox(
|
||||
height: 10,
|
||||
width: 10,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
value: download != null && download.progress > 0
|
||||
? download.progress / 100
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (downloadIndicator != null) downloadIndicator,
|
||||
Expanded(
|
||||
child: Text(
|
||||
song.artist ?? song.album ?? '',
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
color: inheritedStyle.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SongTitle extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const _SongTitle({
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mediaItem = ref.watch(mediaItemProvider).valueOrNull;
|
||||
final mediaItemData = ref.watch(mediaItemDataProvider);
|
||||
final inheritedStyle = DefaultTextStyle.of(context).style;
|
||||
final theme = Theme.of(context);
|
||||
final queueContext = QueueContext.maybeOf(context);
|
||||
|
||||
final playing = mediaItem != null &&
|
||||
mediaItemData != null &&
|
||||
mediaItem.id == song.id &&
|
||||
mediaItemData.contextId == queueContext?.id &&
|
||||
mediaItemData.contextType == queueContext?.type;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (playing)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 2),
|
||||
child: Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
size: 18,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
song.title,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
color: playing ? theme.colorScheme.primary : inheritedStyle.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FabPadding extends StatelessWidget {
|
||||
const FabPadding({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(height: 86);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../services/sync_service.dart';
|
||||
import 'items.dart';
|
||||
import 'snackbars.dart';
|
||||
|
||||
class PagedListQueryView<T> extends HookConsumerWidget {
|
||||
final PagingController<int, T> pagingController;
|
||||
final bool refreshSyncAll;
|
||||
final bool fabPadding;
|
||||
final bool useSliver;
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
|
||||
const PagedListQueryView({
|
||||
super.key,
|
||||
required this.pagingController,
|
||||
this.refreshSyncAll = false,
|
||||
this.fabPadding = true,
|
||||
this.useSliver = false,
|
||||
required this.itemBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final builderDelegate = PagedChildBuilderDelegate<T>(
|
||||
itemBuilder: (context, item, index) => itemBuilder(context, item, index),
|
||||
noMoreItemsIndicatorBuilder:
|
||||
fabPadding ? (context) => const FabPadding() : null,
|
||||
);
|
||||
|
||||
final listView = useSliver
|
||||
? PagedSliverList<int, T>(
|
||||
pagingController: pagingController,
|
||||
builderDelegate: builderDelegate,
|
||||
)
|
||||
: PagedListView<int, T>(
|
||||
pagingController: pagingController,
|
||||
builderDelegate: builderDelegate,
|
||||
);
|
||||
|
||||
if (refreshSyncAll) {
|
||||
return SyncAllRefresh(child: listView);
|
||||
} else {
|
||||
return listView;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GridSize {
|
||||
small,
|
||||
large,
|
||||
}
|
||||
|
||||
class PagedGridQueryView<T> extends HookConsumerWidget {
|
||||
final PagingController<int, T> pagingController;
|
||||
final bool refreshSyncAll;
|
||||
final bool fabPadding;
|
||||
final GridSize size;
|
||||
final Widget Function(BuildContext context, T item, int index, GridSize size)
|
||||
itemBuilder;
|
||||
|
||||
const PagedGridQueryView({
|
||||
super.key,
|
||||
required this.pagingController,
|
||||
this.refreshSyncAll = false,
|
||||
this.fabPadding = true,
|
||||
this.size = GridSize.small,
|
||||
required this.itemBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
SliverGridDelegate gridDelegate;
|
||||
double spacing;
|
||||
|
||||
if (size == GridSize.small) {
|
||||
spacing = 4;
|
||||
gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
);
|
||||
} else {
|
||||
spacing = 12;
|
||||
gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
final listView = PagedGridView<int, T>(
|
||||
padding: MediaQuery.of(context).padding + EdgeInsets.all(spacing),
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
itemBuilder: (context, item, index) =>
|
||||
itemBuilder(context, item, index, size),
|
||||
noMoreItemsIndicatorBuilder:
|
||||
fabPadding ? (context) => const FabPadding() : null,
|
||||
),
|
||||
gridDelegate: gridDelegate,
|
||||
showNoMoreItemsIndicatorAsGridChild: false,
|
||||
);
|
||||
|
||||
if (refreshSyncAll) {
|
||||
return SyncAllRefresh(child: listView);
|
||||
} else {
|
||||
return listView;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyncAllRefresh extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const SyncAllRefresh({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
try {
|
||||
await ref.read(syncServiceProvider.notifier).syncAll();
|
||||
} catch (e) {
|
||||
showErrorSnackbar(context, e.toString());
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../cache/image_cache.dart';
|
||||
import '../models/support.dart';
|
||||
import '../services/audio_service.dart';
|
||||
import '../state/audio.dart';
|
||||
import '../state/theme.dart';
|
||||
import 'app_router.dart';
|
||||
import 'images.dart';
|
||||
import 'pages/now_playing_page.dart';
|
||||
|
||||
class NowPlayingBar extends HookConsumerWidget {
|
||||
const NowPlayingBar({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
||||
final noItem = ref.watch(mediaItemProvider).valueOrNull == null;
|
||||
|
||||
final widget = GestureDetector(
|
||||
onTap: () {
|
||||
context.navigateTo(const NowPlayingRoute());
|
||||
},
|
||||
child: Material(
|
||||
elevation: 3,
|
||||
color: colors?.darkBackground,
|
||||
// surfaceTintColor: theme?.colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 70,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: _ArtImage(),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
child: _TrackInfo(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 16, top: 2),
|
||||
child: PlayPauseButton(size: 48),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _ProgressBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (noItem) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
if (colors != null) {
|
||||
return Theme(data: colors.theme, child: widget);
|
||||
} else {
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ArtImage extends HookConsumerWidget {
|
||||
const _ArtImage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final imageCache = ref.watch(imageCacheProvider);
|
||||
final uri =
|
||||
ref.watch(mediaItemProvider.select((e) => e.valueOrNull?.artUri));
|
||||
final cacheKey = ref.watch(mediaItemDataProvider.select(
|
||||
(value) => value?.artCache?.thumbnailArtCacheKey,
|
||||
));
|
||||
|
||||
UriCacheInfo? cache;
|
||||
if (uri != null && cacheKey != null) {
|
||||
cache = UriCacheInfo(
|
||||
uri: uri,
|
||||
cacheKey: cacheKey,
|
||||
cacheManager: imageCache,
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: CardClip(
|
||||
key: ValueKey(cacheKey ?? 'default'),
|
||||
child: cache == null
|
||||
? const PlaceholderImage()
|
||||
: UriCacheInfoImage(
|
||||
cache: cache,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackInfo extends HookConsumerWidget {
|
||||
const _TrackInfo();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final item = ref.watch(mediaItemProvider);
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...item.when(
|
||||
data: (data) => [
|
||||
// Text(
|
||||
// data?.title ?? 'Nothing!!!',
|
||||
// maxLines: 1,
|
||||
// softWrap: false,
|
||||
// overflow: TextOverflow.fade,
|
||||
// style: Theme.of(context).textTheme.labelLarge,
|
||||
// ),
|
||||
ScrollableText(
|
||||
data?.title ?? 'Nothing!!!',
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
data?.artist ?? 'Nothing!!!',
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
error: (_, __) => const [Text('Error!')],
|
||||
loading: () => const [Text('loading.....')],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlayPauseButton extends HookConsumerWidget {
|
||||
final double size;
|
||||
|
||||
const PlayPauseButton({
|
||||
super.key,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final playing = ref.watch(playingProvider);
|
||||
final state = ref.watch(processingStateProvider);
|
||||
|
||||
Widget icon;
|
||||
if (state == AudioProcessingState.loading ||
|
||||
state == AudioProcessingState.buffering) {
|
||||
icon = Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.circle),
|
||||
SizedBox(
|
||||
height: size / 3,
|
||||
width: size / 3,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: size / 16,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (playing) {
|
||||
icon = const Icon(Icons.pause_circle_rounded);
|
||||
} else {
|
||||
icon = const Icon(Icons.play_circle_rounded);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
if (playing) {
|
||||
ref.read(audioControlProvider).pause();
|
||||
} else {
|
||||
ref.read(audioControlProvider).play();
|
||||
}
|
||||
},
|
||||
icon: icon,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressBar extends HookConsumerWidget {
|
||||
const _ProgressBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
||||
final position = ref.watch(positionProvider);
|
||||
final duration = ref.watch(durationProvider);
|
||||
|
||||
return Container(
|
||||
height: 4,
|
||||
color: colors?.darkerBackground,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: position,
|
||||
child: Container(color: colors?.onDarkerBackground),
|
||||
),
|
||||
Flexible(flex: duration - position, child: Container()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../models/query.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/audio_service.dart';
|
||||
import '../../state/music.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../buttons.dart';
|
||||
import '../images.dart';
|
||||
import '../items.dart';
|
||||
|
||||
class ArtistPage extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const ArtistPage({
|
||||
super.key,
|
||||
@pathParam required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ref.listen(sourceIdProvider, (_, __) => context.router.popUntilRoot());
|
||||
|
||||
final artist = ref.watch(artistProvider(id));
|
||||
final albums = ref.watch(albumsByArtistIdProvider(id));
|
||||
|
||||
return Scaffold(
|
||||
floatingActionButton: RadioPlayFab(
|
||||
onPressed: () => artist.hasValue
|
||||
? ref.read(audioControlProvider).playRadio(
|
||||
context: QueueContextType.artist,
|
||||
contextId: artist.valueOrNull!.id,
|
||||
query: ListQuery(
|
||||
filters: IList([
|
||||
FilterWith.equals(
|
||||
column: 'artist_id',
|
||||
value: artist.valueOrNull!.id,
|
||||
)
|
||||
]),
|
||||
),
|
||||
getSongs: (query) => ref
|
||||
.read(databaseProvider)
|
||||
.songsList(ref.read(sourceIdProvider), query)
|
||||
.get(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
ArtistArtImage(
|
||||
artistId: id,
|
||||
thumbnail: false,
|
||||
height: 400,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: _Title(text: artist.valueOrNull?.name ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
albums.when(
|
||||
data: (albums) {
|
||||
albums = albums.sort((a, b) => (b.year ?? 0) - (a.year ?? 0));
|
||||
return SliverPadding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||
sliver: SliverAlignedGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 24,
|
||||
itemCount: albums.length,
|
||||
itemBuilder: (context, i) {
|
||||
final album = albums.elementAt(i);
|
||||
return AlbumCard(
|
||||
album: album,
|
||||
subtitle: AlbumSubtitle.year,
|
||||
onTap: () => context.navigateTo(AlbumSongsRoute(
|
||||
id: album.id,
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (_, __) => SliverToBoxAdapter(
|
||||
child: Container(color: Colors.red),
|
||||
),
|
||||
loading: () => const SliverToBoxAdapter(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Title extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _Title({
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.displayMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset.fromDirection(pi / 4, 3),
|
||||
blurRadius: 16,
|
||||
color: Colors.black26,
|
||||
),
|
||||
Shadow(
|
||||
offset: Offset.fromDirection(3 * pi / 4, 3),
|
||||
blurRadius: 16,
|
||||
color: Colors.black26,
|
||||
),
|
||||
Shadow(
|
||||
offset: Offset.fromDirection(5 * pi / 4, 3),
|
||||
blurRadius: 16,
|
||||
color: Colors.black26,
|
||||
),
|
||||
Shadow(
|
||||
offset: Offset.fromDirection(7 * pi / 4, 3),
|
||||
blurRadius: 16,
|
||||
color: Colors.black26,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../now_playing_bar.dart';
|
||||
|
||||
part 'bottom_nav_page.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
TabObserver bottomTabObserver(BottomTabObserverRef ref) {
|
||||
return TabObserver();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<String> bottomTabPath(BottomTabPathRef ref) async* {
|
||||
final observer = ref.watch(bottomTabObserverProvider);
|
||||
await for (var tab in observer.path) {
|
||||
yield tab;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LastBottomNavStateService extends _$LastBottomNavStateService {
|
||||
@override
|
||||
Future<void> build() async {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final tab = ref.watch(bottomTabPathProvider).valueOrNull;
|
||||
if (tab == null || tab == 'settings' || tab == 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.saveLastBottomNavState(LastBottomNavStateData(id: 1, tab: tab));
|
||||
}
|
||||
}
|
||||
|
||||
class BottomNavTabsPage extends HookConsumerWidget {
|
||||
const BottomNavTabsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final observer = ref.watch(bottomTabObserverProvider);
|
||||
const navElevation = 3.0;
|
||||
|
||||
return AutoTabsRouter(
|
||||
lazyLoad: false,
|
||||
inheritNavigatorObservers: false,
|
||||
navigatorObservers: () => [observer],
|
||||
routes: const [
|
||||
LibraryRouter(),
|
||||
BrowseRouter(),
|
||||
SearchRouter(),
|
||||
SettingsRouter(),
|
||||
],
|
||||
builder: (context, child, animation) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light.copyWith(
|
||||
systemNavigationBarColor: ElevationOverlay.applySurfaceTint(
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surfaceTint,
|
||||
navElevation,
|
||||
),
|
||||
statusBarColor: Colors.transparent,
|
||||
),
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
const OfflineIndicator(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: const _BottomNavBar(
|
||||
navElevation: navElevation,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OfflineIndicator extends HookConsumerWidget {
|
||||
const OfflineIndicator({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final offline = ref.watch(offlineModeProvider);
|
||||
final testing = useState(false);
|
||||
|
||||
if (!offline) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 20,
|
||||
bottom: 20,
|
||||
),
|
||||
child: FilledButton.tonal(
|
||||
style: const ButtonStyle(
|
||||
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
|
||||
EdgeInsets.zero,
|
||||
),
|
||||
fixedSize: MaterialStatePropertyAll<Size>(
|
||||
Size(42, 42),
|
||||
),
|
||||
minimumSize: MaterialStatePropertyAll<Size>(
|
||||
Size(42, 42),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
testing.value = true;
|
||||
await ref.read(offlineModeProvider.notifier).setMode(false);
|
||||
testing.value = false;
|
||||
},
|
||||
child: testing.value
|
||||
? const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
)
|
||||
: const Padding(
|
||||
padding: EdgeInsets.only(left: 2, bottom: 2),
|
||||
child: Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
// color: Theme.of(context).colorScheme.secondary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomNavBar extends HookConsumerWidget {
|
||||
final double navElevation;
|
||||
|
||||
const _BottomNavBar({
|
||||
required this.navElevation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabsRouter = AutoTabsRouter.of(context);
|
||||
|
||||
useListenableSelector(tabsRouter, () => tabsRouter.activeIndex);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const NowPlayingBar(),
|
||||
NavigationBar(
|
||||
elevation: navElevation,
|
||||
height: 50,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: (index) {
|
||||
// TODO: replace this with a proper first-time setup flow
|
||||
final hasActiveSource = ref.read(settingsServiceProvider.select(
|
||||
(value) => value.activeSource != null,
|
||||
));
|
||||
|
||||
if (!hasActiveSource) {
|
||||
tabsRouter.setActiveIndex(3);
|
||||
} else {
|
||||
tabsRouter.setActiveIndex(index);
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.music_note),
|
||||
label: 'Library',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Builder(builder: (context) {
|
||||
return SvgPicture.asset(
|
||||
'assets/tag_FILL0_wght400_GRAD0_opsz24.svg',
|
||||
colorFilter: ColorFilter.mode(
|
||||
IconTheme.of(context).color!.withOpacity(
|
||||
IconTheme.of(context).opacity ?? 1,
|
||||
),
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
height: 28,
|
||||
);
|
||||
}),
|
||||
label: 'Browse',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.search_rounded),
|
||||
label: 'Search',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_rounded),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bottom_nav_page.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$bottomTabObserverHash() => r'e10c0b870f9b9052ad85fea4342569932edfeefb';
|
||||
|
||||
/// See also [bottomTabObserver].
|
||||
@ProviderFor(bottomTabObserver)
|
||||
final bottomTabObserverProvider = Provider<TabObserver>.internal(
|
||||
bottomTabObserver,
|
||||
name: r'bottomTabObserverProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$bottomTabObserverHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef BottomTabObserverRef = ProviderRef<TabObserver>;
|
||||
String _$bottomTabPathHash() => r'62539f7bf5b8f7e5f0531f564e634228ba1506bf';
|
||||
|
||||
/// See also [bottomTabPath].
|
||||
@ProviderFor(bottomTabPath)
|
||||
final bottomTabPathProvider = StreamProvider<String>.internal(
|
||||
bottomTabPath,
|
||||
name: r'bottomTabPathProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$bottomTabPathHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef BottomTabPathRef = StreamProviderRef<String>;
|
||||
String _$lastBottomNavStateServiceHash() =>
|
||||
r'487cb94cbb70884642c05a72524eb6fd7a4d12ce';
|
||||
|
||||
/// See also [LastBottomNavStateService].
|
||||
@ProviderFor(LastBottomNavStateService)
|
||||
final lastBottomNavStateServiceProvider =
|
||||
AsyncNotifierProvider<LastBottomNavStateService, void>.internal(
|
||||
LastBottomNavStateService.new,
|
||||
name: r'lastBottomNavStateServiceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$lastBottomNavStateServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LastBottomNavStateService = AsyncNotifier<void>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,281 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../models/music.dart';
|
||||
import '../../models/query.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/audio_service.dart';
|
||||
import '../../services/cache_service.dart';
|
||||
import '../../state/music.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../buttons.dart';
|
||||
import '../images.dart';
|
||||
import '../items.dart';
|
||||
|
||||
part 'browse_page.g.dart';
|
||||
|
||||
@riverpod
|
||||
Stream<List<Album>> albumsCategoryList(
|
||||
AlbumsCategoryListRef ref,
|
||||
ListQuery opt,
|
||||
) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.albumsList(sourceId, opt).watch();
|
||||
}
|
||||
|
||||
class BrowsePage extends HookConsumerWidget {
|
||||
const BrowsePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
final frequent = ref
|
||||
.watch(albumsCategoryListProvider(const ListQuery(
|
||||
page: Pagination(limit: 20),
|
||||
sort: SortBy(column: 'frequent_rank'),
|
||||
filters: IListConst([
|
||||
FilterWith.isNull(column: 'frequent_rank', invert: true),
|
||||
]),
|
||||
)))
|
||||
.valueOrNull;
|
||||
final recent = ref
|
||||
.watch(albumsCategoryListProvider(const ListQuery(
|
||||
page: Pagination(limit: 20),
|
||||
sort: SortBy(column: 'recent_rank'),
|
||||
filters: IListConst([
|
||||
FilterWith.isNull(column: 'recent_rank', invert: true),
|
||||
]),
|
||||
)))
|
||||
.valueOrNull;
|
||||
final starred = ref
|
||||
.watch(albumsCategoryListProvider(const ListQuery(
|
||||
page: Pagination(limit: 20),
|
||||
sort: SortBy(column: 'starred'),
|
||||
filters: IListConst([
|
||||
FilterWith.isNull(column: 'starred', invert: true),
|
||||
]),
|
||||
)))
|
||||
.valueOrNull;
|
||||
final random = ref
|
||||
.watch(albumsCategoryListProvider(const ListQuery(
|
||||
page: Pagination(limit: 20),
|
||||
sort: SortBy(column: 'RANDOM()'),
|
||||
)))
|
||||
.valueOrNull;
|
||||
|
||||
final genres = ref
|
||||
.watch(albumGenresProvider(const Pagination(
|
||||
limit: 20,
|
||||
)))
|
||||
.valueOrNull;
|
||||
|
||||
return Scaffold(
|
||||
floatingActionButton: RadioPlayFab(
|
||||
onPressed: () {
|
||||
ref.read(audioControlProvider).playRadio(
|
||||
context: QueueContextType.library,
|
||||
getSongs: (query) => ref
|
||||
.read(databaseProvider)
|
||||
.songsList(ref.read(sourceIdProvider), query)
|
||||
.get(),
|
||||
);
|
||||
},
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverSafeArea(
|
||||
sliver: SliverPadding(padding: EdgeInsets.only(top: 8)),
|
||||
),
|
||||
_GenreCategory(
|
||||
title: 'Genres',
|
||||
items: genres?.toList() ?? [],
|
||||
),
|
||||
_AlbumCategory(
|
||||
title: l.resourcesSortByFrequentlyPlayed,
|
||||
items: frequent ?? [],
|
||||
),
|
||||
_AlbumCategory(
|
||||
title: l.resourcesSortByRecentlyPlayed,
|
||||
items: recent ?? [],
|
||||
),
|
||||
_AlbumCategory(
|
||||
title: l.resourcesFilterStarred,
|
||||
items: starred ?? [],
|
||||
),
|
||||
_AlbumCategory(
|
||||
title: l.resourcesSortByRandom,
|
||||
items: random ?? [],
|
||||
),
|
||||
const SliverToBoxAdapter(child: FabPadding()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GenreCategory extends HookConsumerWidget {
|
||||
final String title;
|
||||
final List<String> items;
|
||||
|
||||
const _GenreCategory({
|
||||
required this.title,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
sliver: _Category(
|
||||
title: title,
|
||||
height: 140,
|
||||
itemWidth: 140,
|
||||
items: items.map((genre) => _GenreItem(genre: genre)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GenreItem extends HookConsumerWidget {
|
||||
final String genre;
|
||||
|
||||
const _GenreItem({
|
||||
required this.genre,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final albums = ref
|
||||
.watch(albumsByGenreProvider(
|
||||
genre,
|
||||
const Pagination(limit: 4),
|
||||
))
|
||||
.valueOrNull;
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (albums == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return ImageCard(
|
||||
onTap: () {
|
||||
context.navigateTo(GenreSongsRoute(genre: genre));
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
CardClip(
|
||||
child: MultiImage(
|
||||
cacheInfo: albums.map((album) => cache.albumArt(album)),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
type: MaterialType.canvas,
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
genre,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumCategory extends HookConsumerWidget {
|
||||
final String title;
|
||||
final List<Album> items;
|
||||
|
||||
const _AlbumCategory({
|
||||
required this.title,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _Category(
|
||||
title: title,
|
||||
height: 190,
|
||||
itemWidth: 140,
|
||||
items: items
|
||||
.map(
|
||||
(album) => AlbumCard(
|
||||
album: album,
|
||||
onTap: () => context.navigateTo(
|
||||
AlbumSongsRoute(
|
||||
id: album.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Category extends HookConsumerWidget {
|
||||
final String title;
|
||||
final List<Widget> items;
|
||||
final double height;
|
||||
final double itemWidth;
|
||||
|
||||
const _Category({
|
||||
required this.title,
|
||||
required this.items,
|
||||
required this.height,
|
||||
required this.itemWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return MultiSliver(
|
||||
children: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => SizedBox(
|
||||
width: itemWidth,
|
||||
child: items[index],
|
||||
),
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'browse_page.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$albumsCategoryListHash() =>
|
||||
r'e0516a585bf39e8140c72c08fd41f33a817c747d';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
typedef AlbumsCategoryListRef = AutoDisposeStreamProviderRef<List<Album>>;
|
||||
|
||||
/// See also [albumsCategoryList].
|
||||
@ProviderFor(albumsCategoryList)
|
||||
const albumsCategoryListProvider = AlbumsCategoryListFamily();
|
||||
|
||||
/// See also [albumsCategoryList].
|
||||
class AlbumsCategoryListFamily extends Family<AsyncValue<List<Album>>> {
|
||||
/// See also [albumsCategoryList].
|
||||
const AlbumsCategoryListFamily();
|
||||
|
||||
/// See also [albumsCategoryList].
|
||||
AlbumsCategoryListProvider call(
|
||||
ListQuery opt,
|
||||
) {
|
||||
return AlbumsCategoryListProvider(
|
||||
opt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AlbumsCategoryListProvider getProviderOverride(
|
||||
covariant AlbumsCategoryListProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.opt,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'albumsCategoryListProvider';
|
||||
}
|
||||
|
||||
/// See also [albumsCategoryList].
|
||||
class AlbumsCategoryListProvider
|
||||
extends AutoDisposeStreamProvider<List<Album>> {
|
||||
/// See also [albumsCategoryList].
|
||||
AlbumsCategoryListProvider(
|
||||
this.opt,
|
||||
) : super.internal(
|
||||
(ref) => albumsCategoryList(
|
||||
ref,
|
||||
opt,
|
||||
),
|
||||
from: albumsCategoryListProvider,
|
||||
name: r'albumsCategoryListProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$albumsCategoryListHash,
|
||||
dependencies: AlbumsCategoryListFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
AlbumsCategoryListFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final ListQuery opt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is AlbumsCategoryListProvider && other.opt == opt;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, opt.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../hooks/use_list_query_paging_controller.dart';
|
||||
import '../items.dart';
|
||||
import '../lists.dart';
|
||||
|
||||
class LibraryAlbumsPage extends HookConsumerWidget {
|
||||
const LibraryAlbumsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pagingController = useLibraryPagingController(
|
||||
ref,
|
||||
libraryTabIndex: 0,
|
||||
getItems: (query) {
|
||||
final db = ref.read(databaseProvider);
|
||||
final sourceId = ref.read(sourceIdProvider);
|
||||
|
||||
return ref.read(offlineModeProvider)
|
||||
? db.albumsListDownloaded(sourceId, query).get()
|
||||
: db.albumsList(sourceId, query).get();
|
||||
},
|
||||
);
|
||||
|
||||
return PagedGridQueryView(
|
||||
pagingController: pagingController,
|
||||
refreshSyncAll: true,
|
||||
itemBuilder: (context, item, index, size) => AlbumCard(
|
||||
album: item,
|
||||
style:
|
||||
size == GridSize.small ? CardStyle.imageOnly : CardStyle.withText,
|
||||
onTap: () => context.navigateTo(AlbumSongsRoute(id: item.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../hooks/use_list_query_paging_controller.dart';
|
||||
import '../items.dart';
|
||||
import '../lists.dart';
|
||||
|
||||
class LibraryArtistsPage extends HookConsumerWidget {
|
||||
const LibraryArtistsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pagingController = useLibraryPagingController(
|
||||
ref,
|
||||
libraryTabIndex: 1,
|
||||
getItems: (query) {
|
||||
final db = ref.read(databaseProvider);
|
||||
final sourceId = ref.read(sourceIdProvider);
|
||||
|
||||
return ref.read(offlineModeProvider)
|
||||
? db.artistsListDownloaded(sourceId, query).get()
|
||||
: db.artistsList(sourceId, query).get();
|
||||
},
|
||||
);
|
||||
|
||||
return PagedListQueryView(
|
||||
pagingController: pagingController,
|
||||
refreshSyncAll: true,
|
||||
itemBuilder: (context, item, index) => ArtistListTile(
|
||||
artist: item,
|
||||
onTap: () => context.navigateTo(ArtistRoute(id: item.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,635 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../models/query.dart';
|
||||
import '../app_router.dart';
|
||||
import '../context_menus.dart';
|
||||
|
||||
part 'library_page.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
TabObserver libraryTabObserver(LibraryTabObserverRef ref) {
|
||||
return TabObserver();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<String> libraryTabPath(LibraryTabPathRef ref) async* {
|
||||
final observer = ref.watch(libraryTabObserverProvider);
|
||||
await for (var tab in observer.path) {
|
||||
yield tab;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LastLibraryStateService extends _$LastLibraryStateService {
|
||||
@override
|
||||
Future<void> build() async {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final tab = await ref.watch(libraryTabPathProvider.future);
|
||||
|
||||
await db.saveLastLibraryState(LastLibraryStateData(
|
||||
id: 1,
|
||||
tab: tab,
|
||||
albumsList: ref.watch(libraryListQueryProvider(0)).query,
|
||||
artistsList: ref.watch(libraryListQueryProvider(1)).query,
|
||||
playlistsList: ref.watch(libraryListQueryProvider(2)).query,
|
||||
songsList: ref.watch(libraryListQueryProvider(3)).query,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LibraryLists extends _$LibraryLists {
|
||||
@override
|
||||
IList<LibraryListQuery> build() {
|
||||
return const IListConst([
|
||||
/// Albums
|
||||
LibraryListQuery(
|
||||
options: ListQueryOptions(
|
||||
sortColumns: IListConst([
|
||||
'albums.name',
|
||||
'albums.created',
|
||||
'albums.album_artist',
|
||||
'albums.year',
|
||||
]),
|
||||
filterColumns: IListConst([
|
||||
'albums.starred',
|
||||
'albums.album_artist',
|
||||
'albums.year',
|
||||
'albums.genre',
|
||||
]),
|
||||
),
|
||||
query: ListQuery(
|
||||
page: Pagination(limit: 60),
|
||||
sort: SortBy(column: 'albums.name'),
|
||||
),
|
||||
),
|
||||
|
||||
/// Artists
|
||||
LibraryListQuery(
|
||||
options: ListQueryOptions(
|
||||
sortColumns: IListConst([
|
||||
'artists.name',
|
||||
'artists.album_count',
|
||||
]),
|
||||
filterColumns: IListConst([
|
||||
'artists.starred',
|
||||
]),
|
||||
),
|
||||
query: ListQuery(
|
||||
page: Pagination(limit: 30),
|
||||
sort: SortBy(column: 'artists.name'),
|
||||
),
|
||||
),
|
||||
|
||||
/// Playlists
|
||||
LibraryListQuery(
|
||||
options: ListQueryOptions(
|
||||
sortColumns: IListConst([
|
||||
'playlists.name',
|
||||
'playlists.created',
|
||||
'playlists.changed',
|
||||
]),
|
||||
filterColumns: IListConst([
|
||||
'playlists.owner',
|
||||
]),
|
||||
),
|
||||
query: ListQuery(
|
||||
page: Pagination(limit: 30),
|
||||
sort: SortBy(column: 'playlists.name'),
|
||||
),
|
||||
),
|
||||
|
||||
/// Songs
|
||||
LibraryListQuery(
|
||||
options: ListQueryOptions(
|
||||
sortColumns: IListConst([
|
||||
'songs.album',
|
||||
'songs.artist',
|
||||
'songs.created',
|
||||
'songs.title',
|
||||
'songs.year',
|
||||
]),
|
||||
filterColumns: IListConst([
|
||||
'songs.starred',
|
||||
'songs.artist',
|
||||
'songs.album',
|
||||
'songs.year',
|
||||
'songs.genre',
|
||||
]),
|
||||
),
|
||||
query: ListQuery(
|
||||
page: Pagination(limit: 30),
|
||||
sort: SortBy(column: 'songs.album'),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final last = await db.getLastLibraryState().getSingleOrNull();
|
||||
if (last == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = state
|
||||
.replace(0, state[0].copyWith(query: last.albumsList))
|
||||
.replace(1, state[1].copyWith(query: last.artistsList))
|
||||
.replace(2, state[2].copyWith(query: last.playlistsList))
|
||||
.replace(3, state[3].copyWith(query: last.songsList));
|
||||
}
|
||||
|
||||
void setSortColumn(int index, String column) {
|
||||
state = state.replace(
|
||||
index,
|
||||
state[index].copyWith.query.sort!(column: column),
|
||||
);
|
||||
}
|
||||
|
||||
void toggleDirection(int index) {
|
||||
final toggled = state[index].query.sort?.dir == SortDirection.asc
|
||||
? SortDirection.desc
|
||||
: SortDirection.asc;
|
||||
state = state.replace(
|
||||
index,
|
||||
state[index].copyWith.query.sort!(dir: toggled),
|
||||
);
|
||||
}
|
||||
|
||||
void setFilter(int index, FilterWith filter) {
|
||||
state = state.replace(
|
||||
index,
|
||||
state[index].copyWith.query(
|
||||
filters: state[index].query.filters.updateById(
|
||||
[filter],
|
||||
(e) => e.column,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void removeFilter(int index, String column) {
|
||||
state = state.replace(
|
||||
index,
|
||||
state[index].copyWith.query(
|
||||
filters: state[index]
|
||||
.query
|
||||
.filters
|
||||
.removeWhere((f) => f.column == column)),
|
||||
);
|
||||
}
|
||||
|
||||
void clearFilters(int index) {
|
||||
state = state.replace(index, state[index].copyWith.query(filters: IList()));
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
LibraryListQuery libraryListQuery(LibraryListQueryRef ref, int index) {
|
||||
return ref.watch(libraryListsProvider.select((value) => value[index]));
|
||||
}
|
||||
|
||||
class LibraryTabsPage extends HookConsumerWidget {
|
||||
const LibraryTabsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final observer = ref.watch(libraryTabObserverProvider);
|
||||
|
||||
return AutoTabsRouter.tabBar(
|
||||
inheritNavigatorObservers: false,
|
||||
navigatorObservers: () => [observer],
|
||||
routes: const [
|
||||
LibraryAlbumsRoute(),
|
||||
LibraryArtistsRoute(),
|
||||
LibraryPlaylistsRoute(),
|
||||
LibrarySongsRoute(),
|
||||
],
|
||||
builder: (context, child, tabController) {
|
||||
return Scaffold(
|
||||
body: child,
|
||||
floatingActionButton: const _LibraryFilterFab(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LibraryFilterFab extends HookConsumerWidget {
|
||||
const _LibraryFilterFab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabsRouter = AutoTabsRouter.of(context);
|
||||
final activeIndex =
|
||||
useListenableSelector(tabsRouter, () => tabsRouter.activeIndex);
|
||||
final tabHasFilters = ref.watch(libraryListQueryProvider(activeIndex)
|
||||
.select((value) => value.query.filters.isNotEmpty));
|
||||
|
||||
List<Widget> dot = [];
|
||||
if (tabHasFilters) {
|
||||
dot.addAll([
|
||||
PositionedDirectional(
|
||||
top: 3,
|
||||
end: 0,
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
size: 11,
|
||||
),
|
||||
),
|
||||
const PositionedDirectional(
|
||||
top: 5,
|
||||
end: 1,
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
size: 7,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () async {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
child: LibraryMenu(
|
||||
tabsRouter: tabsRouter,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'List',
|
||||
child: Stack(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.sort_rounded,
|
||||
size: 28,
|
||||
),
|
||||
...dot,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryMenu extends HookConsumerWidget {
|
||||
final TabsRouter tabsRouter;
|
||||
|
||||
const LibraryMenu({
|
||||
super.key,
|
||||
required this.tabsRouter,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FilterChip(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
onSelected: (value) {},
|
||||
selected: true,
|
||||
label: const Icon(
|
||||
Icons.grid_on,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
FilterChip(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
onSelected: (value) {},
|
||||
label: const Icon(
|
||||
Icons.grid_view_outlined,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
FilterChip(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
onSelected: (value) {},
|
||||
label: const Icon(
|
||||
Icons.format_list_bulleted,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_FilterToggleButton(tabsRouter: tabsRouter),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
||||
ListSortFilterOptions(index: tabsRouter.activeIndex),
|
||||
const SliverPadding(padding: EdgeInsets.only(top: 16)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterToggleButton extends HookConsumerWidget {
|
||||
final TabsRouter tabsRouter;
|
||||
|
||||
const _FilterToggleButton({
|
||||
required this.tabsRouter,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabHasFilters = ref.watch(
|
||||
libraryListQueryProvider(tabsRouter.activeIndex)
|
||||
.select((value) => value.query.filters.isNotEmpty));
|
||||
|
||||
return FilledButton(
|
||||
onPressed: tabHasFilters
|
||||
? () {
|
||||
ref
|
||||
.read(libraryListsProvider.notifier)
|
||||
.clearFilters(tabsRouter.activeIndex);
|
||||
}
|
||||
: null,
|
||||
child: const Icon(Icons.filter_list_off_rounded),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ListSortFilterOptions extends HookConsumerWidget {
|
||||
final int index;
|
||||
|
||||
const ListSortFilterOptions({
|
||||
super.key,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
void Function()? _filterOnEdit(
|
||||
String column,
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final type = column.split('.').last;
|
||||
switch (type) {
|
||||
case 'year':
|
||||
return () {
|
||||
// TODO: year filter dialog
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return Dialog(
|
||||
// child: Text('adsf'),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
};
|
||||
case 'genre':
|
||||
case 'album_artist':
|
||||
case 'owner':
|
||||
case 'album':
|
||||
case 'artist':
|
||||
// TODO: other filter dialogs
|
||||
return () {};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void Function(bool? value)? _filterOnChanged(String column, WidgetRef ref) {
|
||||
final type = column.split('.').last;
|
||||
switch (type) {
|
||||
case 'starred':
|
||||
return (value) {
|
||||
if (value!) {
|
||||
ref.read(libraryListsProvider.notifier).setFilter(
|
||||
index,
|
||||
FilterWith.isNull(column: column, invert: true),
|
||||
);
|
||||
} else {
|
||||
ref.read(libraryListsProvider.notifier).removeFilter(index, column);
|
||||
}
|
||||
};
|
||||
case 'year':
|
||||
// TODO: add/remove filter
|
||||
return null;
|
||||
case 'genre':
|
||||
case 'album_artist':
|
||||
case 'owner':
|
||||
case 'album':
|
||||
case 'artist':
|
||||
// TODO: add/remove filter
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final list = ref.watch(libraryListQueryProvider(index));
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate.fixed([
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Sort by',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
for (var column in list.options.sortColumns)
|
||||
SortOptionTile(
|
||||
column: column,
|
||||
value: list.query.sort!.copyWith(column: column),
|
||||
groupValue: list.query.sort!,
|
||||
onColumnChanged: (column) {
|
||||
if (column != null) {
|
||||
ref
|
||||
.read(libraryListsProvider.notifier)
|
||||
.setSortColumn(index, column);
|
||||
}
|
||||
},
|
||||
onDirectionToggle: () =>
|
||||
ref.read(libraryListsProvider.notifier).toggleDirection(index),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
'Filter',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
for (var column in list.options.filterColumns)
|
||||
FilterOptionTile(
|
||||
column: column,
|
||||
state: list.query.filters.singleWhereOrNull(
|
||||
(e) => e.column == column,
|
||||
),
|
||||
onEdit: _filterOnEdit(column, context, ref),
|
||||
onChanged: _filterOnChanged(column, ref),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SortOptionTile extends HookConsumerWidget {
|
||||
final String column;
|
||||
final SortBy value;
|
||||
final SortBy groupValue;
|
||||
final void Function(String? value) onColumnChanged;
|
||||
final void Function() onDirectionToggle;
|
||||
|
||||
const SortOptionTile({
|
||||
super.key,
|
||||
required this.column,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onColumnChanged,
|
||||
required this.onDirectionToggle,
|
||||
});
|
||||
|
||||
String _sortTitle(AppLocalizations l, String type) {
|
||||
type = type.split('.').last;
|
||||
switch (type) {
|
||||
case 'name':
|
||||
return l.resourcesSortByName;
|
||||
case 'album_artist':
|
||||
return l.resourcesSortByArtist;
|
||||
case 'created':
|
||||
return l.resourcesSortByAdded;
|
||||
case 'year':
|
||||
return l.resourcesSortByYear;
|
||||
case 'album_count':
|
||||
return l.resourcesSortByAlbumCount;
|
||||
case 'changed':
|
||||
return l.resourcesSortByUpdated;
|
||||
case 'album':
|
||||
return l.resourcesSortByAlbum;
|
||||
case 'artist':
|
||||
return l.resourcesSortByArtist;
|
||||
case 'title':
|
||||
return l.resourcesSortByTitle;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return RadioListTile<String?>(
|
||||
value: value.column,
|
||||
groupValue: groupValue.column,
|
||||
onChanged: onColumnChanged,
|
||||
selected: value.column == groupValue.column,
|
||||
title: Text(_sortTitle(l, column)),
|
||||
secondary: value.column == groupValue.column
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
value.dir == SortDirection.desc
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
),
|
||||
onPressed: onDirectionToggle,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FilterOptionTile extends HookConsumerWidget {
|
||||
final String column;
|
||||
final FilterWith? state;
|
||||
final void Function(bool? value)? onChanged;
|
||||
final void Function()? onEdit;
|
||||
|
||||
const FilterOptionTile({
|
||||
super.key,
|
||||
required this.column,
|
||||
required this.state,
|
||||
required this.onChanged,
|
||||
this.onEdit,
|
||||
});
|
||||
|
||||
String _filterTitle(AppLocalizations l, String type) {
|
||||
type = type.split('.').last;
|
||||
switch (type) {
|
||||
case 'starred':
|
||||
return l.resourcesFilterStarred;
|
||||
case 'year':
|
||||
return l.resourcesFilterYear;
|
||||
case 'genre':
|
||||
return l.resourcesFilterGenre;
|
||||
case 'album_artist':
|
||||
return l.resourcesFilterArtist;
|
||||
case 'owner':
|
||||
return l.resourcesFilterOwner;
|
||||
case 'album':
|
||||
return l.resourcesFilterAlbum;
|
||||
case 'artist':
|
||||
return l.resourcesFilterArtist;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return CheckboxListTile(
|
||||
value: state == null
|
||||
? false
|
||||
: state!.map(
|
||||
equals: (value) => value.invert ? null : true,
|
||||
greaterThan: (value) => true,
|
||||
isNull: (_) => true,
|
||||
betweenInt: (_) => true,
|
||||
isIn: (value) => value.invert ? null : true,
|
||||
),
|
||||
tristate: state?.map(
|
||||
equals: (value) => true,
|
||||
greaterThan: (value) => false,
|
||||
isNull: (_) => false,
|
||||
betweenInt: (_) => false,
|
||||
isIn: (_) => true,
|
||||
) ??
|
||||
false,
|
||||
title: Text(_filterTitle(l, column)),
|
||||
secondary: onEdit == null
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.edit_rounded),
|
||||
onPressed: onEdit,
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'library_page.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$libraryTabObserverHash() =>
|
||||
r'a976ea55e2168e4684114c47592f25a2b187f15f';
|
||||
|
||||
/// See also [libraryTabObserver].
|
||||
@ProviderFor(libraryTabObserver)
|
||||
final libraryTabObserverProvider = Provider<TabObserver>.internal(
|
||||
libraryTabObserver,
|
||||
name: r'libraryTabObserverProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$libraryTabObserverHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef LibraryTabObserverRef = ProviderRef<TabObserver>;
|
||||
String _$libraryTabPathHash() => r'fe60984ea9d629683d344f809749b1b9362735fa';
|
||||
|
||||
/// See also [libraryTabPath].
|
||||
@ProviderFor(libraryTabPath)
|
||||
final libraryTabPathProvider = StreamProvider<String>.internal(
|
||||
libraryTabPath,
|
||||
name: r'libraryTabPathProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$libraryTabPathHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef LibraryTabPathRef = StreamProviderRef<String>;
|
||||
String _$libraryListQueryHash() => r'6079338e19e0249aaa09868dd405fd3aefc42c2b';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
typedef LibraryListQueryRef = ProviderRef<LibraryListQuery>;
|
||||
|
||||
/// See also [libraryListQuery].
|
||||
@ProviderFor(libraryListQuery)
|
||||
const libraryListQueryProvider = LibraryListQueryFamily();
|
||||
|
||||
/// See also [libraryListQuery].
|
||||
class LibraryListQueryFamily extends Family<LibraryListQuery> {
|
||||
/// See also [libraryListQuery].
|
||||
const LibraryListQueryFamily();
|
||||
|
||||
/// See also [libraryListQuery].
|
||||
LibraryListQueryProvider call(
|
||||
int index,
|
||||
) {
|
||||
return LibraryListQueryProvider(
|
||||
index,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LibraryListQueryProvider getProviderOverride(
|
||||
covariant LibraryListQueryProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.index,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'libraryListQueryProvider';
|
||||
}
|
||||
|
||||
/// See also [libraryListQuery].
|
||||
class LibraryListQueryProvider extends Provider<LibraryListQuery> {
|
||||
/// See also [libraryListQuery].
|
||||
LibraryListQueryProvider(
|
||||
this.index,
|
||||
) : super.internal(
|
||||
(ref) => libraryListQuery(
|
||||
ref,
|
||||
index,
|
||||
),
|
||||
from: libraryListQueryProvider,
|
||||
name: r'libraryListQueryProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$libraryListQueryHash,
|
||||
dependencies: LibraryListQueryFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
LibraryListQueryFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final int index;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is LibraryListQueryProvider && other.index == index;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, index.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
String _$lastLibraryStateServiceHash() =>
|
||||
r'a49e26b5dc0fcb0f697ec2def08e7336f64c4cb3';
|
||||
|
||||
/// See also [LastLibraryStateService].
|
||||
@ProviderFor(LastLibraryStateService)
|
||||
final lastLibraryStateServiceProvider =
|
||||
AsyncNotifierProvider<LastLibraryStateService, void>.internal(
|
||||
LastLibraryStateService.new,
|
||||
name: r'lastLibraryStateServiceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$lastLibraryStateServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LastLibraryStateService = AsyncNotifier<void>;
|
||||
String _$libraryListsHash() => r'7c9fd1ca3b0d70253e0f5d8197abf18b3a18c995';
|
||||
|
||||
/// See also [LibraryLists].
|
||||
@ProviderFor(LibraryLists)
|
||||
final libraryListsProvider =
|
||||
NotifierProvider<LibraryLists, IList<LibraryListQuery>>.internal(
|
||||
LibraryLists.new,
|
||||
name: r'libraryListsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$libraryListsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LibraryLists = Notifier<IList<LibraryListQuery>>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../hooks/use_list_query_paging_controller.dart';
|
||||
import '../items.dart';
|
||||
import '../lists.dart';
|
||||
|
||||
class LibraryPlaylistsPage extends HookConsumerWidget {
|
||||
const LibraryPlaylistsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pagingController = useLibraryPagingController(
|
||||
ref,
|
||||
libraryTabIndex: 2,
|
||||
getItems: (query) {
|
||||
final db = ref.read(databaseProvider);
|
||||
final sourceId = ref.read(sourceIdProvider);
|
||||
|
||||
return ref.read(offlineModeProvider)
|
||||
? db.playlistsListDownloaded(sourceId, query).get()
|
||||
: db.playlistsList(sourceId, query).get();
|
||||
},
|
||||
);
|
||||
|
||||
return PagedListQueryView(
|
||||
pagingController: pagingController,
|
||||
refreshSyncAll: true,
|
||||
itemBuilder: (context, item, index) => PlaylistListTile(
|
||||
playlist: item,
|
||||
onTap: () => context.navigateTo(PlaylistSongsRoute(id: item.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../models/music.dart';
|
||||
import '../../models/query.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/audio_service.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../hooks/use_list_query_paging_controller.dart';
|
||||
import '../items.dart';
|
||||
import '../lists.dart';
|
||||
import 'library_page.dart';
|
||||
import 'songs_page.dart';
|
||||
|
||||
part 'library_songs_page.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<Song>> songsList(SongsListRef ref, ListQuery opt) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.songsList(sourceId, opt).get();
|
||||
}
|
||||
|
||||
class LibrarySongsPage extends HookConsumerWidget {
|
||||
const LibrarySongsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
|
||||
final query = ref.watch(libraryListQueryProvider(3).select(
|
||||
(value) => value.query,
|
||||
));
|
||||
|
||||
final getSongs = useCallback(
|
||||
(ListQuery query) {
|
||||
final db = ref.read(databaseProvider);
|
||||
final sourceId = ref.read(sourceIdProvider);
|
||||
|
||||
return ref.read(offlineModeProvider)
|
||||
? db.songsListDownloaded(sourceId, query).get()
|
||||
: db.songsList(sourceId, query).get();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
final play = useCallback(
|
||||
({int? index, bool? shuffle}) => audio.playSongs(
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
startIndex: index,
|
||||
context: QueueContextType.song,
|
||||
shuffle: shuffle,
|
||||
),
|
||||
[query, getSongs],
|
||||
);
|
||||
|
||||
final pagingController = useLibraryPagingController(
|
||||
ref,
|
||||
libraryTabIndex: 3,
|
||||
getItems: getSongs,
|
||||
);
|
||||
|
||||
return PagedListQueryView(
|
||||
pagingController: pagingController,
|
||||
refreshSyncAll: true,
|
||||
itemBuilder: (context, item, index) => QueueContext(
|
||||
type: QueueContextType.song,
|
||||
child: SongListTile(
|
||||
song: item,
|
||||
image: true,
|
||||
onTap: () => play(index: index),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'library_songs_page.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$songsListHash() => r'a3149eb61f8f1ff326e9b1de0ac1c02d7baa831f';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongsListRef = AutoDisposeFutureProviderRef<List<Song>>;
|
||||
|
||||
/// See also [songsList].
|
||||
@ProviderFor(songsList)
|
||||
const songsListProvider = SongsListFamily();
|
||||
|
||||
/// See also [songsList].
|
||||
class SongsListFamily extends Family<AsyncValue<List<Song>>> {
|
||||
/// See also [songsList].
|
||||
const SongsListFamily();
|
||||
|
||||
/// See also [songsList].
|
||||
SongsListProvider call(
|
||||
ListQuery opt,
|
||||
) {
|
||||
return SongsListProvider(
|
||||
opt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SongsListProvider getProviderOverride(
|
||||
covariant SongsListProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.opt,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'songsListProvider';
|
||||
}
|
||||
|
||||
/// See also [songsList].
|
||||
class SongsListProvider extends AutoDisposeFutureProvider<List<Song>> {
|
||||
/// See also [songsList].
|
||||
SongsListProvider(
|
||||
this.opt,
|
||||
) : super.internal(
|
||||
(ref) => songsList(
|
||||
ref,
|
||||
opt,
|
||||
),
|
||||
from: songsListProvider,
|
||||
name: r'songsListProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$songsListHash,
|
||||
dependencies: SongsListFamily._dependencies,
|
||||
allTransitiveDependencies: SongsListFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final ListQuery opt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SongsListProvider && other.opt == opt;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, opt.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,429 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:text_scroll/text_scroll.dart';
|
||||
|
||||
import '../../cache/image_cache.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/audio_service.dart';
|
||||
import '../../state/audio.dart';
|
||||
import '../../state/theme.dart';
|
||||
import '../context_menus.dart';
|
||||
import '../gradient.dart';
|
||||
import '../images.dart';
|
||||
import '../now_playing_bar.dart';
|
||||
|
||||
class NowPlayingPage extends HookConsumerWidget {
|
||||
const NowPlayingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
||||
final itemData = ref.watch(mediaItemDataProvider);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final scaffold = AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light.copyWith(
|
||||
systemNavigationBarColor: colors?.gradientLow,
|
||||
statusBarColor: Colors.transparent,
|
||||
),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
itemData?.contextType.value ?? '',
|
||||
style: theme.textTheme.labelMedium,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
// Text(
|
||||
// itemData?.contextTitle ?? '',
|
||||
// style: theme.textTheme.titleMedium,
|
||||
// maxLines: 1,
|
||||
// softWrap: false,
|
||||
// overflow: TextOverflow.fade,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
const MediaItemGradient(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Column(
|
||||
children: const [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _Art(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _TrackInfo(),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_Progress(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _Controls(),
|
||||
),
|
||||
SizedBox(height: 64),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (colors != null) {
|
||||
return Theme(data: colors.theme, child: scaffold);
|
||||
} else {
|
||||
return scaffold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Art extends HookConsumerWidget {
|
||||
const _Art();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final itemData = ref.watch(mediaItemDataProvider);
|
||||
final imageCache = ref.watch(imageCacheProvider);
|
||||
|
||||
UriCacheInfo? cacheInfo;
|
||||
if (itemData?.artCache != null) {
|
||||
cacheInfo = UriCacheInfo(
|
||||
uri: itemData!.artCache!.fullArtUri,
|
||||
cacheKey: itemData.artCache!.fullArtCacheKey,
|
||||
cacheManager: imageCache,
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: CardClip(
|
||||
key: ValueKey(cacheInfo?.cacheKey ?? 'default'),
|
||||
child: cacheInfo != null
|
||||
? CardClip(
|
||||
square: false,
|
||||
child: UriCacheInfoImage(
|
||||
// height: 300,
|
||||
fit: BoxFit.contain,
|
||||
placeholderStyle: PlaceholderStyle.spinner,
|
||||
cache: cacheInfo,
|
||||
),
|
||||
)
|
||||
: const PlaceholderImage(thumbnail: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackInfo extends HookConsumerWidget {
|
||||
const _TrackInfo();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final item = ref.watch(mediaItemProvider).valueOrNull;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ScrollableText(
|
||||
item?.title ?? '',
|
||||
style: theme.textTheme.headlineSmall,
|
||||
speed: 50,
|
||||
),
|
||||
Text(
|
||||
item?.artist ?? '',
|
||||
style: theme.textTheme.titleMedium!,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.star_outline_rounded,
|
||||
size: 36,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollableText extends StatelessWidget {
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
final double speed;
|
||||
|
||||
const ScrollableText(
|
||||
this.text, {
|
||||
super.key,
|
||||
this.style,
|
||||
this.speed = 35,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultStyle = DefaultTextStyle.of(context);
|
||||
|
||||
return AutoSizeText(
|
||||
text,
|
||||
presetFontSizes: style != null && style?.fontSize != null
|
||||
? [style!.fontSize!]
|
||||
: [defaultStyle.style.fontSize ?? 12],
|
||||
style: style,
|
||||
maxLines: 1,
|
||||
// softWrap: false,
|
||||
overflowReplacement: TextScroll(
|
||||
'$text ',
|
||||
style: style,
|
||||
delayBefore: const Duration(seconds: 3),
|
||||
pauseBetween: const Duration(seconds: 4),
|
||||
mode: TextScrollMode.endless,
|
||||
velocity: Velocity(pixelsPerSecond: Offset(speed, 0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Progress extends HookConsumerWidget {
|
||||
const _Progress();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
||||
final position = ref.watch(positionProvider);
|
||||
final duration = ref.watch(durationProvider);
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
|
||||
final changeValue = useState(position.toDouble());
|
||||
final changing = useState(false);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Slider(
|
||||
value: changing.value ? changeValue.value : position.toDouble(),
|
||||
min: 0,
|
||||
max: max(duration.toDouble(), position.toDouble()),
|
||||
thumbColor: colors?.theme.colorScheme.onBackground,
|
||||
activeColor: colors?.theme.colorScheme.onBackground,
|
||||
inactiveColor: colors?.theme.colorScheme.surface,
|
||||
onChanged: (value) {
|
||||
changeValue.value = value;
|
||||
},
|
||||
onChangeStart: (value) {
|
||||
changing.value = true;
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
changing.value = false;
|
||||
audio.seek(Duration(seconds: value.toInt()));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.titleMedium!,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(Duration(
|
||||
seconds: changing.value
|
||||
? changeValue.value.toInt()
|
||||
: position)
|
||||
.toString()
|
||||
.substring(2, 7)),
|
||||
Text(Duration(seconds: duration).toString().substring(2, 7)),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RepeatButton extends HookConsumerWidget {
|
||||
final double size;
|
||||
|
||||
const RepeatButton({
|
||||
super.key,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
final repeat = ref.watch(repeatModeProvider);
|
||||
|
||||
IconData icon;
|
||||
void Function() action;
|
||||
|
||||
switch (repeat) {
|
||||
case AudioServiceRepeatMode.all:
|
||||
case AudioServiceRepeatMode.group:
|
||||
icon = Icons.repeat_on_rounded;
|
||||
action = () => audio.setRepeatMode(AudioServiceRepeatMode.one);
|
||||
break;
|
||||
case AudioServiceRepeatMode.one:
|
||||
icon = Icons.repeat_one_on_rounded;
|
||||
action = () => audio.setRepeatMode(AudioServiceRepeatMode.none);
|
||||
break;
|
||||
default:
|
||||
icon = Icons.repeat_rounded;
|
||||
action = () => audio.setRepeatMode(AudioServiceRepeatMode.all);
|
||||
break;
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 30,
|
||||
onPressed: action,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShuffleButton extends HookConsumerWidget {
|
||||
final double size;
|
||||
|
||||
const ShuffleButton({
|
||||
super.key,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
final shuffle = ref.watch(shuffleModeProvider);
|
||||
final queueMode = ref.watch(queueModeProvider).valueOrNull;
|
||||
|
||||
IconData icon;
|
||||
void Function() action;
|
||||
|
||||
switch (shuffle) {
|
||||
case AudioServiceShuffleMode.all:
|
||||
case AudioServiceShuffleMode.group:
|
||||
icon = Icons.shuffle_on_rounded;
|
||||
action = () => audio.setShuffleMode(AudioServiceShuffleMode.none);
|
||||
break;
|
||||
default:
|
||||
icon = Icons.shuffle_rounded;
|
||||
action = () => audio.setShuffleMode(AudioServiceShuffleMode.all);
|
||||
break;
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(queueMode == QueueMode.radio ? Icons.radio_rounded : icon),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 30,
|
||||
onPressed: queueMode == QueueMode.radio ? null : action,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Controls extends HookConsumerWidget {
|
||||
const _Controls();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final base = ref.watch(baseThemeProvider);
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
|
||||
return IconTheme(
|
||||
data: IconThemeData(color: base.theme.colorScheme.onBackground),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const RepeatButton(size: 30),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_previous_rounded),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 60,
|
||||
onPressed: () => audio.skipToPrevious(),
|
||||
),
|
||||
const PlayPauseButton(size: 90),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 60,
|
||||
onPressed: () => audio.skipToNext(),
|
||||
),
|
||||
const ShuffleButton(size: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.queue_music_rounded),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 30,
|
||||
onPressed: () {},
|
||||
),
|
||||
const _MoreButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoreButton extends HookConsumerWidget {
|
||||
const _MoreButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final song = ref.watch(mediaItemSongProvider).valueOrNull;
|
||||
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 30,
|
||||
onPressed: song != null
|
||||
? () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
child: SongContextMenu(song: song),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../models/music.dart';
|
||||
import '../../models/query.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/audio_service.dart';
|
||||
import '../../state/music.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../items.dart';
|
||||
import 'songs_page.dart';
|
||||
|
||||
part 'search_page.g.dart';
|
||||
|
||||
@riverpod
|
||||
class SearchQuery extends _$SearchQuery {
|
||||
@override
|
||||
String? build() {
|
||||
return null;
|
||||
}
|
||||
|
||||
void setQuery(String query) {
|
||||
state = query;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<SearchResults> searchResult(SearchResultRef ref) async {
|
||||
final query = ref.watch(searchQueryProvider);
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
final ftsQuery = '(source_id : $sourceId) AND (- source_id : "$query"*)';
|
||||
|
||||
final songRowIds = await db.searchSongs(ftsQuery, 5, 0).get();
|
||||
final songs = await db.songsInRowIds(songRowIds).get();
|
||||
final albumRowIds = await db.searchAlbums(ftsQuery, 5, 0).get();
|
||||
final albums = await db.albumsInRowIds(albumRowIds).get();
|
||||
final artistRowIds = await db.searchArtists(ftsQuery, 5, 0).get();
|
||||
final artists = await db.artistsInRowIds(artistRowIds).get();
|
||||
|
||||
return SearchResults(
|
||||
songs: songs.lock,
|
||||
albums: albums.lock,
|
||||
artists: artists.lock,
|
||||
);
|
||||
}
|
||||
|
||||
class SearchPage extends HookConsumerWidget {
|
||||
const SearchPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final results = ref.watch(searchResultProvider).valueOrNull;
|
||||
|
||||
return KeyboardDismissOnTap(
|
||||
dismissOnCapturedTaps: true,
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: CustomScrollView(
|
||||
reverse: true,
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(child: _SearchBar()),
|
||||
if (results != null && results.songs.isNotEmpty)
|
||||
_SongsSection(songs: results.songs),
|
||||
if (results != null && results.albums.isNotEmpty)
|
||||
_AlbumsSection(albums: results.albums),
|
||||
if (results != null && results.artists.isNotEmpty)
|
||||
_ArtistsSection(artists: results.artists),
|
||||
if (results != null)
|
||||
const SliverPadding(padding: EdgeInsets.only(top: 96))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchBar extends HookConsumerWidget {
|
||||
const _SearchBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = useTextEditingController(text: '');
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return Container(
|
||||
color: ElevationOverlay.applySurfaceTint(
|
||||
theme.colorScheme.surface,
|
||||
theme.colorScheme.surfaceTint,
|
||||
1,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 24,
|
||||
left: 24,
|
||||
bottom: 24,
|
||||
top: 8,
|
||||
),
|
||||
child: IgnoreKeyboardDismiss(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: l.searchInputPlaceholder,
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(searchQueryProvider.notifier).setQuery(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends HookConsumerWidget {
|
||||
final String title;
|
||||
|
||||
const _SectionHeader({required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context).textTheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(title, style: theme.headlineMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Section extends HookConsumerWidget {
|
||||
final String title;
|
||||
final Iterable<Widget> children;
|
||||
|
||||
const _Section({
|
||||
required this.title,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
const SizedBox(height: 16),
|
||||
...children.toList().reversed,
|
||||
_SectionHeader(title: title),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SongsSection extends HookConsumerWidget {
|
||||
final IList<Song>? songs;
|
||||
|
||||
const _SongsSection({required this.songs});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _Section(
|
||||
title: l.resourcesSongName(100),
|
||||
children: (songs ?? <Song>[]).map(
|
||||
(song) => QueueContext(
|
||||
type: QueueContextType.album,
|
||||
id: song.albumId!,
|
||||
child: SongListTile(
|
||||
song: song,
|
||||
image: true,
|
||||
onTap: () async {
|
||||
const query = ListQuery(
|
||||
sort: SortBy(column: 'disc, track'),
|
||||
);
|
||||
final albumSongs = await ref.read(
|
||||
albumSongsListProvider(song.albumId!, query).future,
|
||||
);
|
||||
|
||||
ref.read(audioControlProvider).playSongs(
|
||||
context: QueueContextType.album,
|
||||
contextId: song.albumId!,
|
||||
shuffle: true,
|
||||
startIndex: albumSongs.indexOf(song),
|
||||
query: query,
|
||||
getSongs: (query) => ref.read(
|
||||
albumSongsListProvider(song.albumId!, query).future),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumsSection extends HookConsumerWidget {
|
||||
final IList<Album>? albums;
|
||||
|
||||
const _AlbumsSection({required this.albums});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _Section(
|
||||
title: l.resourcesAlbumName(100),
|
||||
children: (albums ?? <Album>[]).map(
|
||||
(album) => AlbumListTile(
|
||||
album: album,
|
||||
onTap: () => context.navigateTo(AlbumSongsRoute(id: album.id)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArtistsSection extends HookConsumerWidget {
|
||||
final IList<Artist>? artists;
|
||||
|
||||
const _ArtistsSection({required this.artists});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _Section(
|
||||
title: l.resourcesArtistName(100),
|
||||
children: (artists ?? <Artist>[]).map(
|
||||
(artist) => ArtistListTile(
|
||||
artist: artist,
|
||||
onTap: () => context.navigateTo(ArtistRoute(id: artist.id)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'search_page.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$searchResultHash() => r'e5240c0c51937e1e946138d27aeaea93dc0231c3';
|
||||
|
||||
/// See also [searchResult].
|
||||
@ProviderFor(searchResult)
|
||||
final searchResultProvider = AutoDisposeFutureProvider<SearchResults>.internal(
|
||||
searchResult,
|
||||
name: r'searchResultProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$searchResultHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef SearchResultRef = AutoDisposeFutureProviderRef<SearchResults>;
|
||||
String _$searchQueryHash() => r'f7624215b3d5a8b917cb0af239666a19a18d91d5';
|
||||
|
||||
/// See also [SearchQuery].
|
||||
@ProviderFor(SearchQuery)
|
||||
final searchQueryProvider =
|
||||
AutoDisposeNotifierProvider<SearchQuery, String?>.internal(
|
||||
SearchQuery.new,
|
||||
name: r'searchQueryProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$searchQueryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SearchQuery = AutoDisposeNotifier<String?>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,448 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../log.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../state/init.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../app_router.dart';
|
||||
import '../dialogs.dart';
|
||||
|
||||
const kHorizontalPadding = 16.0;
|
||||
|
||||
class SettingsPage extends HookConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
// final downloads = ref.watch(downloadServiceProvider.select(
|
||||
// (value) => value.downloads,
|
||||
// ));
|
||||
|
||||
return Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 96),
|
||||
_SectionHeader(l.settingsServersName),
|
||||
const _Sources(),
|
||||
_SectionHeader(l.settingsNetworkName),
|
||||
const _Network(),
|
||||
_SectionHeader(l.settingsAboutName),
|
||||
_About(),
|
||||
// const _SectionHeader('Downloads'),
|
||||
// _Section(
|
||||
// children: downloads
|
||||
// .map(
|
||||
// (e) => ListTile(
|
||||
// isThreeLine: true,
|
||||
// title: Text(e.filename ?? e.url),
|
||||
// subtitle: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.start,
|
||||
// children: [
|
||||
// Row(children: [Text('Progress: ${e.progress}%')]),
|
||||
// Row(children: [Text('Status: ${e.status})')]),
|
||||
// Text('Status: ${e.savedDir}'),
|
||||
// ],
|
||||
// ),
|
||||
// trailing:
|
||||
// CircularProgressIndicator(value: e.progress / 100),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Section extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const _Section({required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...children,
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _SectionHeader(this.title);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding),
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.displaySmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Network extends StatelessWidget {
|
||||
const _Network();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const _Section(
|
||||
children: [
|
||||
_OfflineMode(),
|
||||
_MaxBitrateWifi(),
|
||||
_MaxBitrateMobile(),
|
||||
_StreamFormat(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _About extends HookConsumerWidget {
|
||||
_About();
|
||||
|
||||
final _homepage = Uri.parse('https://github.com/austinried/subtracks');
|
||||
final _donate = Uri.parse('https://ko-fi.com/austinried');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
final pkg = ref.watch(packageInfoProvider).requireValue;
|
||||
|
||||
return _Section(
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('subtracks'),
|
||||
subtitle: Text(l.settingsAboutVersion(pkg.version)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(l.settingsAboutActionsLicenses),
|
||||
// trailing: const Icon(Icons.open_in_new_rounded),
|
||||
onTap: () {},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(l.settingsAboutActionsProjectHomepage),
|
||||
subtitle: Text(_homepage.toString()),
|
||||
trailing: const Icon(Icons.open_in_new_rounded),
|
||||
onTap: () => launchUrl(
|
||||
_homepage,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(l.settingsAboutActionsSupport),
|
||||
subtitle: Text(_donate.toString()),
|
||||
trailing: const Icon(Icons.open_in_new_rounded),
|
||||
onTap: () => launchUrl(
|
||||
_donate,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _ShareLogsButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareLogsButton extends StatelessWidget {
|
||||
const _ShareLogsButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.share),
|
||||
label: Text(l.settingsAboutShareLogs),
|
||||
onPressed: () async {
|
||||
final files = await logFiles();
|
||||
if (files.isEmpty) return;
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
final value = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => MultipleChoiceDialog<String>(
|
||||
title: l.settingsAboutChooseLog,
|
||||
current: files.first.path,
|
||||
options: files
|
||||
.map((e) => MultiChoiceOption.string(
|
||||
title: p.basename(e.path),
|
||||
option: e.path,
|
||||
))
|
||||
.toIList(),
|
||||
),
|
||||
);
|
||||
|
||||
if (value == null) return;
|
||||
Share.shareXFiles(
|
||||
[XFile(value, mimeType: 'text/plain')],
|
||||
subject: 'Logs from subtracks: ${String.fromCharCodes(
|
||||
List.generate(8, (_) => Random().nextInt(26) + 65),
|
||||
)}',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MaxBitrateWifi extends HookConsumerWidget {
|
||||
const _MaxBitrateWifi();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bitrate = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.app.maxBitrateWifi,
|
||||
));
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _MaxBitrateOption(
|
||||
title: l.settingsNetworkOptionsMaxBitrateWifiTitle,
|
||||
bitrate: bitrate,
|
||||
onChange: (value) {
|
||||
ref.read(settingsServiceProvider.notifier).setMaxBitrateWifi(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MaxBitrateMobile extends HookConsumerWidget {
|
||||
const _MaxBitrateMobile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bitrate = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.app.maxBitrateMobile,
|
||||
));
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _MaxBitrateOption(
|
||||
title: l.settingsNetworkOptionsMaxBitrateMobileTitle,
|
||||
bitrate: bitrate,
|
||||
onChange: (value) {
|
||||
ref.read(settingsServiceProvider.notifier).setMaxBitrateMobile(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MaxBitrateOption extends HookConsumerWidget {
|
||||
final String title;
|
||||
final int bitrate;
|
||||
final void Function(int value) onChange;
|
||||
|
||||
const _MaxBitrateOption({
|
||||
required this.title,
|
||||
required this.bitrate,
|
||||
required this.onChange,
|
||||
});
|
||||
|
||||
static const options = [0, 24, 32, 64, 96, 128, 192, 256, 320];
|
||||
|
||||
String _bitrateText(AppLocalizations l, int bitrate) {
|
||||
return bitrate == 0
|
||||
? l.settingsNetworkValuesUnlimitedKbps
|
||||
: l.settingsNetworkValuesKbps(bitrate.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return ListTile(
|
||||
title: Text(title),
|
||||
subtitle: Text(_bitrateText(l, bitrate)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => MultipleChoiceDialog<int>(
|
||||
title: title,
|
||||
current: bitrate,
|
||||
options: options
|
||||
.map((opt) => MultiChoiceOption.int(
|
||||
title: _bitrateText(l, opt),
|
||||
option: opt,
|
||||
))
|
||||
.toIList(),
|
||||
),
|
||||
);
|
||||
|
||||
if (value != null) {
|
||||
onChange(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StreamFormat extends HookConsumerWidget {
|
||||
const _StreamFormat();
|
||||
|
||||
static const options = ['', 'mp3', 'opus', 'ogg'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final streamFormat = ref.watch(
|
||||
settingsServiceProvider.select((value) => value.app.streamFormat),
|
||||
);
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return ListTile(
|
||||
title: Text(l.settingsNetworkOptionsStreamFormat),
|
||||
subtitle: Text(
|
||||
streamFormat ?? l.settingsNetworkOptionsStreamFormatServerDefault,
|
||||
),
|
||||
onTap: () async {
|
||||
final value = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => MultipleChoiceDialog<String>(
|
||||
title: l.settingsNetworkOptionsStreamFormat,
|
||||
current: streamFormat ?? '',
|
||||
options: options
|
||||
.map((opt) => MultiChoiceOption.string(
|
||||
title: opt == ''
|
||||
? l.settingsNetworkOptionsStreamFormatServerDefault
|
||||
: opt,
|
||||
option: opt,
|
||||
))
|
||||
.toIList(),
|
||||
),
|
||||
);
|
||||
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(settingsServiceProvider.notifier)
|
||||
.setStreamFormat(value == '' ? null : value);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OfflineMode extends HookConsumerWidget {
|
||||
const _OfflineMode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final offline = ref.watch(offlineModeProvider);
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return SwitchListTile(
|
||||
value: offline,
|
||||
title: Text(l.settingsNetworkOptionsOfflineMode),
|
||||
subtitle: offline
|
||||
? Text(l.settingsNetworkOptionsOfflineModeOn)
|
||||
: Text(l.settingsNetworkOptionsOfflineModeOff),
|
||||
onChanged: (value) {
|
||||
ref.read(offlineModeProvider.notifier).setMode(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Sources extends HookConsumerWidget {
|
||||
const _Sources();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final sources = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.sources,
|
||||
));
|
||||
final activeSource = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.activeSource,
|
||||
));
|
||||
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _Section(
|
||||
children: [
|
||||
for (var source in sources)
|
||||
RadioListTile<int>(
|
||||
value: source.id,
|
||||
groupValue: activeSource?.id,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(settingsServiceProvider.notifier)
|
||||
.setActiveSource(source.id);
|
||||
},
|
||||
title: Text(source.name),
|
||||
subtitle: Text(
|
||||
source.address.toString(),
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
secondary: IconButton(
|
||||
icon: const Icon(Icons.edit_rounded),
|
||||
onPressed: () {
|
||||
context.pushRoute(SourceRoute(id: source.id));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(l.settingsServersActionsAdd),
|
||||
onPressed: () {
|
||||
context.pushRoute(SourceRoute());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// TODO: remove
|
||||
if (kDebugMode)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('Add TEST'),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(settingsServiceProvider.notifier)
|
||||
.addTestSource('TEST');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
import '../../models/music.dart';
|
||||
import '../../models/query.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/audio_service.dart';
|
||||
import '../../services/cache_service.dart';
|
||||
import '../../state/music.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../../state/theme.dart';
|
||||
import '../buttons.dart';
|
||||
import '../context_menus.dart';
|
||||
import '../gradient.dart';
|
||||
import '../hooks/use_download_actions.dart';
|
||||
import '../hooks/use_list_query_paging_controller.dart';
|
||||
import '../images.dart';
|
||||
import '../items.dart';
|
||||
import '../lists.dart';
|
||||
|
||||
class AlbumSongsPage extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const AlbumSongsPage({
|
||||
super.key,
|
||||
@pathParam required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.watch(albumProvider(id)).valueOrNull;
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
final colors = ref.watch(albumArtThemeProvider(id)).valueOrNull;
|
||||
final key = useState(GlobalKey());
|
||||
|
||||
if (album == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final query = useMemoized(() => const ListQuery(
|
||||
page: Pagination(limit: 30),
|
||||
sort: SortBy(column: 'disc, track'),
|
||||
));
|
||||
|
||||
final getSongs = useCallback(
|
||||
(ListQuery query) => ref.read(albumSongsListProvider(id, query).future),
|
||||
[id],
|
||||
);
|
||||
|
||||
final play = useCallback(
|
||||
({int? index, bool? shuffle}) => audio.playSongs(
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
startIndex: index,
|
||||
context: QueueContextType.album,
|
||||
contextId: id,
|
||||
shuffle: shuffle,
|
||||
),
|
||||
[id, query, getSongs],
|
||||
);
|
||||
|
||||
return QueueContext(
|
||||
id: id,
|
||||
type: QueueContextType.album,
|
||||
child: _SongsPage(
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
fab: ShuffleFab(onPressed: () => play(shuffle: true)),
|
||||
onSongTap: (song, index) => play(index: index),
|
||||
background: AlbumArtGradient(key: key.value, id: id),
|
||||
colors: colors,
|
||||
header: _AlbumHeader(
|
||||
album: album,
|
||||
play: () => play(shuffle: false),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumHeader extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final void Function() play;
|
||||
|
||||
const _AlbumHeader({
|
||||
required this.album,
|
||||
required this.play,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
|
||||
final downloadActions = useAlbumDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
album: album,
|
||||
);
|
||||
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _Header(
|
||||
title: album.name,
|
||||
subtitle: album.albumArtist,
|
||||
imageCache: cache.albumArt(album, thumbnail: false),
|
||||
playText: l.resourcesAlbumActionsPlay,
|
||||
onPlay: play,
|
||||
onMore: () => showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
child: AlbumContextMenu(album: album),
|
||||
),
|
||||
),
|
||||
downloadActions: downloadActions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistSongsPage extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const PlaylistSongsPage({
|
||||
super.key,
|
||||
@pathParam required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final playlist = ref.watch(playlistProvider(id)).valueOrNull;
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
final colors = ref.watch(playlistArtThemeProvider(id)).valueOrNull;
|
||||
|
||||
if (playlist == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final query = useMemoized(() => const ListQuery(
|
||||
page: Pagination(limit: 30),
|
||||
sort: SortBy(column: 'playlist_songs.position'),
|
||||
));
|
||||
|
||||
final getSongs = useCallback(
|
||||
(ListQuery query) =>
|
||||
ref.read(playlistSongsListProvider(id, query).future),
|
||||
[id],
|
||||
);
|
||||
|
||||
final play = useCallback(
|
||||
({int? index, bool? shuffle}) => audio.playSongs(
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
startIndex: index,
|
||||
context: QueueContextType.playlist,
|
||||
contextId: id,
|
||||
shuffle: shuffle,
|
||||
),
|
||||
[id, query, getSongs],
|
||||
);
|
||||
|
||||
return QueueContext(
|
||||
id: id,
|
||||
type: QueueContextType.playlist,
|
||||
child: _SongsPage(
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
fab: ShuffleFab(onPressed: () => play(shuffle: true)),
|
||||
onSongTap: (song, index) => play(index: index),
|
||||
songImage: true,
|
||||
background: PlaylistArtGradient(id: id),
|
||||
colors: colors,
|
||||
header: _PlaylistHeader(
|
||||
playlist: playlist,
|
||||
play: () => play(shuffle: false),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaylistHeader extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
final void Function() play;
|
||||
|
||||
const _PlaylistHeader({
|
||||
required this.playlist,
|
||||
required this.play,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
|
||||
final downloadActions = usePlaylistDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
playlist: playlist,
|
||||
);
|
||||
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _Header(
|
||||
title: playlist.name,
|
||||
subtitle: playlist.comment,
|
||||
imageCache: cache.playlistArt(playlist, thumbnail: false),
|
||||
playText: l.resourcesPlaylistActionsPlay,
|
||||
onPlay: play,
|
||||
onMore: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
size: MenuSize.small,
|
||||
child: PlaylistContextMenu(playlist: playlist),
|
||||
),
|
||||
);
|
||||
},
|
||||
downloadActions: downloadActions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GenreSongsPage extends HookConsumerWidget {
|
||||
final String genre;
|
||||
|
||||
const GenreSongsPage({
|
||||
super.key,
|
||||
@pathParam required this.genre,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final query = useMemoized(
|
||||
() => ListQuery(
|
||||
page: const Pagination(limit: 30),
|
||||
sort: const SortBy(
|
||||
column: 'albums.created DESC, albums.name, songs.disc, songs.track',
|
||||
),
|
||||
filters: IList(
|
||||
[FilterWith.equals(column: 'songs.genre', value: genre)],
|
||||
),
|
||||
),
|
||||
[genre],
|
||||
);
|
||||
|
||||
final getSongs = useCallback(
|
||||
(ListQuery query) => ref.read(songsByAlbumListProvider(query).future),
|
||||
[],
|
||||
);
|
||||
|
||||
final play = useCallback(
|
||||
({int? index, bool? shuffle}) => ref.read(audioControlProvider).playRadio(
|
||||
context: QueueContextType.genre,
|
||||
contextId: genre,
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
),
|
||||
[query, getSongs],
|
||||
);
|
||||
|
||||
return QueueContext(
|
||||
id: genre,
|
||||
type: QueueContextType.album,
|
||||
child: _SongsPage(
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
// onSongTap: (song, index) => play(index: index),
|
||||
songImage: true,
|
||||
background: const BackgroundGradient(),
|
||||
fab: RadioPlayFab(
|
||||
onPressed: () => play(),
|
||||
),
|
||||
header: _GenreHeader(genre: genre),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GenreHeader extends HookConsumerWidget {
|
||||
final String genre;
|
||||
|
||||
const _GenreHeader({
|
||||
required this.genre,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final count = ref.watch(songsByGenreCountProvider(genre)).valueOrNull ?? 0;
|
||||
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return _Header(
|
||||
title: genre,
|
||||
subtitle: l.resourcesSongCount(count),
|
||||
downloadActions: const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QueueContext extends InheritedWidget {
|
||||
final QueueContextType type;
|
||||
final String? id;
|
||||
|
||||
const QueueContext({
|
||||
super.key,
|
||||
required this.type,
|
||||
this.id,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static QueueContext? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<QueueContext>();
|
||||
}
|
||||
|
||||
static QueueContext of(BuildContext context) {
|
||||
final QueueContext? result = maybeOf(context);
|
||||
assert(result != null, 'No QueueContext found in context');
|
||||
return result!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant QueueContext oldWidget) =>
|
||||
oldWidget.id != id || oldWidget.type != type;
|
||||
}
|
||||
|
||||
class _SongsPage extends HookConsumerWidget {
|
||||
final ListQuery query;
|
||||
final FutureOr<List<Song>> Function(ListQuery query) getSongs;
|
||||
final void Function(Song song, int index)? onSongTap;
|
||||
final bool songImage;
|
||||
final Widget background;
|
||||
final Widget fab;
|
||||
final ColorTheme? colors;
|
||||
final Widget header;
|
||||
|
||||
const _SongsPage({
|
||||
required this.query,
|
||||
required this.getSongs,
|
||||
this.onSongTap,
|
||||
this.songImage = false,
|
||||
required this.background,
|
||||
required this.fab,
|
||||
this.colors,
|
||||
required this.header,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final base = ref.watch(baseThemeProvider);
|
||||
ref.listen(musicSourceProvider, (previous, next) {
|
||||
if (next.id != previous?.id) {
|
||||
context.router.popUntilRoot();
|
||||
}
|
||||
});
|
||||
|
||||
final pagingController = useListQueryPagingController(
|
||||
ref,
|
||||
query: query,
|
||||
getItems: getSongs,
|
||||
);
|
||||
|
||||
final widget = Scaffold(
|
||||
floatingActionButton: fab,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverStack(
|
||||
children: [
|
||||
SliverPositioned.fill(
|
||||
child: Container(
|
||||
color: base.gradientLow,
|
||||
),
|
||||
),
|
||||
SliverPositioned.directional(
|
||||
textDirection: TextDirection.ltr,
|
||||
start: 0,
|
||||
end: 0,
|
||||
top: 0,
|
||||
child: background,
|
||||
),
|
||||
MultiSliver(
|
||||
children: [
|
||||
SliverSafeArea(
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: header,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PagedListQueryView(
|
||||
pagingController: pagingController,
|
||||
useSliver: true,
|
||||
itemBuilder: (context, item, index) => SongListTile(
|
||||
song: item,
|
||||
image: songImage,
|
||||
onTap: () =>
|
||||
onSongTap != null ? onSongTap!(item, index) : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (colors != null) {
|
||||
return Theme(data: colors!.theme, child: widget);
|
||||
} else {
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends HookConsumerWidget {
|
||||
final UriCacheInfo? imageCache;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? playText;
|
||||
final void Function()? onPlay;
|
||||
final FutureOr<void> Function()? onMore;
|
||||
final List<DownloadAction> downloadActions;
|
||||
|
||||
const _Header({
|
||||
this.imageCache,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.playText,
|
||||
this.onPlay,
|
||||
this.onMore,
|
||||
required this.downloadActions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final inheritedStyle = DefaultTextStyle.of(context).style;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
if (imageCache != null)
|
||||
CardClip(
|
||||
square: false,
|
||||
child: UriCacheInfoImage(
|
||||
height: 300,
|
||||
fit: BoxFit.contain,
|
||||
placeholderStyle: PlaceholderStyle.spinner,
|
||||
cache: imageCache!,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge!.copyWith(
|
||||
color: inheritedStyle.color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
subtitle ?? '',
|
||||
style: theme.textTheme.titleMedium!.copyWith(
|
||||
color: inheritedStyle.color,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
if (downloadActions.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: downloadActions.first.action,
|
||||
icon: downloadActions.first.type == DownloadActionType.delete
|
||||
? const Icon(Icons.download_done_rounded)
|
||||
: downloadActions.first.iconBuilder(context),
|
||||
),
|
||||
if (onPlay != null)
|
||||
FilledButton.icon(
|
||||
onPressed: onPlay,
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: Text(playText ?? ''),
|
||||
),
|
||||
if (onMore != null)
|
||||
IconButton(
|
||||
onPressed: onMore,
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../log.dart';
|
||||
import '../../models/settings.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../items.dart';
|
||||
import '../snackbars.dart';
|
||||
|
||||
class SourcePage extends HookConsumerWidget {
|
||||
final int? id;
|
||||
|
||||
const SourcePage({
|
||||
super.key,
|
||||
@pathParam this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final source = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.sources.singleWhereOrNull((e) => e.id == id)
|
||||
as SubsonicSettings?,
|
||||
));
|
||||
final form = useState(GlobalKey<FormState>()).value;
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context);
|
||||
final isSaving = useState(false);
|
||||
final isDeleting = useState(false);
|
||||
|
||||
final name = LabeledTextField(
|
||||
label: l.settingsServersFieldsName,
|
||||
initialValue: source?.name,
|
||||
required: true,
|
||||
);
|
||||
final address = LabeledTextField(
|
||||
label: l.settingsServersFieldsAddress,
|
||||
initialValue: source?.address.toString(),
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
required: true,
|
||||
validator: (value, label) {
|
||||
if (!value!.contains(RegExp(r'https?:\/\/'))) {
|
||||
return '$label must be a valid URL';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
final username = LabeledTextField(
|
||||
label: l.settingsServersFieldsUsername,
|
||||
initialValue: source?.username,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
required: true,
|
||||
);
|
||||
final password = LabeledTextField(
|
||||
label: l.settingsServersFieldsPassword,
|
||||
initialValue: source?.password,
|
||||
obscureText: true,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
required: true,
|
||||
);
|
||||
|
||||
final forcePlaintextPassword = useState(!(source?.useTokenAuth ?? true));
|
||||
final forcePlaintextSwitch = SwitchListTile(
|
||||
value: forcePlaintextPassword.value,
|
||||
title: Text(l.settingsServersOptionsForcePlaintextPasswordTitle),
|
||||
subtitle: forcePlaintextPassword.value
|
||||
? Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOn)
|
||||
: Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOff),
|
||||
onChanged: (value) => forcePlaintextPassword.value = value,
|
||||
);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async => !isSaving.value && !isDeleting.value,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(),
|
||||
floatingActionButton: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (source != null && source.isActive != true)
|
||||
FloatingActionButton(
|
||||
backgroundColor: theme.colorScheme.tertiaryContainer,
|
||||
foregroundColor: theme.colorScheme.onTertiaryContainer,
|
||||
onPressed: !isSaving.value && !isDeleting.value
|
||||
? () async {
|
||||
final router = context.router;
|
||||
|
||||
try {
|
||||
isDeleting.value = true;
|
||||
await ref
|
||||
.read(settingsServiceProvider.notifier)
|
||||
.deleteSource(source.id);
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
|
||||
router.pop();
|
||||
}
|
||||
: null,
|
||||
child: isDeleting.value
|
||||
? SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: theme.colorScheme.onTertiaryContainer,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.delete_forever_rounded),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
icon: isSaving.value
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const Icon(Icons.save_rounded),
|
||||
label: Text(l.settingsServersActionsSave),
|
||||
onPressed: !isSaving.value && !isDeleting.value
|
||||
? () async {
|
||||
final router = context.router;
|
||||
if (!form.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var error = false;
|
||||
try {
|
||||
isSaving.value = true;
|
||||
if (source != null) {
|
||||
await ref
|
||||
.read(settingsServiceProvider.notifier)
|
||||
.updateSource(
|
||||
source.copyWith(
|
||||
name: name.value,
|
||||
address: Uri.parse(address.value),
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
useTokenAuth: !forcePlaintextPassword.value,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await ref
|
||||
.read(settingsServiceProvider.notifier)
|
||||
.createSource(
|
||||
SourcesCompanion.insert(
|
||||
name: name.value,
|
||||
address: Uri.parse(address.value),
|
||||
),
|
||||
SubsonicSourcesCompanion.insert(
|
||||
features: IList(),
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
useTokenAuth:
|
||||
Value(!forcePlaintextPassword.value),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e, st) {
|
||||
showErrorSnackbar(context, e.toString());
|
||||
log.severe('Saving source', e, st);
|
||||
error = true;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
router.pop();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: form,
|
||||
child: AutofillGroup(
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 96 - kToolbarHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
source == null
|
||||
? l.settingsServersActionsAdd
|
||||
: l.settingsServersActionsEdit,
|
||||
style: theme.textTheme.displaySmall,
|
||||
),
|
||||
),
|
||||
name,
|
||||
address,
|
||||
username,
|
||||
password,
|
||||
const SizedBox(height: 24),
|
||||
forcePlaintextSwitch,
|
||||
const FabPadding(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LabeledTextField extends HookConsumerWidget {
|
||||
final String label;
|
||||
final String? initialValue;
|
||||
final bool obscureText;
|
||||
final bool required;
|
||||
final TextInputType? keyboardType;
|
||||
final Iterable<String>? autofillHints;
|
||||
final String? Function(String? value, String label)? validator;
|
||||
|
||||
// ignore: prefer_const_constructors_in_immutables
|
||||
LabeledTextField({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.initialValue,
|
||||
this.obscureText = false,
|
||||
this.keyboardType,
|
||||
this.validator,
|
||||
this.autofillHints,
|
||||
this.required = false,
|
||||
});
|
||||
|
||||
late final TextEditingController _controller;
|
||||
|
||||
String get value => _controller.text;
|
||||
|
||||
String? _requiredValidator(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$label is required';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
_controller = useTextEditingController(text: initialValue);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Text(label, style: theme.textTheme.titleMedium),
|
||||
TextFormField(
|
||||
controller: _controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
autofillHints: autofillHints,
|
||||
validator: (value) {
|
||||
String? error;
|
||||
|
||||
if (required) {
|
||||
error = _requiredValidator(value);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
if (validator != null) {
|
||||
return validator!(value, label);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showErrorSnackbar(BuildContext context, String message) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(message, style: TextStyle(color: colors.onErrorContainer)),
|
||||
backgroundColor: colors.errorContainer,
|
||||
showCloseIcon: true,
|
||||
closeIconColor: colors.onErrorContainer,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 10),
|
||||
));
|
||||
}
|
||||
30
lib/cache/image_cache.dart
vendored
30
lib/cache/image_cache.dart
vendored
@@ -1,30 +0,0 @@
|
||||
// ignore_for_file: implementation_imports
|
||||
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_cache_manager/src/storage/file_system/file_system_io.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../http/client.dart';
|
||||
|
||||
part 'image_cache.g.dart';
|
||||
|
||||
CacheManager _openImageCache(BaseClient httpClient) {
|
||||
const key = 'images';
|
||||
return CacheManager(
|
||||
Config(
|
||||
key,
|
||||
stalePeriod: const Duration(days: 2147483647),
|
||||
maxNrOfCacheObjects: 2147483647,
|
||||
repo: JsonCacheInfoRepository(databaseName: key),
|
||||
fileSystem: IOFileSystem(key),
|
||||
fileService: HttpFileService(httpClient: httpClient),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
CacheManager imageCache(ImageCacheRef ref) {
|
||||
final http = ref.watch(httpClientProvider);
|
||||
return _openImageCache(http);
|
||||
}
|
||||
23
lib/cache/image_cache.g.dart
vendored
23
lib/cache/image_cache.g.dart
vendored
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'image_cache.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$imageCacheHash() => r'aaeb74898734c2776f594e05eb82262af20e079f';
|
||||
|
||||
/// See also [imageCache].
|
||||
@ProviderFor(imageCache)
|
||||
final imageCacheProvider = Provider<CacheManager>.internal(
|
||||
imageCache,
|
||||
name: r'imageCacheProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$imageCacheHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ImageCacheRef = ProviderRef<CacheManager>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,72 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
|
||||
import '../models/query.dart';
|
||||
import '../models/settings.dart';
|
||||
|
||||
class DurationSecondsConverter extends TypeConverter<Duration, int> {
|
||||
const DurationSecondsConverter();
|
||||
|
||||
@override
|
||||
Duration fromSql(int fromDb) => Duration(seconds: fromDb);
|
||||
|
||||
@override
|
||||
int toSql(Duration value) => value.inSeconds;
|
||||
}
|
||||
|
||||
class UriConverter extends TypeConverter<Uri, String> {
|
||||
const UriConverter();
|
||||
|
||||
@override
|
||||
Uri fromSql(String fromDb) => Uri.parse(fromDb);
|
||||
|
||||
@override
|
||||
String toSql(Uri value) => value.toString();
|
||||
}
|
||||
|
||||
class ListQueryConverter extends TypeConverter<ListQuery, String> {
|
||||
const ListQueryConverter();
|
||||
|
||||
@override
|
||||
ListQuery fromSql(String fromDb) => ListQuery.fromJson(jsonDecode(fromDb));
|
||||
|
||||
@override
|
||||
String toSql(ListQuery value) => jsonEncode(value.toJson());
|
||||
}
|
||||
|
||||
class SubsonicFeatureListConverter
|
||||
extends TypeConverter<IList<SubsonicFeature>, String> {
|
||||
const SubsonicFeatureListConverter();
|
||||
|
||||
@override
|
||||
IList<SubsonicFeature> fromSql(String fromDb) {
|
||||
return IList<SubsonicFeature>.fromJson(
|
||||
jsonDecode(fromDb),
|
||||
(item) => SubsonicFeature.values.byName(item as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(IList<SubsonicFeature> value) {
|
||||
return jsonEncode(value.toJson((e) => e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
class IListIntConverter extends TypeConverter<IList<int>, String> {
|
||||
const IListIntConverter();
|
||||
|
||||
@override
|
||||
IList<int> fromSql(String fromDb) {
|
||||
return IList<int>.fromJson(
|
||||
jsonDecode(fromDb),
|
||||
(item) => int.parse(item as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(IList<int> value) {
|
||||
return jsonEncode(value.toJson((e) => jsonEncode(e)));
|
||||
}
|
||||
}
|
||||
@@ -1,681 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/isolate.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../log.dart';
|
||||
import '../models/music.dart';
|
||||
import '../models/query.dart';
|
||||
import '../models/settings.dart';
|
||||
import '../models/support.dart';
|
||||
import 'converters.dart';
|
||||
import 'error_logging_database.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
// don't exceed SQLITE_MAX_VARIABLE_NUMBER (32766 for version >= 3.32.0)
|
||||
// https://www.sqlite.org/limits.html
|
||||
const kSqliteMaxVariableNumber = 32766;
|
||||
|
||||
@DriftDatabase(include: {'tables.drift'})
|
||||
class SubtracksDatabase extends _$SubtracksDatabase {
|
||||
SubtracksDatabase() : super(_openConnection());
|
||||
SubtracksDatabase.connection(QueryExecutor e) : super(e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Runs a database opertion in a background isolate.
|
||||
///
|
||||
/// **Only pass top-level functions to [computation]!**
|
||||
///
|
||||
/// **Do not use non-serializable data inside [computation]!**
|
||||
Future<Ret> background<Ret>(
|
||||
FutureOr<Ret> Function(SubtracksDatabase) computation,
|
||||
) async {
|
||||
return computeWithDatabase(
|
||||
connect: SubtracksDatabase.connection,
|
||||
computation: computation,
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Album> albumsList(int sourceId, ListQuery opt) {
|
||||
return filterAlbums(
|
||||
(_) => _filterPredicate('albums', sourceId, opt),
|
||||
(_) => _filterOrderBy(opt),
|
||||
(_) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Album> albumsListDownloaded(int sourceId, ListQuery opt) {
|
||||
return filterAlbumsDownloaded(
|
||||
(_, __) => _filterPredicate('albums', sourceId, opt),
|
||||
(_, __) => _filterOrderBy(opt),
|
||||
(_, __) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Artist> artistsList(int sourceId, ListQuery opt) {
|
||||
return filterArtists(
|
||||
(_) => _filterPredicate('artists', sourceId, opt),
|
||||
(_) => _filterOrderBy(opt),
|
||||
(_) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Artist> artistsListDownloaded(int sourceId, ListQuery opt) {
|
||||
return filterArtistsDownloaded(
|
||||
(_, __, ___) => _filterPredicate('artists', sourceId, opt),
|
||||
(_, __, ___) => _filterOrderBy(opt),
|
||||
(_, __, ___) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Playlist> playlistsList(int sourceId, ListQuery opt) {
|
||||
return filterPlaylists(
|
||||
(_) => _filterPredicate('playlists', sourceId, opt),
|
||||
(_) => _filterOrderBy(opt),
|
||||
(_) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Playlist> playlistsListDownloaded(
|
||||
int sourceId, ListQuery opt) {
|
||||
return filterPlaylistsDownloaded(
|
||||
(_, __, ___) => _filterPredicate('playlists', sourceId, opt),
|
||||
(_, __, ___) => _filterOrderBy(opt),
|
||||
(_, __, ___) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Song> songsList(int sourceId, ListQuery opt) {
|
||||
return filterSongs(
|
||||
(_) => _filterPredicate('songs', sourceId, opt),
|
||||
(_) => _filterOrderBy(opt),
|
||||
(_) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Song> songsListDownloaded(int sourceId, ListQuery opt) {
|
||||
return filterSongsDownloaded(
|
||||
(_) => _filterPredicate('songs', sourceId, opt),
|
||||
(_) => _filterOrderBy(opt),
|
||||
(_) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
Expression<bool> _filterPredicate(String table, int sourceId, ListQuery opt) {
|
||||
return opt.filters.map((filter) => buildFilter<bool>(filter)).fold(
|
||||
CustomExpression('$table.source_id = $sourceId'),
|
||||
(previousValue, element) => previousValue & element,
|
||||
);
|
||||
}
|
||||
|
||||
OrderBy _filterOrderBy(ListQuery opt) {
|
||||
return opt.sort != null
|
||||
? OrderBy([_buildOrder(opt.sort!)])
|
||||
: const OrderBy.nothing();
|
||||
}
|
||||
|
||||
Limit _filterLimit(ListQuery opt) {
|
||||
return Limit(opt.page.limit, opt.page.offset);
|
||||
}
|
||||
|
||||
MultiSelectable<Song> albumSongsList(SourceId sid, ListQuery opt) {
|
||||
return listQuery(
|
||||
select(songs)
|
||||
..where((tbl) =>
|
||||
tbl.sourceId.equals(sid.sourceId) & tbl.albumId.equals(sid.id)),
|
||||
opt,
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Song> songsByAlbumList(int sourceId, ListQuery opt) {
|
||||
return filterSongsByGenre(
|
||||
(_, __) => _filterPredicate('songs', sourceId, opt),
|
||||
(_, __) => _filterOrderBy(opt),
|
||||
(_, __) => _filterLimit(opt),
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelectable<Song> playlistSongsList(SourceId sid, ListQuery opt) {
|
||||
return listQueryJoined(
|
||||
select(songs).join([
|
||||
innerJoin(
|
||||
playlistSongs,
|
||||
playlistSongs.sourceId.equalsExp(songs.sourceId) &
|
||||
playlistSongs.songId.equalsExp(songs.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(playlistSongs.sourceId.equals(sid.sourceId) &
|
||||
playlistSongs.playlistId.equals(sid.id)),
|
||||
opt,
|
||||
).map((row) => row.readTable(songs));
|
||||
}
|
||||
|
||||
Future<void> saveArtists(Iterable<ArtistsCompanion> artists) async {
|
||||
await batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(this.artists, artists);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteArtistsNotIn(int sourceId, Set<String> ids) {
|
||||
return transaction(() async {
|
||||
final allIds = (await (selectOnly(artists)
|
||||
..addColumns([artists.id])
|
||||
..where(artists.sourceId.equals(sourceId)))
|
||||
.map((row) => row.read(artists.id))
|
||||
.get())
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
final downloadIds = (await artistIdsWithDownloadStatus(sourceId).get())
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
|
||||
final diff = allIds.difference(downloadIds).difference(ids);
|
||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
||||
await (delete(artists)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
||||
.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> saveAlbums(Iterable<AlbumsCompanion> albums) async {
|
||||
await batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(this.albums, albums);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteAlbumsNotIn(int sourceId, Set<String> ids) {
|
||||
return transaction(() async {
|
||||
final allIds = (await (selectOnly(albums)
|
||||
..addColumns([albums.id])
|
||||
..where(albums.sourceId.equals(sourceId)))
|
||||
.map((row) => row.read(albums.id))
|
||||
.get())
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
final downloadIds = (await albumIdsWithDownloadStatus(sourceId).get())
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
|
||||
final diff = allIds.difference(downloadIds).difference(ids);
|
||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
||||
await (delete(albums)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
||||
.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> savePlaylists(
|
||||
Iterable<PlaylistWithSongsCompanion> playlistsWithSongs,
|
||||
) async {
|
||||
final playlists = playlistsWithSongs.map((e) => e.playist);
|
||||
final playlistSongs = playlistsWithSongs.expand((e) => e.songs);
|
||||
final sourceId = playlists.first.sourceId.value;
|
||||
|
||||
await (delete(this.playlistSongs)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.sourceId.equals(sourceId) &
|
||||
tbl.playlistId.isIn(playlists.map((e) => e.id.value)),
|
||||
))
|
||||
.go();
|
||||
|
||||
await batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(this.playlists, playlists);
|
||||
batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deletePlaylistsNotIn(int sourceId, Set<String> ids) {
|
||||
return transaction(() async {
|
||||
final allIds = (await (selectOnly(playlists)
|
||||
..addColumns([playlists.id])
|
||||
..where(playlists.sourceId.equals(sourceId)))
|
||||
.map((row) => row.read(playlists.id))
|
||||
.get())
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
final downloadIds = (await playlistIdsWithDownloadStatus(sourceId).get())
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
|
||||
final diff = allIds.difference(downloadIds).difference(ids);
|
||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
||||
await (delete(playlists)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
||||
.go();
|
||||
await (delete(playlistSongs)
|
||||
..where((tbl) =>
|
||||
tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(slice)))
|
||||
.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> savePlaylistSongs(
|
||||
int sourceId,
|
||||
List<String> ids,
|
||||
Iterable<PlaylistSongsCompanion> playlistSongs,
|
||||
) async {
|
||||
await (delete(this.playlistSongs)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(ids),
|
||||
))
|
||||
.go();
|
||||
await batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> saveSongs(Iterable<SongsCompanion> songs) async {
|
||||
await batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(this.songs, songs);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteSongsNotIn(int sourceId, Set<String> ids) {
|
||||
return transaction(() async {
|
||||
final allIds = (await (selectOnly(songs)
|
||||
..addColumns([songs.id])
|
||||
..where(
|
||||
songs.sourceId.equals(sourceId) &
|
||||
songs.downloadFilePath.isNull() &
|
||||
songs.downloadTaskId.isNull(),
|
||||
))
|
||||
.map((row) => row.read(songs.id))
|
||||
.get())
|
||||
.whereNotNull()
|
||||
.toSet();
|
||||
|
||||
final diff = allIds.difference(ids);
|
||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
||||
await (delete(songs)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
||||
.go();
|
||||
await (delete(playlistSongs)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.songId.isIn(slice),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Selectable<LastBottomNavStateData> getLastBottomNavState() {
|
||||
return select(lastBottomNavState)..where((tbl) => tbl.id.equals(1));
|
||||
}
|
||||
|
||||
Future<void> saveLastBottomNavState(LastBottomNavStateData update) {
|
||||
return into(lastBottomNavState).insertOnConflictUpdate(update);
|
||||
}
|
||||
|
||||
Selectable<LastLibraryStateData> getLastLibraryState() {
|
||||
return select(lastLibraryState)..where((tbl) => tbl.id.equals(1));
|
||||
}
|
||||
|
||||
Future<void> saveLastLibraryState(LastLibraryStateData update) {
|
||||
return into(lastLibraryState).insertOnConflictUpdate(update);
|
||||
}
|
||||
|
||||
Selectable<LastAudioStateData> getLastAudioState() {
|
||||
return select(lastAudioState)..where((tbl) => tbl.id.equals(1));
|
||||
}
|
||||
|
||||
Future<void> saveLastAudioState(LastAudioStateCompanion update) {
|
||||
return into(lastAudioState).insertOnConflictUpdate(update);
|
||||
}
|
||||
|
||||
Future<void> insertQueue(Iterable<QueueCompanion> songs) async {
|
||||
await batch((batch) {
|
||||
batch.insertAll(queue, songs);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> clearQueue() async {
|
||||
await delete(queue).go();
|
||||
}
|
||||
|
||||
Future<void> setCurrentTrack(int index) async {
|
||||
await transaction(() async {
|
||||
await (update(queue)..where((tbl) => tbl.index.equals(index).not()))
|
||||
.write(const QueueCompanion(currentTrack: Value(null)));
|
||||
await (update(queue)..where((tbl) => tbl.index.equals(index)))
|
||||
.write(const QueueCompanion(currentTrack: Value(true)));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> createSource(
|
||||
SourcesCompanion source,
|
||||
SubsonicSourcesCompanion subsonic,
|
||||
) async {
|
||||
await transaction(() async {
|
||||
final count = await sourcesCount().getSingle();
|
||||
if (count == 0) {
|
||||
source = source.copyWith(isActive: const Value(true));
|
||||
}
|
||||
|
||||
final id = await into(sources).insert(source);
|
||||
subsonic = subsonic.copyWith(sourceId: Value(id));
|
||||
await into(subsonicSources).insert(subsonic);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateSource(SubsonicSettings source) async {
|
||||
await transaction(() async {
|
||||
await into(sources).insertOnConflictUpdate(source.toSourceInsertable());
|
||||
await into(subsonicSources)
|
||||
.insertOnConflictUpdate(source.toSubsonicInsertable());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteSource(int sourceId) async {
|
||||
await transaction(() async {
|
||||
await (delete(subsonicSources)
|
||||
..where((tbl) => tbl.sourceId.equals(sourceId)))
|
||||
.go();
|
||||
await (delete(sources)..where((tbl) => tbl.id.equals(sourceId))).go();
|
||||
|
||||
await (delete(songs)..where((tbl) => tbl.sourceId.equals(sourceId))).go();
|
||||
await (delete(albums)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
||||
.go();
|
||||
await (delete(artists)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
||||
.go();
|
||||
await (delete(playlistSongs)
|
||||
..where((tbl) => tbl.sourceId.equals(sourceId)))
|
||||
.go();
|
||||
await (delete(playlists)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
||||
.go();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setActiveSource(int id) async {
|
||||
await batch((batch) {
|
||||
batch.update(
|
||||
sources,
|
||||
const SourcesCompanion(isActive: Value(null)),
|
||||
where: (t) => t.id.isNotValue(id),
|
||||
);
|
||||
batch.update(
|
||||
sources,
|
||||
const SourcesCompanion(isActive: Value(true)),
|
||||
where: (t) => t.id.equals(id),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateSettings(AppSettingsCompanion settings) async {
|
||||
await into(appSettings).insertOnConflictUpdate(settings);
|
||||
}
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'subtracks.sqlite'));
|
||||
// return NativeDatabase.createInBackground(file, logStatements: true);
|
||||
|
||||
return ErrorLoggingDatabase(
|
||||
NativeDatabase.createInBackground(file),
|
||||
(e, s) => log.severe('SQL error', e, s),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
SubtracksDatabase database(DatabaseRef ref) {
|
||||
return SubtracksDatabase();
|
||||
}
|
||||
|
||||
OrderingTerm _buildOrder(SortBy sort) {
|
||||
OrderingMode? mode =
|
||||
sort.dir == SortDirection.asc ? OrderingMode.asc : OrderingMode.desc;
|
||||
return OrderingTerm(
|
||||
expression: CustomExpression(sort.column),
|
||||
mode: mode,
|
||||
);
|
||||
}
|
||||
|
||||
SimpleSelectStatement<T, R> listQuery<T extends HasResultSet, R>(
|
||||
SimpleSelectStatement<T, R> query,
|
||||
ListQuery opt,
|
||||
) {
|
||||
if (opt.page.limit > 0) {
|
||||
query.limit(opt.page.limit, offset: opt.page.offset);
|
||||
}
|
||||
|
||||
if (opt.sort != null) {
|
||||
OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc
|
||||
? OrderingMode.asc
|
||||
: OrderingMode.desc;
|
||||
query.orderBy([
|
||||
(t) => OrderingTerm(
|
||||
expression: CustomExpression(opt.sort!.column),
|
||||
mode: mode,
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
for (var filter in opt.filters) {
|
||||
query.where((tbl) => buildFilter(filter));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
JoinedSelectStatement<T, R> listQueryJoined<T extends HasResultSet, R>(
|
||||
JoinedSelectStatement<T, R> query,
|
||||
ListQuery opt,
|
||||
) {
|
||||
if (opt.page.limit > 0) {
|
||||
query.limit(opt.page.limit, offset: opt.page.offset);
|
||||
}
|
||||
|
||||
if (opt.sort != null) {
|
||||
OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc
|
||||
? OrderingMode.asc
|
||||
: OrderingMode.desc;
|
||||
query.orderBy([
|
||||
OrderingTerm(
|
||||
expression: CustomExpression(opt.sort!.column),
|
||||
mode: mode,
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
for (var filter in opt.filters) {
|
||||
query.where(buildFilter(filter));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
CustomExpression<T> buildFilter<T extends Object>(
|
||||
FilterWith filter,
|
||||
) {
|
||||
return filter.when(
|
||||
equals: (column, value, invert) => CustomExpression<T>(
|
||||
'$column ${invert ? '<>' : '='} \'$value\'',
|
||||
),
|
||||
greaterThan: (column, value, orEquals) => CustomExpression<T>(
|
||||
'$column ${orEquals ? '>=' : '>'} $value',
|
||||
),
|
||||
isNull: (column, invert) => CustomExpression<T>(
|
||||
'$column ${invert ? 'IS NOT' : 'IS'} NULL',
|
||||
),
|
||||
betweenInt: (column, from, to) => CustomExpression<T>(
|
||||
'$column BETWEEN $from AND $to',
|
||||
),
|
||||
isIn: (column, invert, values) => CustomExpression<T>(
|
||||
'$column ${invert ? 'NOT IN' : 'IN'} (${values.join(',')})',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class AlbumSongsCompanion {
|
||||
final AlbumsCompanion album;
|
||||
final Iterable<SongsCompanion> songs;
|
||||
|
||||
AlbumSongsCompanion(this.album, this.songs);
|
||||
}
|
||||
|
||||
class ArtistAlbumsCompanion {
|
||||
final ArtistsCompanion artist;
|
||||
final Iterable<AlbumsCompanion> albums;
|
||||
|
||||
ArtistAlbumsCompanion(this.artist, this.albums);
|
||||
}
|
||||
|
||||
class PlaylistWithSongsCompanion {
|
||||
final PlaylistsCompanion playist;
|
||||
final Iterable<PlaylistSongsCompanion> songs;
|
||||
|
||||
PlaylistWithSongsCompanion(this.playist, this.songs);
|
||||
}
|
||||
|
||||
// Future<void> saveArtist(
|
||||
// SubtracksDatabase db,
|
||||
// ArtistAlbumsCompanion artistAlbums,
|
||||
// ) async {
|
||||
// return db.background((db) async {
|
||||
// final artist = artistAlbums.artist;
|
||||
// final albums = artistAlbums.albums;
|
||||
|
||||
// await db.batch((batch) {
|
||||
// batch.insertAllOnConflictUpdate(db.artists, [artist]);
|
||||
// batch.insertAllOnConflictUpdate(db.albums, albums);
|
||||
|
||||
// // remove this artistId from albums not found in source
|
||||
// // don't delete them since they coud have been moved to another artist
|
||||
// // that we haven't synced yet
|
||||
// final albumIds = {for (var a in albums) a.id.value};
|
||||
// batch.update(
|
||||
// db.albums,
|
||||
// const AlbumsCompanion(artistId: Value(null)),
|
||||
// where: (tbl) =>
|
||||
// tbl.sourceId.equals(artist.sourceId.value) &
|
||||
// tbl.artistId.equals(artist.id.value) &
|
||||
// tbl.id.isNotIn(albumIds),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// Future<void> saveAlbum(
|
||||
// SubtracksDatabase db,
|
||||
// AlbumSongsCompanion albumSongs,
|
||||
// ) async {
|
||||
// return db.background((db) async {
|
||||
// final album = albumSongs.album.copyWith(synced: Value(DateTime.now()));
|
||||
// final songs = albumSongs.songs;
|
||||
|
||||
// final songIds = {for (var a in songs) a.id.value};
|
||||
// final hardDeletedSongIds = (await (db.selectOnly(db.songs)
|
||||
// ..addColumns([db.songs.id])
|
||||
// ..where(
|
||||
// db.songs.sourceId.equals(album.sourceId.value) &
|
||||
// db.songs.albumId.equals(album.id.value) &
|
||||
// db.songs.id.isNotIn(songIds) &
|
||||
// db.songs.downloadFilePath.isNull() &
|
||||
// db.songs.downloadTaskId.isNull(),
|
||||
// ))
|
||||
// .map((row) => row.read(db.songs.id))
|
||||
// .get())
|
||||
// .whereNotNull();
|
||||
|
||||
// await db.batch((batch) {
|
||||
// batch.insertAllOnConflictUpdate(db.albums, [album]);
|
||||
// batch.insertAllOnConflictUpdate(db.songs, songs);
|
||||
|
||||
// // soft delete songs that have been downloaded so that the user
|
||||
// // can decide to keep or remove them later
|
||||
// // TODO: add a setting to skip soft delete and just remove download too
|
||||
// batch.update(
|
||||
// db.songs,
|
||||
// const SongsCompanion(isDeleted: Value(true)),
|
||||
// where: (tbl) =>
|
||||
// tbl.sourceId.equals(album.sourceId.value) &
|
||||
// tbl.albumId.equals(album.id.value) &
|
||||
// tbl.id.isNotIn(songIds) &
|
||||
// (tbl.downloadFilePath.isNotNull() | tbl.downloadTaskId.isNotNull()),
|
||||
// );
|
||||
|
||||
// // safe to hard delete songs that have not been downloaded
|
||||
// batch.deleteWhere(
|
||||
// db.songs,
|
||||
// (tbl) =>
|
||||
// tbl.sourceId.equals(album.sourceId.value) &
|
||||
// tbl.id.isIn(hardDeletedSongIds),
|
||||
// );
|
||||
|
||||
// // also need to remove these songs from any playlists that contain them
|
||||
// batch.deleteWhere(
|
||||
// db.playlistSongs,
|
||||
// (tbl) =>
|
||||
// tbl.sourceId.equals(album.sourceId.value) &
|
||||
// tbl.songId.isIn(hardDeletedSongIds),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// Future<void> savePlaylist(
|
||||
// SubtracksDatabase db,
|
||||
// PlaylistWithSongsCompanion playlistWithSongs,
|
||||
// ) async {
|
||||
// return db.background((db) async {
|
||||
// final playlist =
|
||||
// playlistWithSongs.playist.copyWith(synced: Value(DateTime.now()));
|
||||
// final songs = playlistWithSongs.songs;
|
||||
|
||||
// await db.batch((batch) {
|
||||
// batch.insertAllOnConflictUpdate(db.playlists, [playlist]);
|
||||
// batch.insertAllOnConflictUpdate(db.songs, songs);
|
||||
|
||||
// batch.insertAllOnConflictUpdate(
|
||||
// db.playlistSongs,
|
||||
// songs.mapIndexed(
|
||||
// (index, song) => PlaylistSongsCompanion.insert(
|
||||
// sourceId: playlist.sourceId.value,
|
||||
// playlistId: playlist.id.value,
|
||||
// songId: song.id.value,
|
||||
// position: index,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
|
||||
// // the new playlist could be shorter than the old one, so we delete
|
||||
// // playlist songs above our new playlist's length
|
||||
// batch.deleteWhere(
|
||||
// db.playlistSongs,
|
||||
// (tbl) =>
|
||||
// tbl.sourceId.equals(playlist.sourceId.value) &
|
||||
// tbl.playlistId.equals(playlist.id.value) &
|
||||
// tbl.position.isBiggerOrEqualValue(songs.length),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,94 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/isolate.dart';
|
||||
|
||||
/// https://github.com/simolus3/drift/issues/2326#issuecomment-1445138730
|
||||
class ErrorLoggingDatabase implements QueryExecutor {
|
||||
final QueryExecutor inner;
|
||||
final void Function(Object, StackTrace) onError;
|
||||
|
||||
ErrorLoggingDatabase(this.inner, this.onError);
|
||||
|
||||
Future<T> _handleErrors<T>(Future<T> Function() body) {
|
||||
return Future.sync(body)
|
||||
.onError<DriftWrappedException>((error, stackTrace) {
|
||||
onError(error, error.trace ?? stackTrace);
|
||||
throw error;
|
||||
}).onError<DriftRemoteException>((error, stackTrace) {
|
||||
onError(error, error.remoteStackTrace ?? stackTrace);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
return _ErrorLoggingTransactionExecutor(inner.beginTransaction(), onError);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
return _handleErrors(inner.close);
|
||||
}
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => inner.dialect;
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
return _handleErrors(() => inner.ensureOpen(user));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(BatchedStatements statements) {
|
||||
return _handleErrors(() => inner.runBatched(statements));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, [List<Object?>? args]) {
|
||||
return _handleErrors(() => inner.runCustom(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List<Object?> args) {
|
||||
return _handleErrors(() => inner.runDelete(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List<Object?> args) {
|
||||
return _handleErrors(() => inner.runInsert(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
String statement, List<Object?> args) {
|
||||
return _handleErrors(() => inner.runSelect(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List<Object?> args) {
|
||||
return _handleErrors(() => inner.runUpdate(statement, args));
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorLoggingTransactionExecutor extends ErrorLoggingDatabase
|
||||
implements TransactionExecutor {
|
||||
final TransactionExecutor transaction;
|
||||
|
||||
_ErrorLoggingTransactionExecutor(
|
||||
this.transaction, void Function(Object, StackTrace) onError)
|
||||
: super(transaction, onError);
|
||||
|
||||
@override
|
||||
Future<void> rollback() {
|
||||
return _handleErrors(transaction.rollback);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send() {
|
||||
return _handleErrors(transaction.send);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get supportsNestedTransactions => transaction.supportsNestedTransactions;
|
||||
}
|
||||
@@ -1,567 +0,0 @@
|
||||
import '../models/music.dart';
|
||||
import '../models/settings.dart';
|
||||
import '../models/support.dart';
|
||||
import 'converters.dart';
|
||||
|
||||
--
|
||||
-- SCHEMA
|
||||
--
|
||||
|
||||
CREATE TABLE queue(
|
||||
"index" INT NOT NULL PRIMARY KEY UNIQUE,
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
context ENUM(QueueContextType) NOT NULL,
|
||||
context_id TEXT,
|
||||
current_track BOOLEAN UNIQUE
|
||||
);
|
||||
CREATE INDEX queue_index ON queue ("index");
|
||||
CREATE INDEX queue_current_track ON queue ("current_track");
|
||||
|
||||
CREATE TABLE last_audio_state(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
queue_mode ENUM(QueueMode) NOT NULL,
|
||||
shuffle_indicies TEXT MAPPED BY `const IListIntConverter()`,
|
||||
repeat ENUM(RepeatMode) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE last_bottom_nav_state(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
tab TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE last_library_state(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
tab TEXT NOT NULL,
|
||||
albums_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
||||
artists_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
||||
playlists_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
||||
songs_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`
|
||||
);
|
||||
|
||||
CREATE TABLE app_settings(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
max_bitrate_wifi INT NOT NULL,
|
||||
max_bitrate_mobile INT NOT NULL,
|
||||
stream_format TEXT
|
||||
) WITH AppSettings;
|
||||
|
||||
CREATE TABLE sources(
|
||||
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
address TEXT NOT NULL MAPPED BY `const UriConverter()`,
|
||||
is_active BOOLEAN UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP))
|
||||
);
|
||||
|
||||
CREATE TABLE subsonic_sources(
|
||||
source_id INT NOT NULL PRIMARY KEY,
|
||||
features TEXT NOT NULL MAPPED BY `const SubsonicFeatureListConverter()`,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
use_token_auth BOOLEAN NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE artists(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
album_count INT NOT NULL,
|
||||
starred DATETIME,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Artist;
|
||||
CREATE INDEX artists_source_id ON artists (source_id);
|
||||
|
||||
CREATE VIRTUAL TABLE artists_fts USING fts5(source_id, name, content=artists, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER artists_ai AFTER INSERT ON artists BEGIN
|
||||
INSERT INTO artists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER artists_ad AFTER DELETE ON artists BEGIN
|
||||
INSERT INTO artists_fts(artists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER artists_au AFTER UPDATE ON artists BEGIN
|
||||
INSERT INTO artists_fts(artists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
INSERT INTO artists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TABLE albums(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
artist_id TEXT,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
album_artist TEXT COLLATE NOCASE,
|
||||
created DATETIME NOT NULL,
|
||||
cover_art TEXT,
|
||||
genre TEXT,
|
||||
year INT,
|
||||
starred DATETIME,
|
||||
song_count INT NOT NULL,
|
||||
frequent_rank INT,
|
||||
recent_rank INT,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT 0,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Album;
|
||||
CREATE INDEX albums_source_id ON albums (source_id);
|
||||
CREATE INDEX albums_source_id_artist_id_idx ON albums (source_id, artist_id);
|
||||
|
||||
CREATE VIRTUAL TABLE albums_fts USING fts5(source_id, name, content=albums, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER albums_ai AFTER INSERT ON albums BEGIN
|
||||
INSERT INTO albums_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER albums_ad AFTER DELETE ON albums BEGIN
|
||||
INSERT INTO albums_fts(albums_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER albums_au AFTER UPDATE ON albums BEGIN
|
||||
INSERT INTO albums_fts(albums_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
INSERT INTO albums_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TABLE playlists(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
comment TEXT COLLATE NOCASE,
|
||||
cover_art TEXT,
|
||||
song_count INT NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Playlist;
|
||||
CREATE INDEX playlists_source_id ON playlists (source_id);
|
||||
CREATE TABLE playlist_songs(
|
||||
source_id INT NOT NULL,
|
||||
playlist_id TEXT NOT NULL,
|
||||
song_id TEXT NOT NULL,
|
||||
position INT NOT NULL,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, playlist_id, position),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX playlist_songs_source_id_playlist_id_idx ON playlist_songs (source_id, playlist_id);
|
||||
CREATE INDEX playlist_songs_source_id_song_id_idx ON playlist_songs (source_id, song_id);
|
||||
|
||||
CREATE VIRTUAL TABLE playlists_fts USING fts5(source_id, name, content=playlists, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER playlists_ai AFTER INSERT ON playlists BEGIN
|
||||
INSERT INTO playlists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER playlists_ad AFTER DELETE ON playlists BEGIN
|
||||
INSERT INTO playlists_fts(playlists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER playlists_au AFTER UPDATE ON playlists BEGIN
|
||||
INSERT INTO playlists_fts(playlists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
INSERT INTO playlists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TABLE songs(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
album_id TEXT,
|
||||
artist_id TEXT,
|
||||
title TEXT NOT NULL COLLATE NOCASE,
|
||||
album TEXT COLLATE NOCASE,
|
||||
artist TEXT COLLATE NOCASE,
|
||||
duration INT MAPPED BY `const DurationSecondsConverter()`,
|
||||
track INT,
|
||||
disc INT,
|
||||
starred DATETIME,
|
||||
genre TEXT,
|
||||
download_task_id TEXT UNIQUE,
|
||||
download_file_path TEXT UNIQUE,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT 0,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Song;
|
||||
CREATE INDEX songs_source_id_album_id_idx ON songs (source_id, album_id);
|
||||
CREATE INDEX songs_source_id_artist_id_idx ON songs (source_id, artist_id);
|
||||
CREATE INDEX songs_download_task_id_idx ON songs (download_task_id);
|
||||
|
||||
CREATE VIRTUAL TABLE songs_fts USING fts5(source_id, title, content=songs, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER songs_ai AFTER INSERT ON songs BEGIN
|
||||
INSERT INTO songs_fts(rowid, source_id, title)
|
||||
VALUES (new.rowid, new.source_id, new.title);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER songs_ad AFTER DELETE ON songs BEGIN
|
||||
INSERT INTO songs_fts(songs_fts, rowid, source_id, title)
|
||||
VALUES('delete', old.rowid, old.source_id, old.title);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER songs_au AFTER UPDATE ON songs BEGIN
|
||||
INSERT INTO songs_fts(songs_fts, rowid, source_id, title)
|
||||
VALUES('delete', old.rowid, old.source_id, old.title);
|
||||
INSERT INTO songs_fts(rowid, source_id, title)
|
||||
VALUES (new.rowid, new.source_id, new.title);
|
||||
END;
|
||||
|
||||
--
|
||||
-- QUERIES
|
||||
--
|
||||
|
||||
sourcesCount:
|
||||
SELECT COUNT(*)
|
||||
FROM sources;
|
||||
|
||||
allSubsonicSources WITH SubsonicSettings:
|
||||
SELECT
|
||||
sources.id,
|
||||
sources.name,
|
||||
sources.address,
|
||||
sources.is_active,
|
||||
sources.created_at,
|
||||
subsonic_sources.features,
|
||||
subsonic_sources.username,
|
||||
subsonic_sources.password,
|
||||
subsonic_sources.use_token_auth
|
||||
FROM sources
|
||||
JOIN subsonic_sources ON subsonic_sources.source_id = sources.id;
|
||||
|
||||
albumIdsWithDownloadStatus:
|
||||
SELECT albums.id
|
||||
FROM albums
|
||||
JOIN songs on songs.source_id = albums.source_id AND songs.album_id = albums.id
|
||||
WHERE
|
||||
albums.source_id = :source_id
|
||||
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
||||
GROUP BY albums.id;
|
||||
|
||||
artistIdsWithDownloadStatus:
|
||||
SELECT artists.id
|
||||
FROM artists
|
||||
LEFT JOIN albums ON artists.source_id = albums.source_id AND artists.id = albums.artist_id
|
||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE
|
||||
artists.source_id = :source_id
|
||||
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
||||
GROUP BY artists.id;
|
||||
|
||||
playlistIdsWithDownloadStatus:
|
||||
SELECT playlists.id
|
||||
FROM playlists
|
||||
LEFT JOIN playlist_songs ON playlist_songs.source_id = playlists.source_id AND playlist_songs.playlist_id = playlists.id
|
||||
LEFT JOIN songs ON playlist_songs.source_id = songs.source_id AND playlist_songs.song_id = songs.id
|
||||
WHERE
|
||||
playlists.source_id = :source_id
|
||||
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
||||
GROUP BY playlists.id;
|
||||
|
||||
searchArtists:
|
||||
SELECT rowid
|
||||
FROM artists_fts
|
||||
WHERE artists_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
searchAlbums:
|
||||
SELECT rowid
|
||||
FROM albums_fts
|
||||
WHERE albums_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
searchPlaylists:
|
||||
SELECT rowid
|
||||
FROM playlists_fts
|
||||
WHERE playlists_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
searchSongs:
|
||||
SELECT rowid
|
||||
FROM songs_fts
|
||||
WHERE songs_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
artistById:
|
||||
SELECT * FROM artists
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumById:
|
||||
SELECT * FROM albums
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumsByArtistId:
|
||||
SELECT * FROM albums
|
||||
WHERE source_id = :source_id AND artist_id = :artist_id;
|
||||
|
||||
albumsInIds:
|
||||
SELECT * FROM albums
|
||||
WHERE source_id = :source_id AND id IN :ids;
|
||||
|
||||
playlistById:
|
||||
SELECT * FROM playlists
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
songById:
|
||||
SELECT * FROM songs
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumGenres:
|
||||
SELECT
|
||||
genre
|
||||
FROM albums
|
||||
WHERE genre IS NOT NULL AND source_id = :source_id
|
||||
GROUP BY genre
|
||||
ORDER BY COUNT(genre) DESC
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
albumsByGenre:
|
||||
SELECT
|
||||
albums.*
|
||||
FROM albums
|
||||
JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE songs.source_id = :source_id AND songs.genre = :genre
|
||||
GROUP BY albums.id
|
||||
ORDER BY albums.created DESC, albums.name
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
filterSongsByGenre:
|
||||
SELECT
|
||||
songs.*
|
||||
FROM songs
|
||||
JOIN albums ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
songsByGenreCount:
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM songs
|
||||
WHERE songs.source_id = :source_id AND songs.genre = :genre;
|
||||
|
||||
songsWithDownloadTasks:
|
||||
SELECT * FROM songs
|
||||
WHERE download_task_id IS NOT NULL;
|
||||
|
||||
songByDownloadTask:
|
||||
SELECT * FROM songs
|
||||
WHERE download_task_id = :task_id;
|
||||
|
||||
clearSongDownloadTaskBySong:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
completeSongDownload:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL,
|
||||
download_file_path = :file_path
|
||||
WHERE download_task_id = :task_id;
|
||||
|
||||
clearSongDownloadTask:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL,
|
||||
download_file_path = NULL
|
||||
WHERE download_task_id = :task_id;
|
||||
|
||||
updateSongDownloadTask:
|
||||
UPDATE songs SET
|
||||
download_task_id = :task_id
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
deleteSongDownloadFile:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL,
|
||||
download_file_path = NULL
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumDownloadStatus WITH ListDownloadStatus:
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS downloaded,
|
||||
COUNT(CASE WHEN songs.download_task_id IS NOT NULL THEN songs.id ELSE NULL END) AS downloading
|
||||
FROM albums
|
||||
JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE albums.source_id = :source_id AND albums.id = :id;
|
||||
|
||||
playlistDownloadStatus WITH ListDownloadStatus:
|
||||
SELECT
|
||||
COUNT(DISTINCT songs.id) as total,
|
||||
COUNT(DISTINCT CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS downloaded,
|
||||
COUNT(DISTINCT CASE WHEN songs.download_task_id IS NOT NULL THEN songs.id ELSE NULL END) AS downloading
|
||||
FROM playlists
|
||||
JOIN playlist_songs ON
|
||||
playlist_songs.source_id = playlists.source_id
|
||||
AND playlist_songs.playlist_id = playlists.id
|
||||
JOIN songs ON
|
||||
songs.source_id = playlist_songs.source_id
|
||||
AND songs.id = playlist_songs.song_id
|
||||
WHERE
|
||||
playlists.source_id = :source_id AND playlists.id = :id;
|
||||
|
||||
filterAlbums:
|
||||
SELECT
|
||||
albums.*
|
||||
FROM albums
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterAlbumsDownloaded:
|
||||
SELECT
|
||||
albums.*
|
||||
FROM albums
|
||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE $predicate
|
||||
GROUP BY albums.source_id, albums.id
|
||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterArtists:
|
||||
SELECT
|
||||
artists.*
|
||||
FROM artists
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterArtistsDownloaded WITH Artist:
|
||||
SELECT
|
||||
artists.*,
|
||||
COUNT(DISTINCT CASE WHEN songs.download_file_path IS NOT NULL THEN songs.album_id ELSE NULL END) AS album_count
|
||||
FROM artists
|
||||
LEFT JOIN albums ON artists.source_id = albums.source_id AND artists.id = albums.artist_id
|
||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE $predicate
|
||||
GROUP BY artists.source_id, artists.id
|
||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterPlaylists:
|
||||
SELECT
|
||||
playlists.*
|
||||
FROM playlists
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterPlaylistsDownloaded WITH Playlist:
|
||||
SELECT
|
||||
playlists.*,
|
||||
COUNT(CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS song_count
|
||||
FROM playlists
|
||||
LEFT JOIN playlist_songs ON playlist_songs.source_id = playlists.source_id AND playlist_songs.playlist_id = playlists.id
|
||||
LEFT JOIN songs ON playlist_songs.source_id = songs.source_id AND playlist_songs.song_id = songs.id
|
||||
WHERE $predicate
|
||||
GROUP BY playlists.source_id, playlists.id
|
||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterSongs:
|
||||
SELECT
|
||||
songs.*
|
||||
FROM songs
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterSongsDownloaded:
|
||||
SELECT
|
||||
songs.*
|
||||
FROM songs
|
||||
WHERE $predicate AND songs.download_file_path IS NOT NULL
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
playlistIsDownloaded:
|
||||
SELECT
|
||||
COUNT(*) = 0
|
||||
FROM playlists
|
||||
JOIN playlist_songs ON
|
||||
playlist_songs.source_id = playlists.source_id
|
||||
AND playlist_songs.playlist_id = playlists.id
|
||||
JOIN songs ON
|
||||
songs.source_id = playlist_songs.source_id
|
||||
AND songs.id = playlist_songs.song_id
|
||||
WHERE
|
||||
playlists.source_id = :source_id AND playlists.id = :id
|
||||
AND songs.download_file_path IS NULL;
|
||||
|
||||
playlistHasDownloadsInProgress:
|
||||
SELECT
|
||||
COUNT(*) > 0
|
||||
FROM playlists
|
||||
JOIN playlist_songs ON
|
||||
playlist_songs.source_id = playlists.source_id
|
||||
AND playlist_songs.playlist_id = playlists.id
|
||||
JOIN songs ON
|
||||
songs.source_id = playlist_songs.source_id
|
||||
AND songs.id = playlist_songs.song_id
|
||||
WHERE playlists.source_id = :source_id AND playlists.id = :id
|
||||
AND songs.download_task_id IS NOT NULL;
|
||||
|
||||
songsInIds:
|
||||
SELECT *
|
||||
FROM songs
|
||||
WHERE source_id = :source_id AND id IN :ids;
|
||||
|
||||
songsInRowIds:
|
||||
SELECT *
|
||||
FROM songs
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
albumsInRowIds:
|
||||
SELECT *
|
||||
FROM albums
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
artistsInRowIds:
|
||||
SELECT *
|
||||
FROM artists
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
playlistsInRowIds:
|
||||
SELECT *
|
||||
FROM playlists
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
currentTrackIndex:
|
||||
SELECT
|
||||
queue."index"
|
||||
FROM queue
|
||||
WHERE queue.current_track = 1;
|
||||
|
||||
queueLength:
|
||||
SELECT COUNT(*) FROM queue;
|
||||
|
||||
queueInIndicies:
|
||||
SELECT *
|
||||
FROM queue
|
||||
WHERE queue."index" IN :indicies;
|
||||
|
||||
getAppSettings:
|
||||
SELECT * FROM app_settings
|
||||
WHERE id = 1;
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<String> datapasePath(String database) async =>
|
||||
join((await getApplicationSupportDirectory()).path, 'databases', database);
|
||||
|
||||
extension DateTimeExt on DateTime {
|
||||
String toDb() => DateFormat('yyyy-MM-dd hh:mm:ss').format(toUtc());
|
||||
|
||||
static DateTime parseUtc(Object? obj) {
|
||||
final str = obj.toString();
|
||||
return DateTime.parse(hasTimeZone(str) ? str : '${obj}Z').toLocal();
|
||||
}
|
||||
|
||||
static DateTime? tryParseUtc(Object? obj) {
|
||||
final str = obj.toString();
|
||||
return DateTime.tryParse(hasTimeZone(str) ? str : '${obj}Z')?.toLocal();
|
||||
}
|
||||
|
||||
static bool hasTimeZone(String str) =>
|
||||
RegExp(r'(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])').hasMatch(str);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:http/http.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../log.dart';
|
||||
|
||||
part 'client.g.dart';
|
||||
|
||||
const Map<String, String> subtracksHeaders = {
|
||||
'user-agent': 'subtracks/android',
|
||||
};
|
||||
|
||||
class SubtracksHttpClient extends BaseClient {
|
||||
SubtracksHttpClient();
|
||||
|
||||
@override
|
||||
Future<StreamedResponse> send(BaseRequest request) {
|
||||
request.headers.addAll(subtracksHeaders);
|
||||
log.info('${request.method} ${request.url}');
|
||||
|
||||
try {
|
||||
return request.send();
|
||||
} catch (e, st) {
|
||||
log.severe('HTTP client: ${request.method} ${request.url}', e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
BaseClient httpClient(HttpClientRef ref) {
|
||||
return SubtracksHttpClient();
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'client.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$httpClientHash() => r'78f604dbc249a854728d71bde8289d48f6700be7';
|
||||
|
||||
/// See also [httpClient].
|
||||
@ProviderFor(httpClient)
|
||||
final httpClientProvider = Provider<BaseClient>.internal(
|
||||
httpClient,
|
||||
name: r'httpClientProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$httpClientHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef HttpClientRef = ProviderRef<BaseClient>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "مميز",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "ازل التمييز",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "لا شيء هنا…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "الرئيسية",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "المكتبة",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "بحث",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "الإعدادات",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "شَغل الألبوم",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "أعرض الألبوم",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "فرز الألبومات",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{} few{ألبوم} many{} other{}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "أظهر الفنان",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "فرز الفنانين",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{} few{فنان} many{فنانان} other{فنانان}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "حسب النوع",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "موسوم",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "شغل قائمة التشغيل",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{} few{قائمة تشغيل} many{قائمتان تشغيل} other{قائمتان تشغيل}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{} few{صف} many{صفين} other{صفين}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "أشهر الأغاني",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{} few{أُغْنِيَة} many{} other{}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "أضيف حديثا",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "حسب الفنان/ة",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "مشغل كثيرا",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "حسب الاسم",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "عشوائي",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "شغل حديثا",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "حسب السنة",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "بحث: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "بحث",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "المزيد…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "نتائج البحث",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "الرخص",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "موقع المشروع",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "حول",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "الإصدار {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "موسيقى",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "لا تستورد سجل التشغيل",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "استيراد سجل التشغيل",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "استيراد سجل التشغيل",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "الشبكة",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "أقصى معدل نقل بيانات (mobile)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "أقصى معدل نقل بيانات (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "الحد الأقصى من وقت التخزين المؤقت",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "الحد الأدنى من وقت التخزين المؤقت",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value} كيلو بايت في الثانية",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} ثواني",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "غير محدود",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "مسح ذاكرة التخزين المؤقت للصور",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "إعادة ضبط",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "أضف سيرفر",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "حذف",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "عدل السيرفر",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "حفظ",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "أخبر الأتصال",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "العناوين",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "كلمة المرور",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "إسم المستخدم",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "الأتصال ب {address} فشل، ابحث في الإعدادات او السيرفر",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "الأتصال ب {address} جيد!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "السيرفرات",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "أرسل كلمة المرور على شكل توكِن",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "أرسل كلمة المرور بنص عادي (قديم ، تأكد من أن اتصالك آمن!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "أظهر كلمة المرور",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Afegir als favorits",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Retirar estrella",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Aquí no hi ha res…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Inici",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Biblioteca",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Cercar",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Paràmetres",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Reproduir l'àlbum",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Veure l'àlbum",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Ordenar els àlbums",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Àlbum} other{Àlbums}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Veure l'artista",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Ordenar els artistes",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artista} other{Artistes}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Per gènere",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Favorits",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Reproduir la llista de reproducció",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Llista de reproducció} other{Llistes de reproducció}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Cua} other{Cues}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Millors cançons",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Cançó} other{Cançons}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Afegit recentment",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Per artista",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Escoltat freqüentment",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Pel nom",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Aleatori",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Reproduït recentment",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Per any",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Cercar: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Cercar",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Més…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Resultats de la cerca",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Llicències",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Pàgina d'inici del projecte",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Quant a",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "versió {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Música",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "No capturar l'historial de reproducció",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Capturar l'historial de reproduccions",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Capturar la lectura",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Xarxa",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Taxa de bits màxima (mòbil)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Taxa de bits màxima (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Temps màxim en memòria intermèdia",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Temps mínim en memòria intermèdia",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value} kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} segons",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Il·limitat",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Esborrar la memòria cau d'imatges",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Reinicialitzar",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Afegir un servidor",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Esborrar",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Editar el servidor",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Desar",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Comprovar la connexió",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adreça",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Contrasenya",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Nom d’usuari",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "La connexió a {address} ha fallat, comprova la configuració o el servidor",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Connexió a {address} OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Servidors",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Enviar contrasenya com a token + salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar la contrasenya en text sense format (obsolet, assegura't que la teva connexió sigui segura!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Forçar la contrasenya de text sense format",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Ohodnotit",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Zrušit hodnocení",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Zde nic není…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Domů",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Knihovna",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Hledat",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Nastavení",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Přehrát album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Zobrazit album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Seřadit alba",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} few{Alba} many{Alba} other{Alba}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Zobrazit umělce",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Seřadit umělce",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Umělec} few{Umělci} many{Umělci} other{Umělci}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Podle žánru",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Ohodnocené",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Přehrát seznam skladeb",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Seznam skladeb} few{Seznamy skladeb} many{Seznamy skladeb} other{Seznamy skladeb}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Fronta} few{Fronty} many{Fronty} other{Fronty}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Top skladby",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Skladba} few{Skladby} many{Skladby} other{Skladby}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Nedávno přidané",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Umělce",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Často přehrávané",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Názvu",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Náhodně",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Často přehrávané",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Roku",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Hledat: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Hledat",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Více…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Výsledky hledání",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsNetworkName": "Síť",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Maximální datový tok (mobil)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Maximální datový tok (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} sekund",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Neomezeno",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsServersActionsAdd": "Přidat server",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Odstranit",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Upravit server",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Uložit",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Otestovat spojení",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adresa",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Heslo",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Uživ. jméno",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Připojení k {address} selhalo, zkontrolujte nastavení nebo server",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Připojení k {address} je OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Servery",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Posílat heslo jako token + salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Posílat heslo v prostém textu (zastaralé, ujistěte se, že je vaše připojení zabezpečené!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Vynutit heslo ve formátu prostého textu",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {},
|
||||
"actionsDownloadDelete": "Smazat stažené",
|
||||
"@actionsDownloadDelete": {},
|
||||
"actionsOk": "OK",
|
||||
"@actionsOk": {},
|
||||
"actionsCancel": "Zrušit",
|
||||
"@actionsCancel": {},
|
||||
"actionsDownload": "Stáhnout",
|
||||
"@actionsDownload": {},
|
||||
"controlsShuffle": "Náhodně",
|
||||
"@controlsShuffle": {},
|
||||
"resourcesFilterAlbum": "Album",
|
||||
"@resourcesFilterAlbum": {},
|
||||
"resourcesFilterArtist": "Umělec",
|
||||
"@resourcesFilterArtist": {},
|
||||
"resourcesFilterYear": "Rok",
|
||||
"@resourcesFilterYear": {},
|
||||
"resourcesFilterOwner": "Majitele",
|
||||
"@resourcesFilterOwner": {},
|
||||
"resourcesSongListDeleteAllTitle": "Smazat stažené?",
|
||||
"@resourcesSongListDeleteAllTitle": {},
|
||||
"resourcesSongListDeleteAllContent": "Toto odstraní všechny stažené soubory s hudbou.",
|
||||
"@resourcesSongListDeleteAllContent": {},
|
||||
"resourcesSortByUpdated": "Naposledy upravené",
|
||||
"@resourcesSortByUpdated": {},
|
||||
"resourcesSortByAlbum": "Alba",
|
||||
"@resourcesSortByAlbum": {},
|
||||
"resourcesSortByAlbumCount": "Počtu alb",
|
||||
"@resourcesSortByAlbumCount": {},
|
||||
"resourcesSortByTitle": "Názvu",
|
||||
"@resourcesSortByTitle": {},
|
||||
"settingsAboutActionsLicenses": "Licence",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Stránka projektu",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutActionsSupport": "Podpořit vývojáře 💜",
|
||||
"@settingsAboutActionsSupport": {},
|
||||
"settingsAboutVersion": "verze {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkOptionsStreamFormat": "Preferovaný formát pro streamování",
|
||||
"@settingsNetworkOptionsStreamFormat": {},
|
||||
"settingsNetworkOptionsStreamFormatServerDefault": "Použít nastavení serveru",
|
||||
"@settingsNetworkOptionsStreamFormatServerDefault": {},
|
||||
"settingsResetActionsClearImageCache": "Smazat mezipaměť obrázků",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Resetovat",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersFieldsName": "Jméno",
|
||||
"@settingsServersFieldsName": {},
|
||||
"settingsAboutName": "O aplikaci",
|
||||
"@settingsAboutName": {},
|
||||
"actionsDownloadCancel": "Zrušit stahování",
|
||||
"@actionsDownloadCancel": {},
|
||||
"actionsDelete": "Smazat",
|
||||
"@actionsDelete": {}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
{
|
||||
"messagesNothingHere": "Intet her…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Hjem",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Bibliotek",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Søg",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Indstillinger",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Afspil album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Se album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Sortér albums",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} other{Albums}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Se kunstnere",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Sortér kunstnere",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Kunstner} other{Kunstnere}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Efter genre",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesPlaylistActionsPlay": "Afspil spilleliste",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Spilleliste} other{Spillelister}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Kø} other{Køer}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Top sange",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Sang} other{Sange}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByArtist": "Efter kunstner",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByName": "Efter navn",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Tilfældig",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByYear": "Efter år",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Søg: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Søg",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Mere…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Søgeresultater",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licenser",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Projekt hjemmeside",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Omkring",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "version {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Musik",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Scrobble afspilningshistorik",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Scrobble afspilninger",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Netværk",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Maksimum bitrate (mobil)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Maksimum bitrate (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Maksimum buffertid",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Minimum buffertid",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} sekunder",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Ubegrænset",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Ryd billede cache",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Nulstil",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Tilføj server",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Slet",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Redigér server",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Gem",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Test forbindelse",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adresse",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Adgangskode",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Brugernavn",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Forbindelse til {address} mislykkedes, tjek indstillinger eller server",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Forbindelse til {address} OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Servere",
|
||||
"@settingsServersName": {}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Markieren",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Markierung entfernen",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Hier ist nichts…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Startseite",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Bibliothek",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Suche",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Einstellungen",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Album abspielen",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Album anzeigen",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Alben sortieren",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} other{Alben}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Interpret anzeigen",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Interpreten sortieren",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Interpret} other{Interpreten}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Nach Genre",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Favoriten",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Wiedergabeliste abspielen",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Wiedergabeliste} other{Wiedergabelisten}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Warteschlange} other{Warteschlangen}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Top Lieder",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Lied} other{Lieder}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Kürzlich hinzugefügt",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Nach Interpreten",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Häufig abgespielt",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Nach Name",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Zufällig",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Kürzlich abgespielt",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Nach Jahr",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Suche: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Suche",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Mehr…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Suchergebnis",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Lizenzen",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Projektseite",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Über",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "Version {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Musik",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Kein Scrobble für Wiedergabeverlauf",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Scrobble Wiedergabeverlauf",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Scrobble Wiedergabe",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Netzwerk",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Maximale Bitrate (Mobil)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Maximale Bitrate (WLAN)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Maximale Pufferzeit",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Minimale Pufferzeit",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} Sekunden",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Unbegrenzt",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Bildzwischenspeicher löschen",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Zurücksetzen",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Server hinzufügen",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Löschen",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Server bearbeiten",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Speichern",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Verbindung testen",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adresse",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Passwort",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Nutzername",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Verbindung zu {address} fehlgeschlagen, überprüfe Einstellungen oder Server",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Verbindung zu {address} ist OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Server",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Sende Passwort als Token + Salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Passwort als Klartext senden (Veraltet, stellen Sie sicher, dass Ihre Verbindung sicher ist!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Erzwinge Klartextpasswort",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {},
|
||||
"actionsDelete": "Löschen",
|
||||
"@actionsDelete": {},
|
||||
"actionsDownload": "Herunterladen",
|
||||
"@actionsDownload": {},
|
||||
"actionsDownloadCancel": "Download abbrechen",
|
||||
"@actionsDownloadCancel": {},
|
||||
"controlsShuffle": "Zufall",
|
||||
"@controlsShuffle": {},
|
||||
"actionsCancel": "Abbrechen",
|
||||
"@actionsCancel": {},
|
||||
"actionsDownloadDelete": "Heruntergeladene Inhalte löschen",
|
||||
"@actionsDownloadDelete": {},
|
||||
"actionsOk": "OK",
|
||||
"@actionsOk": {},
|
||||
"resourcesAlbumCount": "{count,plural, =1{{count} Album} other{{count} Alben}}",
|
||||
"@resourcesAlbumCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterAlbum": "Album",
|
||||
"@resourcesFilterAlbum": {},
|
||||
"resourcesArtistCount": "{count,plural, =1{{count} Künstler} other{{count} Künstler}}",
|
||||
"@resourcesArtistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterArtist": "Künstler",
|
||||
"@resourcesFilterArtist": {},
|
||||
"resourcesFilterOwner": "Besitzer",
|
||||
"@resourcesFilterOwner": {},
|
||||
"resourcesFilterYear": "Jahr",
|
||||
"@resourcesFilterYear": {},
|
||||
"resourcesPlaylistCount": "{count,plural, =1{{count} Playlist} other{{count} Playlists}}",
|
||||
"@resourcesPlaylistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongCount": "{count,plural, =1{{count} Song} other{{count} Songs}}",
|
||||
"@resourcesSongCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListDeleteAllContent": "Hierdurch werden alle heruntergeladenen Inhalte entfernt.",
|
||||
"@resourcesSongListDeleteAllContent": {},
|
||||
"resourcesSortByAlbum": "Album",
|
||||
"@resourcesSortByAlbum": {},
|
||||
"resourcesSortByAlbumCount": "Albenanzahl",
|
||||
"@resourcesSortByAlbumCount": {},
|
||||
"resourcesSortByTitle": "Titel",
|
||||
"@resourcesSortByTitle": {},
|
||||
"resourcesSortByUpdated": "Kürzlich hinzugefügt",
|
||||
"@resourcesSortByUpdated": {},
|
||||
"settingsAboutActionsSupport": "Den Entwickler unterstützen",
|
||||
"@settingsAboutActionsSupport": {},
|
||||
"settingsNetworkOptionsOfflineMode": "Offline Modus",
|
||||
"@settingsNetworkOptionsOfflineMode": {},
|
||||
"settingsNetworkOptionsOfflineModeOff": "Nutze das Internet um Musik zu synchronisieren.",
|
||||
"@settingsNetworkOptionsOfflineModeOff": {},
|
||||
"settingsNetworkOptionsOfflineModeOn": "Nutze nicht das Internet um Musik zu synchronisieren.",
|
||||
"@settingsNetworkOptionsOfflineModeOn": {},
|
||||
"settingsNetworkOptionsStreamFormat": "Bevorzugtes Streaming-Format",
|
||||
"@settingsNetworkOptionsStreamFormat": {},
|
||||
"settingsServersFieldsName": "Name",
|
||||
"@settingsServersFieldsName": {},
|
||||
"resourcesSongListDeleteAllTitle": "Downloads löschen?",
|
||||
"@resourcesSongListDeleteAllTitle": {},
|
||||
"settingsNetworkOptionsStreamFormatServerDefault": "Server-Standard verwenden",
|
||||
"@settingsNetworkOptionsStreamFormatServerDefault": {}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
{
|
||||
"actionsCancel": "Cancel",
|
||||
"@actionsCancel": {},
|
||||
"actionsDelete": "Delete",
|
||||
"@actionsDelete": {},
|
||||
"actionsDownload": "Download",
|
||||
"@actionsDownload": {},
|
||||
"actionsDownloadCancel": "Cancel download",
|
||||
"@actionsDownloadCancel": {},
|
||||
"actionsDownloadDelete": "Delete downloaded",
|
||||
"@actionsDownloadDelete": {},
|
||||
"actionsOk": "OK",
|
||||
"@actionsOk": {},
|
||||
"actionsStar": "Star",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Unstar",
|
||||
"@actionsUnstar": {},
|
||||
"controlsShuffle": "Shuffle",
|
||||
"@controlsShuffle": {},
|
||||
"messagesNothingHere": "Nothing here…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Home",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Library",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Search",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Settings",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Play album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "View album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumCount": "{count,plural, =1{{count} album} other{{count} albums}}",
|
||||
"@resourcesAlbumCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesAlbumListsSort": "Sort albums",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} other{Albums}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "View artist",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistCount": "{count,plural, =1{{count} artist} other{{count} artists}}",
|
||||
"@resourcesArtistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistListsSort": "Sort artists",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artist} other{Artists}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterAlbum": "Album",
|
||||
"@resourcesFilterAlbum": {},
|
||||
"resourcesFilterArtist": "Artist",
|
||||
"@resourcesFilterArtist": {},
|
||||
"resourcesFilterGenre": "Genre",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterOwner": "Owner",
|
||||
"@resourcesFilterOwner": {},
|
||||
"resourcesFilterStarred": "Starred",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesFilterYear": "Year",
|
||||
"@resourcesFilterYear": {},
|
||||
"resourcesPlaylistActionsPlay": "Play playlist",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistCount": "{count,plural, =1{{count} playlist} other{{count} playlists}}",
|
||||
"@resourcesPlaylistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Playlist} other{Playlists}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Queue} other{Queues}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongCount": "{count,plural, =1{{count} song} other{{count} songs}}",
|
||||
"@resourcesSongCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListDeleteAllContent": "This will remove all downloaded song files.",
|
||||
"@resourcesSongListDeleteAllContent": {},
|
||||
"resourcesSongListDeleteAllTitle": "Delete downloads?",
|
||||
"@resourcesSongListDeleteAllTitle": {},
|
||||
"resourcesSongListsArtistTopSongs": "Top Songs",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Song} other{Songs}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Recently added",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByAlbum": "Album",
|
||||
"@resourcesSortByAlbum": {},
|
||||
"resourcesSortByAlbumCount": "Album count",
|
||||
"@resourcesSortByAlbumCount": {},
|
||||
"resourcesSortByArtist": "Artist",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Frequently played",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Name",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Random",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Recently played",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByTitle": "Title",
|
||||
"@resourcesSortByTitle": {},
|
||||
"resourcesSortByUpdated": "Recently updated",
|
||||
"@resourcesSortByUpdated": {},
|
||||
"resourcesSortByYear": "Year",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Search: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Search",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "More…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Search results",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licenses",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Project homepage",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutActionsSupport": "Support the developer 💜",
|
||||
"@settingsAboutActionsSupport": {},
|
||||
"settingsAboutName": "About",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutShareLogs": "Share logs",
|
||||
"@settingsAboutShareLogs": {},
|
||||
"settingsAboutChooseLog": "Choose a log file",
|
||||
"@settingsAboutChooseLog": {},
|
||||
"settingsAboutVersion": "version {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Music",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Don't scrobble play history",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Scrobble play history",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Scrobble plays",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Network",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Maximum bitrate (mobile data)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Maximum bitrate (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Maximum buffer time",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Minimum buffer time",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkOptionsOfflineMode": "Offline mode",
|
||||
"@settingsNetworkOptionsOfflineMode": {},
|
||||
"settingsNetworkOptionsOfflineModeOff": "Use the internet to sync music.",
|
||||
"@settingsNetworkOptionsOfflineModeOff": {},
|
||||
"settingsNetworkOptionsOfflineModeOn": "Don't use the internet to sync or play music.",
|
||||
"@settingsNetworkOptionsOfflineModeOn": {},
|
||||
"settingsNetworkOptionsStreamFormat": "Preferred stream format",
|
||||
"@settingsNetworkOptionsStreamFormat": {},
|
||||
"settingsNetworkOptionsStreamFormatServerDefault": "Use server default",
|
||||
"@settingsNetworkOptionsStreamFormatServerDefault": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} seconds",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Unlimited",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Clear Image Cache",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Reset",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Add source",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Delete",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Edit source",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Save",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Test connection",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Address",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsName": "Name",
|
||||
"@settingsServersFieldsName": {},
|
||||
"settingsServersFieldsPassword": "Password",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Username",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Connection to {address} failed, check settings or server",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Connection to {address} OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Sources",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Send password as token + salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Send password in plaintext (legacy, make sure your connection is secure!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Force plaintext password",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Favorito",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Retirar favorito",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Nada aquí…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Casa",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Biblioteca",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Buscar",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Entorno",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Reproducir Álbum",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Ver Álbum",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Ordenar Álbumes",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Álbum} other{Álbumes}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Ver Artista",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Oredenar Artistas",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artista} other{Artistas}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Por Género",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Estrellas",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Reproducir Lista de reproducción",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Lista de reproducción} other{Listas de reproducción}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Cola} other{Colas}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Mejores Canciones",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Cancion} other{Canciones}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Recientemente Añadido",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Por Artista",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Jugado Frecuentemente",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Por Nombre",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Aleatorio",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Recientemente Jugado",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Por Año",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Buscar: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Buscar",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Más…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Resultados de la búsqueda",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licencias",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Página de inicio del proyecto",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Información",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "versión {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Música",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "No hagas historial de reproducción de scrobble",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Historial de reproducción de scrobble",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Obras de teatro de Scrobble",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Sitio",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Tasa de bits máxima (mobile)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Tasa de bits máxima (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Máxima de buffer tiempo",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Mínimo de buffer tiempo",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value} kilobytes por segundo",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} segundos",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Ilimitados",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Caché de imágenes claras",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Reinicializar",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Agregar Servidor",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Supr",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Editar Servidor",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Enviar",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Conexión de prueba",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Alocución",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "La contraseña",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Nombre de usuario",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "La conexión a {address} falló, verifique la configuracón o el servidor",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "¡Conexión con {address} Ok!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Servidores",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Enviar contraseña como token + sal",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar contraseña en texto plano (¡legado, asegúrese de que su conexión sea segura!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Forzar contraseña de texto sin formato",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {},
|
||||
"actionsDelete": "Borrar",
|
||||
"@actionsDelete": {},
|
||||
"actionsOk": "Ok",
|
||||
"@actionsOk": {},
|
||||
"actionsDownload": "Descargar",
|
||||
"@actionsDownload": {},
|
||||
"actionsDownloadCancel": "Anular descargar",
|
||||
"@actionsDownloadCancel": {},
|
||||
"controlsShuffle": "Reproducir aleatoriamente",
|
||||
"@controlsShuffle": {},
|
||||
"actionsCancel": "Cancelar",
|
||||
"@actionsCancel": {},
|
||||
"actionsDownloadDelete": "Eliminar descargado",
|
||||
"@actionsDownloadDelete": {}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Mettre en favoris",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Enlever des favoris",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Rien ici…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Accueil",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Bibliothèque",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Recherche",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Paramètres",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Jouer l'album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Voir l'album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Trier les albums",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} other{Albums}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Voir l'artiste",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Trier les artistes",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artiste} other{Artistes}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Par Genre",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Favoris",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Lire la playlist",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Playlist} other{Playlists}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{File d'attente} other{Files d'attente}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Meilleures Chansons",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Chanson} other{Chansons}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Récemment Ajouté",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Par Artiste",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Fréquemment Joué",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Par Nom",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Aléatoire",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Récemment Joué",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Par Année",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Recherche : {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Recherche",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Plus…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Résultats de recherche",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licences",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Page d'accueil du projet",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "À propos",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "version {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Musique",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Ne pas scrobbler l'historique de lecture",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Scrobbler l'historique de lecture",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Scrobbler la lecture",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Réseau",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Débit binaire maximum (mobile)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Débit binaire maximum (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Temps maximum en mémoire tampon",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Temps minimum en mémoire tampon",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbit/s",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} secondes",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Illimité",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Vider le cache d'images",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Réinitialiser",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Ajouter un serveur",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Supprimer",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Modifier le serveur",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Sauvegarder",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Tester la connexion",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adresse",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Mot de passe",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Nom d'utilisateur",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Échec de la connexion à {address}, vérifiez les paramètres ou le serveur",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Connexion à {address} OK !",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Serveurs",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Envoyer le mot de passe sous forme de jeton + salage",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Envoyer le mot de passe en test clair (héritage, assurez-vous que la connexion est sécurisée !)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Forcer le mot de passe en texte clair",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Estrela",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Retirar",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Nada por aquí…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Inicio",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Biblioteca",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Buscar",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Axustes",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Reproducir",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Ver Álbum",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Ordenar Álbums",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Álbum} other{Álbums}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Ver Artista",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Ordenar Artistas",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artista} other{Artistas}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Por xénero",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Favoritas",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Reproducir lista",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Listaxe} other{Listaxes}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Fila} other{Filas}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Máis reproducidas",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Canción} other{Cancións}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Últimas engadidas",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Por artista",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Reproducidas a miúdo",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Por nome",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Ao chou",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Reproducidas a miúdo",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Por ano",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Buscar: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Buscar",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Máis…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Resultados",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licenzas",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Web do Proxecto",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Acerca de",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "versión {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Música",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Non rexistrar o historial de reprodución",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Rexistrar o historial de reprodución",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Rexistrar reprodución",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Rede",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Bitrate máx. (móbil)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Bitrate máx. (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Tempo máximo na memoria",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Tempo mínimo na memoria",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} segundos",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Sen límite",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Limpar a caché de imaxes",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Restablecer",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Engadir Servidor",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Eliminar",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Editar Servidor",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Gardar",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Comprobar Conexión",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Enderezo",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Contrasinal",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Identificador",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Fallou a conexión a {address}, comproba os axustes",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Conexión con {address} OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Servidores",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Enviar contrasinal como token + salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar contrasinal en texto plano (herdado, pon coidado en que a conexión sexa segura!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Forzar contrasinal en texto plano",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {},
|
||||
"actionsCancel": "Cancelar",
|
||||
"@actionsCancel": {},
|
||||
"actionsDelete": "Eliminar",
|
||||
"@actionsDelete": {},
|
||||
"actionsDownload": "Descargar",
|
||||
"@actionsDownload": {},
|
||||
"actionsDownloadCancel": "Cancelar a descarga",
|
||||
"@actionsDownloadCancel": {},
|
||||
"actionsDownloadDelete": "Eliminar o descargado",
|
||||
"@actionsDownloadDelete": {},
|
||||
"actionsOk": "OK",
|
||||
"@actionsOk": {},
|
||||
"controlsShuffle": "Barallar",
|
||||
"@controlsShuffle": {},
|
||||
"resourcesAlbumCount": "{count,plural, =1{{count} álbum} other{{count} álbums}}",
|
||||
"@resourcesAlbumCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistCount": "{count,plural, =1{{count} artista} other{{count} artistas}}",
|
||||
"@resourcesArtistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterAlbum": "Álbum",
|
||||
"@resourcesFilterAlbum": {},
|
||||
"resourcesFilterArtist": "Artista",
|
||||
"@resourcesFilterArtist": {},
|
||||
"resourcesFilterOwner": "Dono",
|
||||
"@resourcesFilterOwner": {},
|
||||
"resourcesFilterYear": "Ano",
|
||||
"@resourcesFilterYear": {},
|
||||
"resourcesPlaylistCount": "{count,plural, =1{{count} lista} other{{count} listas}}",
|
||||
"@resourcesPlaylistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongCount": "{count,plural, =1{{count} canción} other{{count} cancións}}",
|
||||
"@resourcesSongCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListDeleteAllContent": "Vas eliminar todas as cancións descargadas.",
|
||||
"@resourcesSongListDeleteAllContent": {},
|
||||
"resourcesSongListDeleteAllTitle": "Eliminar descargas?",
|
||||
"@resourcesSongListDeleteAllTitle": {},
|
||||
"resourcesSortByAlbum": "Álbum",
|
||||
"@resourcesSortByAlbum": {},
|
||||
"resourcesSortByAlbumCount": "Número de álbums",
|
||||
"@resourcesSortByAlbumCount": {},
|
||||
"settingsAboutActionsSupport": "Axuda ao desenvolvemento 💜",
|
||||
"@settingsAboutActionsSupport": {},
|
||||
"settingsNetworkOptionsOfflineMode": "Modo sen conexión",
|
||||
"@settingsNetworkOptionsOfflineMode": {},
|
||||
"settingsNetworkOptionsOfflineModeOff": "Usa internet para sincr. música.",
|
||||
"@settingsNetworkOptionsOfflineModeOff": {},
|
||||
"settingsNetworkOptionsOfflineModeOn": "Non usar internet para sincr. ou reproducir música.",
|
||||
"@settingsNetworkOptionsOfflineModeOn": {},
|
||||
"settingsNetworkOptionsStreamFormat": "Modo de reprodución preferido",
|
||||
"@settingsNetworkOptionsStreamFormat": {},
|
||||
"settingsNetworkOptionsStreamFormatServerDefault": "Usar por defecto do servidor",
|
||||
"@settingsNetworkOptionsStreamFormatServerDefault": {},
|
||||
"settingsServersFieldsName": "Nome",
|
||||
"@settingsServersFieldsName": {},
|
||||
"resourcesSortByTitle": "Título",
|
||||
"@resourcesSortByTitle": {},
|
||||
"resourcesSortByUpdated": "Actualizado recentemente",
|
||||
"@resourcesSortByUpdated": {},
|
||||
"settingsAboutShareLogs": "Compartir rexistros",
|
||||
"@settingsAboutShareLogs": {},
|
||||
"settingsAboutChooseLog": "Escolle un ficheiro de rexistro",
|
||||
"@settingsAboutChooseLog": {}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Aggiungi ai preferiti",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Rimuovi dai preferiti",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Non c'è niente qui…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Home",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Libreria",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Cerca",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Impostazioni",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Riproduci album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Vedi album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Ordina album",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} other{Album}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Vedi artista",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Ordina artisti",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artista} other{Artisti}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Per genere",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Preferiti",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Riproduci playlist",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Playlist} other{Playlist}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Coda} other{Code}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Brani più popolari",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Brano} other{Brani}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Aggiunti di recente",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Per artista",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Ascoltati frequentemente",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Per nome",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Casuale",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Ascoltati di recente",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Per anno",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Ricerca: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Ricerca",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Mostra di più…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Risultati della ricerca",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licenze",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Pagina principale del progetto",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Informazioni",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "versione {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Musica",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Non eseguire lo scrobbling della cronologia d'ascolto",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Scrobbling della cronologia di ascolto",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Scrobbling delle riproduzioni",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Rete",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Bitrate massimo (rete dati)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Bitrate massimo (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Tempo di buffer massimo",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Tempo di buffer minimo",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} secondi",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Illimitato",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Pulisci la cache delle immagini",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Reimposta",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Aggiungi server",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Rimuovi",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Modifica server",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Salva",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Prova connessione",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Indirizzo",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Password",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Nome utente",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Connessione a {address} fallita, controlla le impostazioni o il server",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Connesso a {address} con successo!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Server",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Invia la password come token + salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Invia password in chiaro (deprecato, assicurati che la tua connessione sia sicura!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Forza password in chiaro",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"navigationTabsHome": "ホーム",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "ライブラリ",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "検索",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "設定",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumName": "{count,plural, other{アルバム}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistName": "{count,plural, other{アーティスト}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterStarred": "星付きアルバム",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistName": "{count,plural, other{プレイリスト}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "人気曲",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, other{歌}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByFrequentlyPlayed": "よく聴くアルバム",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByRandom": "ランダムアルバム",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "最近再生した",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"searchInputPlaceholder": "検索",
|
||||
"@searchInputPlaceholder": {},
|
||||
"settingsAboutActionsProjectHomepage": "ホームページ",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsMusicName": "音楽",
|
||||
"@settingsMusicName": {},
|
||||
"settingsNetworkName": "ネット",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsResetName": "リセット",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersName": "サーバ",
|
||||
"@settingsServersName": {}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Stjernemerk",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Fjern stjernemerking",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Ingenting her …",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Hjem",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Bibliotek",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Søk",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Innstillinger",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Spill album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Vis album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Sorter album",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} other{Album}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Vis artist",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Sorter artister",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artist} other{Artister}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Etter sjanger",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Stjernemerket",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Spill av spilleliste",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Spilleliste} other{Spillelister}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Kø} other{Køer}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Toppspor",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Spor} other{Spor}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Nylig tillagt",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Etter artist",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Ofte spilt",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Etter navn",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Tilfeldig",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Nylig spilt",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Etter år",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Søk: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Søk",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Mer …",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Søkeresultater",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Lisenser",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Prosjekthjemmeside",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Om",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "versjon {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Musikk",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Ikke utfør sporinfodeling av avspillingshistorikk",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Sporinfodelings-avspillinghistorikk",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Sporinfodelingsavspillinger",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Nettverk",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Maksimal bitrate (mobil)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Maksimal bitrate (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Maksimal mellomlagringstid",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Minimal mellomlagringstid",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value} kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} sekunder",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Ubegrenset",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Tøm bildehurtiglager",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Tilbakestill",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Legg til tjener",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Slett",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Rediger tjener",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Lagre",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Test tilkobling",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adresse",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Passord",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Brukernavn",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Tilkobling til {address} mislyktes. Sjekk innstillingene eller tjeneren.",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Tilkobling til {address} OK.",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Tjenere",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Send passord som symbol + salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Send passord i klartekst (Foreldet. Forsikre deg om at tilkoblingen er sikker.)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Påtving klartekstspassord",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "ਤਾਰਾ",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "ਤਾਰਾ ਹਟਾਓ",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "ਇੱਥੇ ਕੁਝ ਨਹੀਂ ਹੈ…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "ਘਰ",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "ਲਾਇਬ੍ਰੇਰੀ",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "ਖੋਜ",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "ਸੈਟਿੰਗਾਂ",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "ਐਲਬਮ ਚਲਾਓ",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "ਐਲਬਮ ਦੇਖੋ",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "ਐਲਬਮਾਂ ਨੂੰ ਕਰਮਬੱਧ ਕਰੋ",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{ਐਲਬਮ} other{ਐਲਬਮਾਂ}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "ਕਲਾਕਾਰ ਦੇਖੋ",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "ਕਲਾਕਾਰਾਂ ਦੀ ਛਾਂਟੀ",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{ਕਲਾਕਾਰ} other{ਕਲਾਕਾਰਾਂ}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "ਸ਼ੈਲੀ ਦੁਆਰਾ",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "ਸਟਾਰ ਕੀਤੇ ਗਏ",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "ਪਲੇਲਿਸਟ ਚਲਾਓ",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{ਪਲੇਲਿਸਟ} other{ਪਲੇਲਿਸਟਸ}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{ਕਤਾਰ} other{ਕਤਾਰਾਂ}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "ਉੱਤਮ ਗਾਣੇ",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{ਗਾਣਾ} other{ਗਾਣੇ}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "ਤਾਜ਼ਾ ਸ਼ਾਮਿਲ",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "ਕਲਾਕਾਰ ਦੁਆਰਾ",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "ਅਕਸਰ ਚਲਾਏ ਗਏ",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "ਨਾਮ ਦੁਆਰਾ",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "ਅਟਕਲ-ਪੱਚੂ",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "ਹਾਲ ਹੀ ਵਿੱਚ ਚਲਾਏ ਗਏ",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "ਸਾਲ ਦੁਆਰਾ",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "ਖੋਜ: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "ਖੋਜੋ",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "ਹੋਰ…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "ਖੋਜ ਨਤੀਜੇ",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "ਲਾਇਸੰਸ",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "ਪ੍ਰੋਜੈਕਟ ਹੋਮਪੇਜ",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "ਬਾਰੇ",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "ਸੰਸਕਰਣ {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "ਸੰਗੀਤ",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "ਸੁਣੇ ਹੋਏ ਗਾਣੇ ਸਕ੍ਰੋਬਲ ਨਾ ਕਰੋ",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "ਸੁਣੇ ਹੋਏ ਗਾਣੇ ਸਕ੍ਰੋਬਲ ਕਰੋ",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "ਗਾਣੇ ਸਕ੍ਰੋਬਲ ਕਰੋ",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "ਨੈੱਟਵਰਕ",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "ਵੱਧ ਤੋਂ ਵੱਧ ਬਿੱਟਰੇਟ (ਮੋਬਾਈਲ)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "ਵੱਧ ਤੋਂ ਵੱਧ ਬਿੱਟਰੇਟ (ਵਾਈ-ਫਾਈ)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "ਵੱਧ ਤੋਂ ਵੱਧ ਬਫਰ ਸਮਾਂ",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "ਘੱਟੋ-ਘੱਟ ਬਫਰ ਸਮਾਂ",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} ਸਕਿੰਟ",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "ਅਸੀਮਿਤ",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "ਚਿੱਤਰ ਕੈਸ਼ ਸਾਫ਼ ਕਰੋ",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "ਰੀਸੈਟ ਕਰੋ",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "ਸਰਵਰ ਸ਼ਾਮਲ ਕਰੋ",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "ਮਿਟਾਓ",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "ਸਰਵਰ ਦਾ ਸੰਪਾਦਨ",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "ਸੇਵ ਕਰੋ",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "ਟੈਸਟ ਕਨੈਕਸ਼ਨ",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "ਪਤਾ",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "ਪਾਸਵਰਡ",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "ਯੂਜ਼ਰਨੇਮ",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "{address} ਨਾਲ ਕਨੈਕਸ਼ਨ ਅਸਫਲ, ਸੈਟਿੰਗਾਂ ਜਾਂ ਸਰਵਰ ਦੀ ਜਾਂਚ ਕਰੋ",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "{address} ਨਾਲ ਕਨੈਕਸ਼ਨ ਠੀਕ ਹੈ!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "ਸਰਵਰ",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "ਪਾਸਵਰਡ ਨੂੰ ਟੋਕਨ + ਸਾਲ੍ਟ ਵਜੋਂ ਭੇਜੋ",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "ਪਾਸਵਰਡ ਨੂੰ ਸਾਦੇ ਟੈਕਸਟ ਵਿੱਚ ਭੇਜੋ (ਪੁਰਾਣਾ, ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡਾ ਕਨੈਕਸ਼ਨ ਸੁਰੱਖਿਅਤ ਹੈ!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "ਸਾਦੇ ਪਾਸਵਰਡ ਨੂੰ ਜਬਰੀ ਕਰੋ",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Ulubione",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Usuń ulubione",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Pusto tu…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Strona główna",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Kolekcja",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Wyszukaj",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Ustawienia",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Otwarzaj album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Zobacz album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Sortowanie albumów",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Album} few{Albumy} many{Albumów} other{Albumów}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Zobacz wykonawcę",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Sortowanie wykonawców",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Wykonawca} few{Wykonawcy} many{Wykonawców} other{Wykonawców}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Wg gatunku",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Ulubione",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Odtwarzaj playlistę",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Playlista} few{Playlisty} many{Playlist} other{Playlist}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Kolejka} few{Kolejki} many{Kolejek} other{Kolejek}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Najpopularniejsze utwory",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Utwór} few{Utwory} many{Utworów} other{Utworów}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Ostatnio dodane",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Wg wykonawcy",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Często odtwarzane",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Wg nazwy",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Losowo",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Ostatnio odtwarzane",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Wg roku",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Szukaj: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Szukaj",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Więcej…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Wyniki wyszukiwania",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licencje",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Strona główna projektu",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Informacje o projekcie",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "wersja {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Muzyka",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Nie śledź swojej historii odtwarzania",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Śledź swoją histrorię odtwarzania",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Śledzenie odtworzeń",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Sieć",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Maksymalny bitrate (sieć komórkowa)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Maksymalny bitrate (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Maksymalny czas buforowania",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Minimalny czas buforowania",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} sekund",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Bez ograniczeń",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Wyczyść pamięć podręczną obrazów",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Przywracanie",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Dodaj serwer",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Usuń",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Edytuj serwer",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Zapisz",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Przetestuj połączenie",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adres serwera",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Hasło",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Nazwa użytkownika",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Błąd połączenia z {address}, sprawdź ustawienia lub serwer",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Połączeno poprawnie z {address}!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Serwery",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Wyślij hasło jako token i ciąg zaburzający",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Wyślij hasło jako tekst (przestarzałe, upewnij się, że Twoje połączenie jest zabezpieczone)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Wymuś hasło jako tekst (plaintext)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Favorito",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Remover favorito",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Não existe nada…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Início",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Biblioteca",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Procurar",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Definições",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Tocar Álbum",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Ver Álbum",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Ordenar Álbuns",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Álbum} other{Álbuns}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Ver Artista",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Ordenar Artistas",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Artista} other{Artistas}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Por Género",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Favoritos",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Tocar Playlist",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Lista} other{Listas}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Fila} other{Filas}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Top Músicas",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Música} other{Músicas}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Adicionado recentemente",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Por Artista",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Mais Tocado",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Por Nome",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Aleatório",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Ouviu recentemente",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Por Ano",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Procurar: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Procurar",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Mais…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Resultados da Pesquisa",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Licenças",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Página do Projeto",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Acerca",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "versão {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Música",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Não enviar histórico de reproduções por scrobble",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Enviar histórico de reproduções por scrobble",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Enviar reproduções por scrobble",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Rede",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Bitrate Máximo (móvel)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Bitrate Máximo (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Tempo de buffer máximo",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Tempo de buffer mínimo",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} segundos",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Ilimitado",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Limpar cache de Imagens",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Redefinir",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Adicionar Servidor",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Apagar",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Editar Servidor",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Guardar",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Testar Ligação",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Endereço",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Senha",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Nome de utilizador",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Ligação a {address} falhou, verifique definições ou servidor",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Ligação a {address} OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Servidores",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Enviar senha como token + sal",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Enviar senha em texto simples (antigo, certifique-se que a sua ligação é segura!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Forçar password em texto simples",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {},
|
||||
"actionsCancel": "Cancelar",
|
||||
"@actionsCancel": {},
|
||||
"actionsDelete": "Apagar",
|
||||
"@actionsDelete": {},
|
||||
"actionsDownload": "Descarregar",
|
||||
"@actionsDownload": {},
|
||||
"actionsDownloadCancel": "Cancelar descarga",
|
||||
"@actionsDownloadCancel": {},
|
||||
"actionsDownloadDelete": "Apagar descarga",
|
||||
"@actionsDownloadDelete": {},
|
||||
"resourcesFilterAlbum": "Álbum",
|
||||
"@resourcesFilterAlbum": {},
|
||||
"resourcesFilterArtist": "Artista",
|
||||
"@resourcesFilterArtist": {},
|
||||
"resourcesFilterYear": "Ano",
|
||||
"@resourcesFilterYear": {},
|
||||
"resourcesSortByAlbum": "Álbum",
|
||||
"@resourcesSortByAlbum": {},
|
||||
"settingsAboutActionsSupport": "Apoie o programador 💜",
|
||||
"@settingsAboutActionsSupport": {},
|
||||
"settingsNetworkOptionsOfflineMode": "Modo offline",
|
||||
"@settingsNetworkOptionsOfflineMode": {},
|
||||
"settingsNetworkOptionsOfflineModeOff": "Usar a internet para sincronizar música.",
|
||||
"@settingsNetworkOptionsOfflineModeOff": {},
|
||||
"settingsNetworkOptionsOfflineModeOn": "Não usar a internet para sincronizar ou tocar música.",
|
||||
"@settingsNetworkOptionsOfflineModeOn": {},
|
||||
"settingsNetworkOptionsStreamFormat": "Formato preferido de streaming",
|
||||
"@settingsNetworkOptionsStreamFormat": {},
|
||||
"resourcesSortByTitle": "Título",
|
||||
"@resourcesSortByTitle": {},
|
||||
"actionsOk": "OK",
|
||||
"@actionsOk": {},
|
||||
"controlsShuffle": "Aleatório",
|
||||
"@controlsShuffle": {}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Избранное",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Убрать из избранного",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Здесь ничего нет…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Главная",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Библиотека",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Поиск",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Настройки",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Воспроизвести альбом",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Посмотреть альбом",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Сортировка альбомов",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Альбом} few{Альбомы} many{Альбомов} other{Альбомов}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Посмотреть исполнителя",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Сортировать исполнителей",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Исполнитель} few{Исполнители} many{Исполнителей} other{Исполнителей}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "По жанру",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Избранные",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Воспроизвести плейлист",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Плейлист} few{Плейлисты} many{Плейлистов} other{Плейлистов}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Очередь} few{Очереди} many{Очередей} other{Очередей}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Лучшие треки",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Трек} few{Трека} many{Треков} other{Треков}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Недавно добавленные",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "По исполнителям",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Часто проигрываемые",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "По имени",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Случайно",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Недавно проигранные",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "По году",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Поиск: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Поиск",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Больше…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Результаты поиска",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Лицензии",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Сайт проекта",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "О Subtracks",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "версия {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Музыка",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Не отправлять историю воспроизведений",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Скробблинг истории воспроизведения",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Скробблинг",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Сеть",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Максимальный битрейт (мобильный интернет)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Максимальный битрейт (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Максимальное время буферизации",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Минимальное время буферизации",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value} кбит/с",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} секунд",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Без ограничений",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Очистить кэш изображения",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Сброс",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Добавить сервер",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Удалить",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Редактировать сервер",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Сохранить",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Проверить подключение",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Адрес",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Пароль",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Имя пользователя",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Не удалось подключиться к {address}, проверьте настройки или сервер",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Подключение к {address} установлено!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Серверы",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Отправить пароль в виде токена",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Отправить пароль в виде текста (устарело, убедитесь, что ваше соединение безопасно!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Принудительно использовать текстовой пароль",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {},
|
||||
"settingsAboutShareLogs": "Поделиться журналами",
|
||||
"@settingsAboutShareLogs": {},
|
||||
"settingsAboutChooseLog": "Выбрать файл журнала",
|
||||
"@settingsAboutChooseLog": {},
|
||||
"settingsNetworkOptionsStreamFormatServerDefault": "Использовать сервер по умолчанию",
|
||||
"@settingsNetworkOptionsStreamFormatServerDefault": {},
|
||||
"actionsDownload": "Скачать",
|
||||
"@actionsDownload": {},
|
||||
"actionsDownloadCancel": "Отменить загрузку",
|
||||
"@actionsDownloadCancel": {},
|
||||
"actionsCancel": "Отменить",
|
||||
"@actionsCancel": {},
|
||||
"resourcesSongCount": "{count,plural, =1{{count} трек} other{{count} треки}}",
|
||||
"@resourcesSongCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAlbum": "По альбомам",
|
||||
"@resourcesSortByAlbum": {},
|
||||
"resourcesSortByTitle": "По заголовку",
|
||||
"@resourcesSortByTitle": {},
|
||||
"resourcesSortByUpdated": "По недавно обновленному",
|
||||
"@resourcesSortByUpdated": {},
|
||||
"resourcesSortByAlbumCount": "По количеству альбомов",
|
||||
"@resourcesSortByAlbumCount": {},
|
||||
"settingsNetworkOptionsOfflineMode": "Автономный режим",
|
||||
"@settingsNetworkOptionsOfflineMode": {},
|
||||
"settingsNetworkOptionsOfflineModeOff": "Использовать интернет для синхронизации музыки.",
|
||||
"@settingsNetworkOptionsOfflineModeOff": {},
|
||||
"settingsServersFieldsName": "Имя",
|
||||
"@settingsServersFieldsName": {},
|
||||
"actionsDelete": "Удалить",
|
||||
"@actionsDelete": {},
|
||||
"actionsDownloadDelete": "Удалить загруженное",
|
||||
"@actionsDownloadDelete": {},
|
||||
"actionsOk": "ОК",
|
||||
"@actionsOk": {},
|
||||
"controlsShuffle": "Перемешать",
|
||||
"@controlsShuffle": {},
|
||||
"resourcesFilterArtist": "По исполнителю",
|
||||
"@resourcesFilterArtist": {},
|
||||
"resourcesFilterAlbum": "По альбомам",
|
||||
"@resourcesFilterAlbum": {},
|
||||
"resourcesFilterYear": "По годам",
|
||||
"@resourcesFilterYear": {},
|
||||
"resourcesFilterOwner": "По владельцу",
|
||||
"@resourcesFilterOwner": {},
|
||||
"resourcesSongListDeleteAllContent": "Это удалит все загруженные файлы песен.",
|
||||
"@resourcesSongListDeleteAllContent": {},
|
||||
"settingsNetworkOptionsStreamFormat": "Предпочтительный формат потока",
|
||||
"@settingsNetworkOptionsStreamFormat": {},
|
||||
"resourcesSongListDeleteAllTitle": "Удалить загрузки?",
|
||||
"@resourcesSongListDeleteAllTitle": {},
|
||||
"settingsNetworkOptionsOfflineModeOn": "Не использовать интернет для синхронизации или воспроизведения музыки.",
|
||||
"@settingsNetworkOptionsOfflineModeOn": {},
|
||||
"settingsAboutActionsSupport": "Поддержать разработчика",
|
||||
"@settingsAboutActionsSupport": {},
|
||||
"resourcesArtistCount": "{count,plural, =1{{count} исполнитель} other{{count} исполнители}}",
|
||||
"@resourcesArtistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesPlaylistCount": "{count,plural, =1{{count} плейлист} other{{count} плейлисты}}",
|
||||
"@resourcesPlaylistCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesAlbumCount": "{count,plural, =1{{count} альбом} other{{count} альбомы}}",
|
||||
"@resourcesAlbumCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Yıldızla",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Yıldızı Kaldır",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Burada bir şey yok …",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Giriş",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Kütüphane",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Arama",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Ayarlar",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Albümü Çal",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Albüme Git",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Albümleri Sırala",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, =1{Albüm} other{Albümler}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Sanatçıya Git",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Sanatçıları Sırala",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, =1{Sanatçı} other{Sanatçılar}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Türe göre",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Yıldızlı",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Listeyi Çal",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, =1{Çalma Listesi} other{Çalma Listeleri}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, =1{Sıra} other{Sıralar}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "En İyi Şarkılar",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, =1{Şarkı} other{Şarkılar}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Son Eklenenler",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Sanatçıya Göre",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Sık Dinlenenler",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "İsme göre",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Rastgele",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Yeni Dinleneler",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Yıla göre",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Aranan: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Ara",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Tüm Sonuçlar …",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Arama Sonuçları",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Lisanslar",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Proje Ana Sayfası",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Hakkında",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "sürüm {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Müzik",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Müzik geçmişini profilime ekleme",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Müzik geçmişini profilime ekle",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Çalan müziği profilime ekle",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Ağ",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Azami bithızı (SIM interneti)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Azami bithızı (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Azami arabellek süresi",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Asgari arabellek süresi",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kb/sn",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} saniye",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Sınırsız",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Görüntü Önbelleğini Temizle",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Sıfırlama",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Sunucu Ekle",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Kaldır",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Sunucu Ayarı",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Kaydet",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Bağlantıyı Sına",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Adres",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Şifre",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Kullanıcı Adı",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "{address} ile bağlantı başarısız. Sunucu ayarlarınızın doğrulığundan emin olun.",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "{address} ile bağlantı başarılı!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Sunucular",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Şifreyi dizgecik ve tuz karışımı olarak gönder",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Şifreyi düzmetin olarak gönderir (eski yöntem, bağlantının güvenli (HTTPS) olduğu sunucularda kullanın!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Düzyazı şifre kullanmayı zorla",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
{
|
||||
"actionsStar": "Đánh dấu sao",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "Bỏ dấu sao",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "Không có gì ở đây…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "Trang chủ",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "Thư Viện",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "Tìm kiếm",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "Thiết Lập",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "Phát Album",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "Xem Album",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "Sắp xếp Album",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, other{Album}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "Xem Nghệ sĩ",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "Sắp xếp nghệ sĩ",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, other{Nghệ sĩ}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "Theo thể loại",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "Có gắn dấu sao",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "Phát Danh sách phát",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, other{Danh sách phát}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, other{Hàng chờ}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "Bài hát hàng đầu",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, other{Bài hát}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "Thêm vào gần đây",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "Theo nghệ sĩ",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "Thường xuyên chơi",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "Theo tên",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "Ngẫu Nhiên",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "Đã phát gần đây",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "Theo năm",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "Tìm kiếm: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "Tìm kiếm",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "Nhiều hơn…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "Kết quả tìm kiếm",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "Giấy phép",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "Trang chủ Dự án",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "Giới thiệu",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "phiên bản {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "Âm nhạc",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "Đừng scrobble lịch sử chơi",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Lịch sử chơi Scrobble",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Scrobble lượt chơi",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "Mạng",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "Tối đa bitrate (mobile)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "Tối đa bitrate (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "Thời gian đệm tối đa",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "Thời gian đệm tối thiểu",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} giây",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "Không giới hạn",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "Xóa bộ nhớ đệm hình ảnh",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "Cài đặt lại",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "Thêm máy chủ",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "Xóa",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "Chỉnh sửa máy chủ",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "Lưu",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "Kiểm tra kết nối",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "Địa chỉ",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "Mật Khẩu",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "Tên đăng nhập",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "Kết nối với {address} không thành công, hãy kiểm tra cài đặt hoặc máy chủ",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "Kết nối với {address} OK!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "Máy chủ",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "Gửi mật khẩu dưới dạng token + salt",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "Gửi mật khẩu ở dạng văn bản rõ ràng (kế thừa, đảm bảo kết nối của bạn an toàn!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "Buộc mật khẩu văn bản thuần túy",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
{
|
||||
"actionsStar": "收藏",
|
||||
"@actionsStar": {},
|
||||
"actionsUnstar": "移除收藏",
|
||||
"@actionsUnstar": {},
|
||||
"messagesNothingHere": "什么都没有…",
|
||||
"@messagesNothingHere": {},
|
||||
"navigationTabsHome": "首页",
|
||||
"@navigationTabsHome": {},
|
||||
"navigationTabsLibrary": "所有",
|
||||
"@navigationTabsLibrary": {},
|
||||
"navigationTabsSearch": "搜索",
|
||||
"@navigationTabsSearch": {},
|
||||
"navigationTabsSettings": "设置",
|
||||
"@navigationTabsSettings": {},
|
||||
"resourcesAlbumActionsPlay": "播放专辑",
|
||||
"@resourcesAlbumActionsPlay": {},
|
||||
"resourcesAlbumActionsView": "查看专辑",
|
||||
"@resourcesAlbumActionsView": {},
|
||||
"resourcesAlbumListsSort": "专辑排序",
|
||||
"@resourcesAlbumListsSort": {},
|
||||
"resourcesAlbumName": "{count,plural, other{专辑}}",
|
||||
"@resourcesAlbumName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesArtistActionsView": "查看歌手",
|
||||
"@resourcesArtistActionsView": {},
|
||||
"resourcesArtistListsSort": "歌手排序",
|
||||
"@resourcesArtistListsSort": {},
|
||||
"resourcesArtistName": "{count,plural, other{歌手}}",
|
||||
"@resourcesArtistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesFilterGenre": "根据类型",
|
||||
"@resourcesFilterGenre": {},
|
||||
"resourcesFilterStarred": "已收藏",
|
||||
"@resourcesFilterStarred": {},
|
||||
"resourcesPlaylistActionsPlay": "全部播放",
|
||||
"@resourcesPlaylistActionsPlay": {},
|
||||
"resourcesPlaylistName": "{count,plural, other{播放列表}}",
|
||||
"@resourcesPlaylistName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesQueueName": "{count,plural, other{队列}}",
|
||||
"@resourcesQueueName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSongListsArtistTopSongs": "热门歌曲",
|
||||
"@resourcesSongListsArtistTopSongs": {},
|
||||
"resourcesSongName": "{count,plural, other{歌曲}}",
|
||||
"@resourcesSongName": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesSortByAdded": "最近添加",
|
||||
"@resourcesSortByAdded": {},
|
||||
"resourcesSortByArtist": "根据歌手",
|
||||
"@resourcesSortByArtist": {},
|
||||
"resourcesSortByFrequentlyPlayed": "播放最多",
|
||||
"@resourcesSortByFrequentlyPlayed": {},
|
||||
"resourcesSortByName": "根据名称",
|
||||
"@resourcesSortByName": {},
|
||||
"resourcesSortByRandom": "随机",
|
||||
"@resourcesSortByRandom": {},
|
||||
"resourcesSortByRecentlyPlayed": "最近播放",
|
||||
"@resourcesSortByRecentlyPlayed": {},
|
||||
"resourcesSortByYear": "根据年份",
|
||||
"@resourcesSortByYear": {},
|
||||
"searchHeaderTitle": "搜索: {query}",
|
||||
"@searchHeaderTitle": {
|
||||
"placeholders": {
|
||||
"query": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchInputPlaceholder": "搜索",
|
||||
"@searchInputPlaceholder": {},
|
||||
"searchMoreResults": "更多…",
|
||||
"@searchMoreResults": {},
|
||||
"searchNowPlayingContext": "搜索结果",
|
||||
"@searchNowPlayingContext": {},
|
||||
"settingsAboutActionsLicenses": "许可",
|
||||
"@settingsAboutActionsLicenses": {},
|
||||
"settingsAboutActionsProjectHomepage": "项目地址",
|
||||
"@settingsAboutActionsProjectHomepage": {},
|
||||
"settingsAboutName": "关于",
|
||||
"@settingsAboutName": {},
|
||||
"settingsAboutVersion": "版本 {version}",
|
||||
"@settingsAboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsMusicName": "音乐",
|
||||
"@settingsMusicName": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOff": "不记录scrobble历史",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOff": {},
|
||||
"settingsMusicOptionsScrobbleDescriptionOn": "Scrobble播放历史",
|
||||
"@settingsMusicOptionsScrobbleDescriptionOn": {},
|
||||
"settingsMusicOptionsScrobbleTitle": "Scrobble模式",
|
||||
"@settingsMusicOptionsScrobbleTitle": {},
|
||||
"settingsNetworkName": "网络",
|
||||
"@settingsNetworkName": {},
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle": "最大比特率 (3G/4G/5G)",
|
||||
"@settingsNetworkOptionsMaxBitrateMobileTitle": {},
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle": "最大比特率 (Wi-Fi)",
|
||||
"@settingsNetworkOptionsMaxBitrateWifiTitle": {},
|
||||
"settingsNetworkOptionsMaxBufferTitle": "最大缓冲时间",
|
||||
"@settingsNetworkOptionsMaxBufferTitle": {},
|
||||
"settingsNetworkOptionsMinBufferTitle": "最小缓冲时间",
|
||||
"@settingsNetworkOptionsMinBufferTitle": {},
|
||||
"settingsNetworkValuesKbps": "{value}kbps",
|
||||
"@settingsNetworkValuesKbps": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesSeconds": "{value} 秒",
|
||||
"@settingsNetworkValuesSeconds": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsNetworkValuesUnlimitedKbps": "不限制",
|
||||
"@settingsNetworkValuesUnlimitedKbps": {},
|
||||
"settingsResetActionsClearImageCache": "清除图片缓存",
|
||||
"@settingsResetActionsClearImageCache": {},
|
||||
"settingsResetName": "重置",
|
||||
"@settingsResetName": {},
|
||||
"settingsServersActionsAdd": "添加服务器",
|
||||
"@settingsServersActionsAdd": {},
|
||||
"settingsServersActionsDelete": "删除",
|
||||
"@settingsServersActionsDelete": {},
|
||||
"settingsServersActionsEdit": "编辑服务器",
|
||||
"@settingsServersActionsEdit": {},
|
||||
"settingsServersActionsSave": "保存",
|
||||
"@settingsServersActionsSave": {},
|
||||
"settingsServersActionsTestConnection": "测试连接",
|
||||
"@settingsServersActionsTestConnection": {},
|
||||
"settingsServersFieldsAddress": "地址",
|
||||
"@settingsServersFieldsAddress": {},
|
||||
"settingsServersFieldsPassword": "密码",
|
||||
"@settingsServersFieldsPassword": {},
|
||||
"settingsServersFieldsUsername": "用户名",
|
||||
"@settingsServersFieldsUsername": {},
|
||||
"settingsServersMessagesConnectionFailed": "连接到 {address} 失败,检查设置或服务器",
|
||||
"@settingsServersMessagesConnectionFailed": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersMessagesConnectionOk": "连接到 {address} 正常!",
|
||||
"@settingsServersMessagesConnectionOk": {
|
||||
"placeholders": {
|
||||
"address": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsServersName": "服务器",
|
||||
"@settingsServersName": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff": "密码以 token + salt 加密发送",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOff": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn": "密码以明文发送(不推荐,注意链接安全!)",
|
||||
"@settingsServersOptionsForcePlaintextPasswordDescriptionOn": {},
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle": "强制使用明文密码",
|
||||
"@settingsServersOptionsForcePlaintextPasswordTitle": {},
|
||||
"actionsDownload": "下载",
|
||||
"@actionsDownload": {},
|
||||
"actionsDownloadCancel": "取消下载",
|
||||
"@actionsDownloadCancel": {},
|
||||
"actionsDownloadDelete": "删除已下载",
|
||||
"@actionsDownloadDelete": {},
|
||||
"actionsOk": "确定",
|
||||
"@actionsOk": {},
|
||||
"resourcesFilterArtist": "歌手",
|
||||
"@resourcesFilterArtist": {},
|
||||
"resourcesFilterOwner": "所有者",
|
||||
"@resourcesFilterOwner": {},
|
||||
"resourcesSongListDeleteAllTitle": "删除下载?",
|
||||
"@resourcesSongListDeleteAllTitle": {},
|
||||
"resourcesSortByAlbum": "专辑",
|
||||
"@resourcesSortByAlbum": {},
|
||||
"resourcesSortByAlbumCount": "专辑数量",
|
||||
"@resourcesSortByAlbumCount": {},
|
||||
"resourcesSortByUpdated": "最近添加",
|
||||
"@resourcesSortByUpdated": {},
|
||||
"settingsAboutActionsSupport": "支持开发者",
|
||||
"@settingsAboutActionsSupport": {},
|
||||
"resourcesFilterAlbum": "专辑",
|
||||
"@resourcesFilterAlbum": {},
|
||||
"resourcesSortByTitle": "标题",
|
||||
"@resourcesSortByTitle": {},
|
||||
"actionsCancel": "取消",
|
||||
"@actionsCancel": {},
|
||||
"actionsDelete": "删除",
|
||||
"@actionsDelete": {},
|
||||
"resourcesFilterYear": "年份",
|
||||
"@resourcesFilterYear": {},
|
||||
"resourcesSongListDeleteAllContent": "该操作会删除所有已下载的歌曲文件。",
|
||||
"@resourcesSongListDeleteAllContent": {}
|
||||
}
|
||||
209
lib/log.dart
209
lib/log.dart
@@ -1,209 +0,0 @@
|
||||
// import 'dart:convert';
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class AnsiColor {
|
||||
/// ANSI Control Sequence Introducer, signals the terminal for new settings.
|
||||
static const ansiEsc = '\x1B[';
|
||||
|
||||
/// Reset all colors and options for current SGRs to terminal defaults.
|
||||
static const ansiDefault = '${ansiEsc}0m';
|
||||
|
||||
final int? fg;
|
||||
final int? bg;
|
||||
final bool color;
|
||||
|
||||
AnsiColor.none()
|
||||
: fg = null,
|
||||
bg = null,
|
||||
color = false;
|
||||
|
||||
AnsiColor.fg(this.fg)
|
||||
: bg = null,
|
||||
color = true;
|
||||
|
||||
AnsiColor.bg(this.bg)
|
||||
: fg = null,
|
||||
color = true;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (fg != null) {
|
||||
return '${ansiEsc}38;5;${fg}m';
|
||||
} else if (bg != null) {
|
||||
return '${ansiEsc}48;5;${bg}m';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
String call(String msg) {
|
||||
if (color) {
|
||||
// ignore: unnecessary_brace_in_string_interps
|
||||
return '${this}$msg$ansiDefault';
|
||||
} else {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
AnsiColor toFg() => AnsiColor.fg(bg);
|
||||
|
||||
AnsiColor toBg() => AnsiColor.bg(fg);
|
||||
|
||||
/// Defaults the terminal's foreground color without altering the background.
|
||||
String get resetForeground => color ? '${ansiEsc}39m' : '';
|
||||
|
||||
/// Defaults the terminal's background color without altering the foreground.
|
||||
String get resetBackground => color ? '${ansiEsc}49m' : '';
|
||||
|
||||
static int grey(double level) => 232 + (level.clamp(0.0, 1.0) * 23).round();
|
||||
}
|
||||
|
||||
final levelColors = {
|
||||
Level.FINEST: AnsiColor.fg(AnsiColor.grey(0.5)),
|
||||
Level.FINER: AnsiColor.fg(AnsiColor.grey(0.5)),
|
||||
Level.FINE: AnsiColor.fg(AnsiColor.grey(0.5)),
|
||||
Level.CONFIG: AnsiColor.fg(81),
|
||||
Level.INFO: AnsiColor.fg(12),
|
||||
Level.WARNING: AnsiColor.fg(208),
|
||||
Level.SEVERE: AnsiColor.fg(196),
|
||||
Level.SHOUT: AnsiColor.fg(199),
|
||||
};
|
||||
|
||||
class LogData {
|
||||
final String? message;
|
||||
final Object? data;
|
||||
|
||||
const LogData(this.message, this.data);
|
||||
}
|
||||
|
||||
String _format(
|
||||
LogRecord event, {
|
||||
bool color = false,
|
||||
bool time = true,
|
||||
bool level = true,
|
||||
bool redact = true,
|
||||
}) {
|
||||
var message = '';
|
||||
if (time) message += '${event.time.toIso8601String()} ';
|
||||
if (level) message += '${event.level.name} ';
|
||||
|
||||
final object = event.object;
|
||||
if (object is LogData) {
|
||||
message += '${object.message}';
|
||||
message += '\n${object.data}';
|
||||
} else if (object != null) {
|
||||
message += 'Object';
|
||||
message += '\n$object';
|
||||
} else {
|
||||
message += event.message;
|
||||
}
|
||||
|
||||
if (event.error != null) {
|
||||
message += '\n${event.error}';
|
||||
}
|
||||
|
||||
if (redact) {
|
||||
message = _redactUrl(message);
|
||||
}
|
||||
|
||||
if (event.stackTrace != null) {
|
||||
message += '\n${event.stackTrace}';
|
||||
}
|
||||
|
||||
return color
|
||||
? message.split('\n').map((e) => levelColors[event.level]!(e)).join('\n')
|
||||
: message;
|
||||
}
|
||||
|
||||
String _redactUrl(String message) {
|
||||
if (!_queryReplace('u').hasMatch(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
message = _redactParam(message, 'u');
|
||||
message = _redactParam(message, 'p');
|
||||
message = _redactParam(message, 's');
|
||||
message = _redactParam(message, 't');
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
RegExp _queryReplace(String key) => RegExp('$key=([^&|\\n|\\t\\s]+)');
|
||||
|
||||
String _redactParam(String url, String key) =>
|
||||
url.replaceAll(_queryReplace(key), '$key=REDACTED');
|
||||
|
||||
Future<Directory> logDirectory() async {
|
||||
return Directory(
|
||||
p.join((await getApplicationDocumentsDirectory()).path, 'logs'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<File>> logFiles() async {
|
||||
final dir = await logDirectory();
|
||||
return dir.listSync().whereType<File>().toList()
|
||||
..sort(
|
||||
(a, b) => b.statSync().modified.compareTo(a.statSync().modified),
|
||||
);
|
||||
}
|
||||
|
||||
File _currentLogFile(String logDir) {
|
||||
final now = DateTime.now();
|
||||
return File(p.join(logDir, '${now.year}-${now.month}-${now.day}.txt'));
|
||||
}
|
||||
|
||||
Future<void> _printFile(String event, String logDir) async {
|
||||
final file = _currentLogFile(logDir);
|
||||
|
||||
if (!event.endsWith('\n')) {
|
||||
event += '\n';
|
||||
}
|
||||
|
||||
await file.writeAsString(event, mode: FileMode.writeOnlyAppend, flush: true);
|
||||
}
|
||||
|
||||
void _printDebug(LogRecord event) {
|
||||
// ignore: avoid_print
|
||||
print(_format(event, color: true, time: false, level: false, redact: false));
|
||||
}
|
||||
|
||||
Future<void> _printRelease(LogRecord event, String logDir) async {
|
||||
await _printFile(
|
||||
_format(event, color: false, time: true, level: true, redact: true),
|
||||
logDir,
|
||||
);
|
||||
}
|
||||
|
||||
final log = Logger('default');
|
||||
|
||||
Future<void> initLogging() async {
|
||||
final dir = (await logDirectory())..create();
|
||||
|
||||
final file = _currentLogFile(dir.path);
|
||||
if (!(await file.exists())) {
|
||||
await file.create();
|
||||
}
|
||||
|
||||
final files = await logFiles();
|
||||
if (files.length > 7) {
|
||||
for (var file in files.slice(7)) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Logger.root.level = kDebugMode ? Level.ALL : Level.INFO;
|
||||
Logger.root.onRecord.asyncMap((event) async {
|
||||
if (kDebugMode) {
|
||||
_printDebug(event);
|
||||
} else {
|
||||
await _printRelease(event, dir.path);
|
||||
}
|
||||
}).listen((_) {}, cancelOnError: false);
|
||||
}
|
||||
@@ -1,26 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:stack_trace/stack_trace.dart' as stack_trace;
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
import 'app/app.dart';
|
||||
import 'log.dart';
|
||||
|
||||
void main() async {
|
||||
// TOOD: probably remove before live
|
||||
// https://stackoverflow.com/a/73770713
|
||||
FlutterError.demangleStackTrace = (StackTrace stack) {
|
||||
if (stack is stack_trace.Trace) return stack.vmTrace;
|
||||
if (stack is stack_trace.Chain) return stack.toTrace().vmTrace;
|
||||
return stack;
|
||||
};
|
||||
|
||||
// keep some threads warm for background (palette generation) tasks
|
||||
await Executor().warmUp();
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await initLogging();
|
||||
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
void main() {
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: Text('Hello World!'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'music.freezed.dart';
|
||||
|
||||
abstract class SourceIdentifiable {
|
||||
int get sourceId;
|
||||
String get id;
|
||||
}
|
||||
|
||||
abstract class SongList extends SourceIdentifiable {
|
||||
String get name;
|
||||
int get songCount;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SourceId with _$SourceId implements SourceIdentifiable {
|
||||
const factory SourceId({
|
||||
required int sourceId,
|
||||
required String id,
|
||||
}) = _SourceId;
|
||||
|
||||
factory SourceId.from(SourceIdentifiable item) {
|
||||
return SourceId(sourceId: item.sourceId, id: item.id);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SourceIdSet with _$SourceIdSet {
|
||||
const factory SourceIdSet({
|
||||
required int sourceId,
|
||||
required ISet<String> ids,
|
||||
}) = _SourceIdSet;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Artist with _$Artist {
|
||||
const factory Artist({
|
||||
required int sourceId,
|
||||
required String id,
|
||||
required String name,
|
||||
required int albumCount,
|
||||
DateTime? starred,
|
||||
// @Default(IListConst([])) IList<Album> albums,
|
||||
}) = _Artist;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Album with _$Album implements SongList {
|
||||
const factory Album({
|
||||
required int sourceId,
|
||||
required String id,
|
||||
required String name,
|
||||
String? artistId,
|
||||
String? albumArtist,
|
||||
required DateTime created,
|
||||
String? coverArt,
|
||||
int? year,
|
||||
DateTime? starred,
|
||||
// DateTime? synced,
|
||||
String? genre,
|
||||
required int songCount,
|
||||
@Default(false) bool isDeleted,
|
||||
// @Default(IListConst([])) IList<Song> songs,
|
||||
int? frequentRank,
|
||||
int? recentRank,
|
||||
}) = _Album;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Playlist with _$Playlist implements SongList {
|
||||
const factory Playlist({
|
||||
required int sourceId,
|
||||
required String id,
|
||||
required String name,
|
||||
String? comment,
|
||||
String? coverArt,
|
||||
required int songCount,
|
||||
required DateTime created,
|
||||
// DateTime? synced,
|
||||
// @Default(IListConst([])) IList<Song> songs,
|
||||
}) = _Playlist;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Song with _$Song implements SourceIdentifiable {
|
||||
const factory Song({
|
||||
required int sourceId,
|
||||
required String id,
|
||||
String? albumId,
|
||||
String? artistId,
|
||||
required String title,
|
||||
String? artist,
|
||||
String? album,
|
||||
Duration? duration,
|
||||
int? track,
|
||||
int? disc,
|
||||
DateTime? starred,
|
||||
String? genre,
|
||||
String? downloadTaskId,
|
||||
String? downloadFilePath,
|
||||
@Default(false) bool isDeleted,
|
||||
}) = _Song;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SearchResults with _$SearchResults {
|
||||
const factory SearchResults({
|
||||
String? query,
|
||||
@Default(IListConst([])) IList<Song> songs,
|
||||
@Default(IListConst([])) IList<Album> albums,
|
||||
@Default(IListConst([])) IList<Artist> artists,
|
||||
}) = _SearchResults;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,97 +0,0 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'query.freezed.dart';
|
||||
part 'query.g.dart';
|
||||
|
||||
enum SortDirection {
|
||||
asc('ASC'),
|
||||
desc('DESC');
|
||||
|
||||
const SortDirection(this.value);
|
||||
final String value;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Pagination with _$Pagination {
|
||||
const factory Pagination({
|
||||
required int limit,
|
||||
@Default(0) int offset,
|
||||
}) = _Pagination;
|
||||
|
||||
factory Pagination.fromJson(Map<String, dynamic> json) =>
|
||||
_$PaginationFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SortBy with _$SortBy {
|
||||
const factory SortBy({
|
||||
required String column,
|
||||
@Default(SortDirection.asc) SortDirection dir,
|
||||
}) = _SortBy;
|
||||
|
||||
factory SortBy.fromJson(Map<String, dynamic> json) => _$SortByFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FilterWith with _$FilterWith {
|
||||
const factory FilterWith.equals({
|
||||
required String column,
|
||||
required String value,
|
||||
@Default(false) bool invert,
|
||||
}) = _FilterWithEquals;
|
||||
|
||||
const factory FilterWith.greaterThan({
|
||||
required String column,
|
||||
required String value,
|
||||
@Default(false) bool orEquals,
|
||||
}) = _FilterWithGreaterThan;
|
||||
|
||||
const factory FilterWith.isNull({
|
||||
required String column,
|
||||
@Default(false) bool invert,
|
||||
}) = _FilterWithIsNull;
|
||||
|
||||
const factory FilterWith.betweenInt({
|
||||
required String column,
|
||||
required int from,
|
||||
required int to,
|
||||
}) = _FilterWithBetweenInt;
|
||||
|
||||
const factory FilterWith.isIn({
|
||||
required String column,
|
||||
@Default(false) bool invert,
|
||||
@Default(IListConst([])) IList<String> values,
|
||||
}) = _FilterWithIsIn;
|
||||
|
||||
factory FilterWith.fromJson(Map<String, dynamic> json) =>
|
||||
_$FilterWithFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ListQuery with _$ListQuery {
|
||||
const factory ListQuery({
|
||||
@Default(Pagination(limit: -1, offset: 0)) Pagination page,
|
||||
SortBy? sort,
|
||||
@Default(IListConst([])) IList<FilterWith> filters,
|
||||
}) = _ListQuery;
|
||||
|
||||
factory ListQuery.fromJson(Map<String, dynamic> json) =>
|
||||
_$ListQueryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ListQueryOptions with _$ListQueryOptions {
|
||||
const factory ListQueryOptions({
|
||||
required IList<String> sortColumns,
|
||||
required IList<String> filterColumns,
|
||||
}) = _ListQueryOptions;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LibraryListQuery with _$LibraryListQuery {
|
||||
const factory LibraryListQuery({
|
||||
required ListQueryOptions options,
|
||||
required ListQuery query,
|
||||
}) = _LibraryListQuery;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,143 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'query.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_Pagination _$$_PaginationFromJson(Map<String, dynamic> json) =>
|
||||
_$_Pagination(
|
||||
limit: json['limit'] as int,
|
||||
offset: json['offset'] as int? ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_PaginationToJson(_$_Pagination instance) =>
|
||||
<String, dynamic>{
|
||||
'limit': instance.limit,
|
||||
'offset': instance.offset,
|
||||
};
|
||||
|
||||
_$_SortBy _$$_SortByFromJson(Map<String, dynamic> json) => _$_SortBy(
|
||||
column: json['column'] as String,
|
||||
dir: $enumDecodeNullable(_$SortDirectionEnumMap, json['dir']) ??
|
||||
SortDirection.asc,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_SortByToJson(_$_SortBy instance) => <String, dynamic>{
|
||||
'column': instance.column,
|
||||
'dir': _$SortDirectionEnumMap[instance.dir]!,
|
||||
};
|
||||
|
||||
const _$SortDirectionEnumMap = {
|
||||
SortDirection.asc: 'asc',
|
||||
SortDirection.desc: 'desc',
|
||||
};
|
||||
|
||||
_$_FilterWithEquals _$$_FilterWithEqualsFromJson(Map<String, dynamic> json) =>
|
||||
_$_FilterWithEquals(
|
||||
column: json['column'] as String,
|
||||
value: json['value'] as String,
|
||||
invert: json['invert'] as bool? ?? false,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FilterWithEqualsToJson(_$_FilterWithEquals instance) =>
|
||||
<String, dynamic>{
|
||||
'column': instance.column,
|
||||
'value': instance.value,
|
||||
'invert': instance.invert,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_$_FilterWithGreaterThan _$$_FilterWithGreaterThanFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$_FilterWithGreaterThan(
|
||||
column: json['column'] as String,
|
||||
value: json['value'] as String,
|
||||
orEquals: json['orEquals'] as bool? ?? false,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FilterWithGreaterThanToJson(
|
||||
_$_FilterWithGreaterThan instance) =>
|
||||
<String, dynamic>{
|
||||
'column': instance.column,
|
||||
'value': instance.value,
|
||||
'orEquals': instance.orEquals,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_$_FilterWithIsNull _$$_FilterWithIsNullFromJson(Map<String, dynamic> json) =>
|
||||
_$_FilterWithIsNull(
|
||||
column: json['column'] as String,
|
||||
invert: json['invert'] as bool? ?? false,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FilterWithIsNullToJson(_$_FilterWithIsNull instance) =>
|
||||
<String, dynamic>{
|
||||
'column': instance.column,
|
||||
'invert': instance.invert,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_$_FilterWithBetweenInt _$$_FilterWithBetweenIntFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$_FilterWithBetweenInt(
|
||||
column: json['column'] as String,
|
||||
from: json['from'] as int,
|
||||
to: json['to'] as int,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FilterWithBetweenIntToJson(
|
||||
_$_FilterWithBetweenInt instance) =>
|
||||
<String, dynamic>{
|
||||
'column': instance.column,
|
||||
'from': instance.from,
|
||||
'to': instance.to,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_$_FilterWithIsIn _$$_FilterWithIsInFromJson(Map<String, dynamic> json) =>
|
||||
_$_FilterWithIsIn(
|
||||
column: json['column'] as String,
|
||||
invert: json['invert'] as bool? ?? false,
|
||||
values: json['values'] == null
|
||||
? const IListConst([])
|
||||
: IList<String>.fromJson(json['values'], (value) => value as String),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_FilterWithIsInToJson(_$_FilterWithIsIn instance) =>
|
||||
<String, dynamic>{
|
||||
'column': instance.column,
|
||||
'invert': instance.invert,
|
||||
'values': instance.values.toJson(
|
||||
(value) => value,
|
||||
),
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_$_ListQuery _$$_ListQueryFromJson(Map<String, dynamic> json) => _$_ListQuery(
|
||||
page: json['page'] == null
|
||||
? const Pagination(limit: -1, offset: 0)
|
||||
: Pagination.fromJson(json['page'] as Map<String, dynamic>),
|
||||
sort: json['sort'] == null
|
||||
? null
|
||||
: SortBy.fromJson(json['sort'] as Map<String, dynamic>),
|
||||
filters: json['filters'] == null
|
||||
? const IListConst([])
|
||||
: IList<FilterWith>.fromJson(json['filters'],
|
||||
(value) => FilterWith.fromJson(value as Map<String, dynamic>)),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_ListQueryToJson(_$_ListQuery instance) =>
|
||||
<String, dynamic>{
|
||||
'page': instance.page,
|
||||
'sort': instance.sort,
|
||||
'filters': instance.filters.toJson(
|
||||
(value) => value,
|
||||
),
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
|
||||
part 'settings.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class Settings with _$Settings {
|
||||
const factory Settings({
|
||||
@Default(IListConst([])) IList<SourceSettings> sources,
|
||||
SourceSettings? activeSource,
|
||||
@Default(AppSettings()) AppSettings app,
|
||||
}) = _Settings;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppSettings with _$AppSettings {
|
||||
const AppSettings._();
|
||||
|
||||
const factory AppSettings({
|
||||
@Default(0) int maxBitrateWifi,
|
||||
@Default(192) int maxBitrateMobile,
|
||||
@Default('mp3') String? streamFormat,
|
||||
}) = _AppSettings;
|
||||
|
||||
AppSettingsCompanion toCompanion() {
|
||||
return AppSettingsCompanion.insert(
|
||||
id: const Value(1),
|
||||
maxBitrateWifi: maxBitrateWifi,
|
||||
maxBitrateMobile: maxBitrateMobile,
|
||||
streamFormat: Value(streamFormat),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ParentChild<T> {
|
||||
final T parent;
|
||||
final T child;
|
||||
|
||||
ParentChild(this.parent, this.child);
|
||||
}
|
||||
|
||||
abstract class SourceSettings {
|
||||
const SourceSettings();
|
||||
|
||||
int get id;
|
||||
String get name;
|
||||
Uri get address;
|
||||
bool? get isActive;
|
||||
DateTime get createdAt;
|
||||
}
|
||||
|
||||
enum SubsonicFeature {
|
||||
emptyQuerySearch('emptyQuerySearch');
|
||||
|
||||
const SubsonicFeature(this.value);
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SubsonicSettings with _$SubsonicSettings implements SourceSettings {
|
||||
const SubsonicSettings._();
|
||||
|
||||
const factory SubsonicSettings({
|
||||
required int id,
|
||||
@Default(IListConst([])) IList<SubsonicFeature> features,
|
||||
required String name,
|
||||
required Uri address,
|
||||
required bool? isActive,
|
||||
required DateTime createdAt,
|
||||
required String username,
|
||||
required String password,
|
||||
@Default(true) bool useTokenAuth,
|
||||
}) = _SubsonicSettings;
|
||||
|
||||
SourcesCompanion toSourceInsertable() {
|
||||
return SourcesCompanion(
|
||||
id: Value(id),
|
||||
name: Value(name),
|
||||
address: Value(address),
|
||||
createdAt: Value(createdAt),
|
||||
);
|
||||
}
|
||||
|
||||
SubsonicSourcesCompanion toSubsonicInsertable() {
|
||||
return SubsonicSourcesCompanion(
|
||||
sourceId: Value(id),
|
||||
features: Value(features),
|
||||
username: Value(username),
|
||||
password: Value(password),
|
||||
useTokenAuth: Value(useTokenAuth),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SubsonicSourceSettings with _$SubsonicSourceSettings {
|
||||
const SubsonicSourceSettings._();
|
||||
|
||||
const factory SubsonicSourceSettings({
|
||||
required SourceSettings source,
|
||||
required SubsonicSettings subsonic,
|
||||
}) = _SubsonicSourceSettings;
|
||||
}
|
||||
|
||||
enum NetworkMode {
|
||||
mobile('mobile'),
|
||||
wifi('wifi');
|
||||
|
||||
const NetworkMode(this.value);
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
}
|
||||
@@ -1,810 +0,0 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Settings {
|
||||
IList<SourceSettings> get sources => throw _privateConstructorUsedError;
|
||||
SourceSettings? get activeSource => throw _privateConstructorUsedError;
|
||||
AppSettings get app => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$SettingsCopyWith<Settings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SettingsCopyWith<$Res> {
|
||||
factory $SettingsCopyWith(Settings value, $Res Function(Settings) then) =
|
||||
_$SettingsCopyWithImpl<$Res, Settings>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{IList<SourceSettings> sources,
|
||||
SourceSettings? activeSource,
|
||||
AppSettings app});
|
||||
|
||||
$AppSettingsCopyWith<$Res> get app;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SettingsCopyWithImpl<$Res, $Val extends Settings>
|
||||
implements $SettingsCopyWith<$Res> {
|
||||
_$SettingsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? sources = null,
|
||||
Object? activeSource = freezed,
|
||||
Object? app = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
sources: null == sources
|
||||
? _value.sources
|
||||
: sources // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceSettings>,
|
||||
activeSource: freezed == activeSource
|
||||
? _value.activeSource
|
||||
: activeSource // ignore: cast_nullable_to_non_nullable
|
||||
as SourceSettings?,
|
||||
app: null == app
|
||||
? _value.app
|
||||
: app // ignore: cast_nullable_to_non_nullable
|
||||
as AppSettings,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$AppSettingsCopyWith<$Res> get app {
|
||||
return $AppSettingsCopyWith<$Res>(_value.app, (value) {
|
||||
return _then(_value.copyWith(app: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_SettingsCopyWith<$Res> implements $SettingsCopyWith<$Res> {
|
||||
factory _$$_SettingsCopyWith(
|
||||
_$_Settings value, $Res Function(_$_Settings) then) =
|
||||
__$$_SettingsCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{IList<SourceSettings> sources,
|
||||
SourceSettings? activeSource,
|
||||
AppSettings app});
|
||||
|
||||
@override
|
||||
$AppSettingsCopyWith<$Res> get app;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_SettingsCopyWithImpl<$Res>
|
||||
extends _$SettingsCopyWithImpl<$Res, _$_Settings>
|
||||
implements _$$_SettingsCopyWith<$Res> {
|
||||
__$$_SettingsCopyWithImpl(
|
||||
_$_Settings _value, $Res Function(_$_Settings) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? sources = null,
|
||||
Object? activeSource = freezed,
|
||||
Object? app = null,
|
||||
}) {
|
||||
return _then(_$_Settings(
|
||||
sources: null == sources
|
||||
? _value.sources
|
||||
: sources // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceSettings>,
|
||||
activeSource: freezed == activeSource
|
||||
? _value.activeSource
|
||||
: activeSource // ignore: cast_nullable_to_non_nullable
|
||||
as SourceSettings?,
|
||||
app: null == app
|
||||
? _value.app
|
||||
: app // ignore: cast_nullable_to_non_nullable
|
||||
as AppSettings,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_Settings implements _Settings {
|
||||
const _$_Settings(
|
||||
{this.sources = const IListConst([]),
|
||||
this.activeSource,
|
||||
this.app = const AppSettings()});
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final IList<SourceSettings> sources;
|
||||
@override
|
||||
final SourceSettings? activeSource;
|
||||
@override
|
||||
@JsonKey()
|
||||
final AppSettings app;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Settings(sources: $sources, activeSource: $activeSource, app: $app)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_Settings &&
|
||||
const DeepCollectionEquality().equals(other.sources, sources) &&
|
||||
(identical(other.activeSource, activeSource) ||
|
||||
other.activeSource == activeSource) &&
|
||||
(identical(other.app, app) || other.app == app));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,
|
||||
const DeepCollectionEquality().hash(sources), activeSource, app);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_SettingsCopyWith<_$_Settings> get copyWith =>
|
||||
__$$_SettingsCopyWithImpl<_$_Settings>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _Settings implements Settings {
|
||||
const factory _Settings(
|
||||
{final IList<SourceSettings> sources,
|
||||
final SourceSettings? activeSource,
|
||||
final AppSettings app}) = _$_Settings;
|
||||
|
||||
@override
|
||||
IList<SourceSettings> get sources;
|
||||
@override
|
||||
SourceSettings? get activeSource;
|
||||
@override
|
||||
AppSettings get app;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_SettingsCopyWith<_$_Settings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AppSettings {
|
||||
int get maxBitrateWifi => throw _privateConstructorUsedError;
|
||||
int get maxBitrateMobile => throw _privateConstructorUsedError;
|
||||
String? get streamFormat => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$AppSettingsCopyWith<AppSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $AppSettingsCopyWith<$Res> {
|
||||
factory $AppSettingsCopyWith(
|
||||
AppSettings value, $Res Function(AppSettings) then) =
|
||||
_$AppSettingsCopyWithImpl<$Res, AppSettings>;
|
||||
@useResult
|
||||
$Res call({int maxBitrateWifi, int maxBitrateMobile, String? streamFormat});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
|
||||
implements $AppSettingsCopyWith<$Res> {
|
||||
_$AppSettingsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? maxBitrateWifi = null,
|
||||
Object? maxBitrateMobile = null,
|
||||
Object? streamFormat = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
maxBitrateWifi: null == maxBitrateWifi
|
||||
? _value.maxBitrateWifi
|
||||
: maxBitrateWifi // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
maxBitrateMobile: null == maxBitrateMobile
|
||||
? _value.maxBitrateMobile
|
||||
: maxBitrateMobile // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
streamFormat: freezed == streamFormat
|
||||
? _value.streamFormat
|
||||
: streamFormat // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_AppSettingsCopyWith<$Res>
|
||||
implements $AppSettingsCopyWith<$Res> {
|
||||
factory _$$_AppSettingsCopyWith(
|
||||
_$_AppSettings value, $Res Function(_$_AppSettings) then) =
|
||||
__$$_AppSettingsCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int maxBitrateWifi, int maxBitrateMobile, String? streamFormat});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_AppSettingsCopyWithImpl<$Res>
|
||||
extends _$AppSettingsCopyWithImpl<$Res, _$_AppSettings>
|
||||
implements _$$_AppSettingsCopyWith<$Res> {
|
||||
__$$_AppSettingsCopyWithImpl(
|
||||
_$_AppSettings _value, $Res Function(_$_AppSettings) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? maxBitrateWifi = null,
|
||||
Object? maxBitrateMobile = null,
|
||||
Object? streamFormat = freezed,
|
||||
}) {
|
||||
return _then(_$_AppSettings(
|
||||
maxBitrateWifi: null == maxBitrateWifi
|
||||
? _value.maxBitrateWifi
|
||||
: maxBitrateWifi // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
maxBitrateMobile: null == maxBitrateMobile
|
||||
? _value.maxBitrateMobile
|
||||
: maxBitrateMobile // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
streamFormat: freezed == streamFormat
|
||||
? _value.streamFormat
|
||||
: streamFormat // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_AppSettings extends _AppSettings {
|
||||
const _$_AppSettings(
|
||||
{this.maxBitrateWifi = 0,
|
||||
this.maxBitrateMobile = 192,
|
||||
this.streamFormat = 'mp3'})
|
||||
: super._();
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final int maxBitrateWifi;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int maxBitrateMobile;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String? streamFormat;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(maxBitrateWifi: $maxBitrateWifi, maxBitrateMobile: $maxBitrateMobile, streamFormat: $streamFormat)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_AppSettings &&
|
||||
(identical(other.maxBitrateWifi, maxBitrateWifi) ||
|
||||
other.maxBitrateWifi == maxBitrateWifi) &&
|
||||
(identical(other.maxBitrateMobile, maxBitrateMobile) ||
|
||||
other.maxBitrateMobile == maxBitrateMobile) &&
|
||||
(identical(other.streamFormat, streamFormat) ||
|
||||
other.streamFormat == streamFormat));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, maxBitrateWifi, maxBitrateMobile, streamFormat);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_AppSettingsCopyWith<_$_AppSettings> get copyWith =>
|
||||
__$$_AppSettingsCopyWithImpl<_$_AppSettings>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _AppSettings extends AppSettings {
|
||||
const factory _AppSettings(
|
||||
{final int maxBitrateWifi,
|
||||
final int maxBitrateMobile,
|
||||
final String? streamFormat}) = _$_AppSettings;
|
||||
const _AppSettings._() : super._();
|
||||
|
||||
@override
|
||||
int get maxBitrateWifi;
|
||||
@override
|
||||
int get maxBitrateMobile;
|
||||
@override
|
||||
String? get streamFormat;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_AppSettingsCopyWith<_$_AppSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SubsonicSettings {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
IList<SubsonicFeature> get features => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
Uri get address => throw _privateConstructorUsedError;
|
||||
bool? get isActive => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
String get username => throw _privateConstructorUsedError;
|
||||
String get password => throw _privateConstructorUsedError;
|
||||
bool get useTokenAuth => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$SubsonicSettingsCopyWith<SubsonicSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SubsonicSettingsCopyWith<$Res> {
|
||||
factory $SubsonicSettingsCopyWith(
|
||||
SubsonicSettings value, $Res Function(SubsonicSettings) then) =
|
||||
_$SubsonicSettingsCopyWithImpl<$Res, SubsonicSettings>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
IList<SubsonicFeature> features,
|
||||
String name,
|
||||
Uri address,
|
||||
bool? isActive,
|
||||
DateTime createdAt,
|
||||
String username,
|
||||
String password,
|
||||
bool useTokenAuth});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SubsonicSettingsCopyWithImpl<$Res, $Val extends SubsonicSettings>
|
||||
implements $SubsonicSettingsCopyWith<$Res> {
|
||||
_$SubsonicSettingsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? features = null,
|
||||
Object? name = null,
|
||||
Object? address = null,
|
||||
Object? isActive = freezed,
|
||||
Object? createdAt = null,
|
||||
Object? username = null,
|
||||
Object? password = null,
|
||||
Object? useTokenAuth = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
features: null == features
|
||||
? _value.features
|
||||
: features // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SubsonicFeature>,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
address: null == address
|
||||
? _value.address
|
||||
: address // ignore: cast_nullable_to_non_nullable
|
||||
as Uri,
|
||||
isActive: freezed == isActive
|
||||
? _value.isActive
|
||||
: isActive // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
username: null == username
|
||||
? _value.username
|
||||
: username // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
useTokenAuth: null == useTokenAuth
|
||||
? _value.useTokenAuth
|
||||
: useTokenAuth // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_SubsonicSettingsCopyWith<$Res>
|
||||
implements $SubsonicSettingsCopyWith<$Res> {
|
||||
factory _$$_SubsonicSettingsCopyWith(
|
||||
_$_SubsonicSettings value, $Res Function(_$_SubsonicSettings) then) =
|
||||
__$$_SubsonicSettingsCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
IList<SubsonicFeature> features,
|
||||
String name,
|
||||
Uri address,
|
||||
bool? isActive,
|
||||
DateTime createdAt,
|
||||
String username,
|
||||
String password,
|
||||
bool useTokenAuth});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_SubsonicSettingsCopyWithImpl<$Res>
|
||||
extends _$SubsonicSettingsCopyWithImpl<$Res, _$_SubsonicSettings>
|
||||
implements _$$_SubsonicSettingsCopyWith<$Res> {
|
||||
__$$_SubsonicSettingsCopyWithImpl(
|
||||
_$_SubsonicSettings _value, $Res Function(_$_SubsonicSettings) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? features = null,
|
||||
Object? name = null,
|
||||
Object? address = null,
|
||||
Object? isActive = freezed,
|
||||
Object? createdAt = null,
|
||||
Object? username = null,
|
||||
Object? password = null,
|
||||
Object? useTokenAuth = null,
|
||||
}) {
|
||||
return _then(_$_SubsonicSettings(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
features: null == features
|
||||
? _value.features
|
||||
: features // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SubsonicFeature>,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
address: null == address
|
||||
? _value.address
|
||||
: address // ignore: cast_nullable_to_non_nullable
|
||||
as Uri,
|
||||
isActive: freezed == isActive
|
||||
? _value.isActive
|
||||
: isActive // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
username: null == username
|
||||
? _value.username
|
||||
: username // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
useTokenAuth: null == useTokenAuth
|
||||
? _value.useTokenAuth
|
||||
: useTokenAuth // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_SubsonicSettings extends _SubsonicSettings {
|
||||
const _$_SubsonicSettings(
|
||||
{required this.id,
|
||||
this.features = const IListConst([]),
|
||||
required this.name,
|
||||
required this.address,
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
required this.username,
|
||||
required this.password,
|
||||
this.useTokenAuth = true})
|
||||
: super._();
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
@JsonKey()
|
||||
final IList<SubsonicFeature> features;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final Uri address;
|
||||
@override
|
||||
final bool? isActive;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final String username;
|
||||
@override
|
||||
final String password;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool useTokenAuth;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SubsonicSettings(id: $id, features: $features, name: $name, address: $address, isActive: $isActive, createdAt: $createdAt, username: $username, password: $password, useTokenAuth: $useTokenAuth)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_SubsonicSettings &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
const DeepCollectionEquality().equals(other.features, features) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.address, address) || other.address == address) &&
|
||||
(identical(other.isActive, isActive) ||
|
||||
other.isActive == isActive) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.username, username) ||
|
||||
other.username == username) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password) &&
|
||||
(identical(other.useTokenAuth, useTokenAuth) ||
|
||||
other.useTokenAuth == useTokenAuth));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
const DeepCollectionEquality().hash(features),
|
||||
name,
|
||||
address,
|
||||
isActive,
|
||||
createdAt,
|
||||
username,
|
||||
password,
|
||||
useTokenAuth);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_SubsonicSettingsCopyWith<_$_SubsonicSettings> get copyWith =>
|
||||
__$$_SubsonicSettingsCopyWithImpl<_$_SubsonicSettings>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _SubsonicSettings extends SubsonicSettings {
|
||||
const factory _SubsonicSettings(
|
||||
{required final int id,
|
||||
final IList<SubsonicFeature> features,
|
||||
required final String name,
|
||||
required final Uri address,
|
||||
required final bool? isActive,
|
||||
required final DateTime createdAt,
|
||||
required final String username,
|
||||
required final String password,
|
||||
final bool useTokenAuth}) = _$_SubsonicSettings;
|
||||
const _SubsonicSettings._() : super._();
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
IList<SubsonicFeature> get features;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
Uri get address;
|
||||
@override
|
||||
bool? get isActive;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
String get username;
|
||||
@override
|
||||
String get password;
|
||||
@override
|
||||
bool get useTokenAuth;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_SubsonicSettingsCopyWith<_$_SubsonicSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SubsonicSourceSettings {
|
||||
SourceSettings get source => throw _privateConstructorUsedError;
|
||||
SubsonicSettings get subsonic => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$SubsonicSourceSettingsCopyWith<SubsonicSourceSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SubsonicSourceSettingsCopyWith<$Res> {
|
||||
factory $SubsonicSourceSettingsCopyWith(SubsonicSourceSettings value,
|
||||
$Res Function(SubsonicSourceSettings) then) =
|
||||
_$SubsonicSourceSettingsCopyWithImpl<$Res, SubsonicSourceSettings>;
|
||||
@useResult
|
||||
$Res call({SourceSettings source, SubsonicSettings subsonic});
|
||||
|
||||
$SubsonicSettingsCopyWith<$Res> get subsonic;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SubsonicSourceSettingsCopyWithImpl<$Res,
|
||||
$Val extends SubsonicSourceSettings>
|
||||
implements $SubsonicSourceSettingsCopyWith<$Res> {
|
||||
_$SubsonicSourceSettingsCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? source = null,
|
||||
Object? subsonic = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as SourceSettings,
|
||||
subsonic: null == subsonic
|
||||
? _value.subsonic
|
||||
: subsonic // ignore: cast_nullable_to_non_nullable
|
||||
as SubsonicSettings,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SubsonicSettingsCopyWith<$Res> get subsonic {
|
||||
return $SubsonicSettingsCopyWith<$Res>(_value.subsonic, (value) {
|
||||
return _then(_value.copyWith(subsonic: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_SubsonicSourceSettingsCopyWith<$Res>
|
||||
implements $SubsonicSourceSettingsCopyWith<$Res> {
|
||||
factory _$$_SubsonicSourceSettingsCopyWith(_$_SubsonicSourceSettings value,
|
||||
$Res Function(_$_SubsonicSourceSettings) then) =
|
||||
__$$_SubsonicSourceSettingsCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({SourceSettings source, SubsonicSettings subsonic});
|
||||
|
||||
@override
|
||||
$SubsonicSettingsCopyWith<$Res> get subsonic;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_SubsonicSourceSettingsCopyWithImpl<$Res>
|
||||
extends _$SubsonicSourceSettingsCopyWithImpl<$Res,
|
||||
_$_SubsonicSourceSettings>
|
||||
implements _$$_SubsonicSourceSettingsCopyWith<$Res> {
|
||||
__$$_SubsonicSourceSettingsCopyWithImpl(_$_SubsonicSourceSettings _value,
|
||||
$Res Function(_$_SubsonicSourceSettings) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? source = null,
|
||||
Object? subsonic = null,
|
||||
}) {
|
||||
return _then(_$_SubsonicSourceSettings(
|
||||
source: null == source
|
||||
? _value.source
|
||||
: source // ignore: cast_nullable_to_non_nullable
|
||||
as SourceSettings,
|
||||
subsonic: null == subsonic
|
||||
? _value.subsonic
|
||||
: subsonic // ignore: cast_nullable_to_non_nullable
|
||||
as SubsonicSettings,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_SubsonicSourceSettings extends _SubsonicSourceSettings {
|
||||
const _$_SubsonicSourceSettings(
|
||||
{required this.source, required this.subsonic})
|
||||
: super._();
|
||||
|
||||
@override
|
||||
final SourceSettings source;
|
||||
@override
|
||||
final SubsonicSettings subsonic;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SubsonicSourceSettings(source: $source, subsonic: $subsonic)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_SubsonicSourceSettings &&
|
||||
(identical(other.source, source) || other.source == source) &&
|
||||
(identical(other.subsonic, subsonic) ||
|
||||
other.subsonic == subsonic));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, source, subsonic);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_SubsonicSourceSettingsCopyWith<_$_SubsonicSourceSettings> get copyWith =>
|
||||
__$$_SubsonicSourceSettingsCopyWithImpl<_$_SubsonicSourceSettings>(
|
||||
this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _SubsonicSourceSettings extends SubsonicSourceSettings {
|
||||
const factory _SubsonicSourceSettings(
|
||||
{required final SourceSettings source,
|
||||
required final SubsonicSettings subsonic}) = _$_SubsonicSourceSettings;
|
||||
const _SubsonicSourceSettings._() : super._();
|
||||
|
||||
@override
|
||||
SourceSettings get source;
|
||||
@override
|
||||
SubsonicSettings get subsonic;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_SubsonicSourceSettingsCopyWith<_$_SubsonicSourceSettings> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import 'package:audio_service/audio_service.dart' show MediaItem;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
|
||||
part 'support.freezed.dart';
|
||||
part 'support.g.dart';
|
||||
|
||||
@freezed
|
||||
class UriCacheInfo with _$UriCacheInfo {
|
||||
const factory UriCacheInfo({
|
||||
required Uri uri,
|
||||
required String cacheKey,
|
||||
required CacheManager cacheManager,
|
||||
}) = _UriCacheInfo;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CacheInfo with _$CacheInfo {
|
||||
const factory CacheInfo({
|
||||
required String cacheKey,
|
||||
required CacheManager cacheManager,
|
||||
}) = _CacheInfo;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Palette with _$Palette {
|
||||
const Palette._();
|
||||
|
||||
const factory Palette({
|
||||
PaletteColor? vibrantColor,
|
||||
PaletteColor? dominantColor,
|
||||
PaletteColor? mutedColor,
|
||||
PaletteColor? darkMutedColor,
|
||||
PaletteColor? darkVibrantColor,
|
||||
PaletteColor? lightMutedColor,
|
||||
PaletteColor? lightVibrantColor,
|
||||
}) = _Palette;
|
||||
|
||||
factory Palette.fromPaletteGenerator(PaletteGenerator generator) {
|
||||
return Palette(
|
||||
vibrantColor: generator.vibrantColor,
|
||||
dominantColor: generator.dominantColor,
|
||||
mutedColor: generator.mutedColor,
|
||||
darkMutedColor: generator.darkMutedColor,
|
||||
darkVibrantColor: generator.darkVibrantColor,
|
||||
lightMutedColor: generator.lightMutedColor,
|
||||
lightVibrantColor: generator.lightVibrantColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ColorTheme with _$ColorTheme {
|
||||
const factory ColorTheme({
|
||||
required ThemeData theme,
|
||||
required Color gradientHigh,
|
||||
required Color gradientLow,
|
||||
required Color darkBackground,
|
||||
required Color darkerBackground,
|
||||
required Color onDarkerBackground,
|
||||
}) = _ColorTheme;
|
||||
}
|
||||
|
||||
enum QueueContextType {
|
||||
song('song'),
|
||||
album('album'),
|
||||
playlist('playlist'),
|
||||
library('library'),
|
||||
genre('genre'),
|
||||
artist('artist');
|
||||
|
||||
const QueueContextType(this.value);
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
}
|
||||
|
||||
enum QueueMode {
|
||||
user('user'),
|
||||
radio('radio');
|
||||
|
||||
const QueueMode(this.value);
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
}
|
||||
|
||||
enum RepeatMode {
|
||||
none('none'),
|
||||
all('all'),
|
||||
one('one');
|
||||
|
||||
const RepeatMode(this.value);
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class QueueItemState with _$QueueItemState {
|
||||
const factory QueueItemState({
|
||||
required String id,
|
||||
required QueueContextType contextType,
|
||||
String? contextId,
|
||||
String? contextTitle,
|
||||
}) = _QueueItemState;
|
||||
|
||||
factory QueueItemState.fromJson(Map<String, dynamic> json) =>
|
||||
_$QueueItemStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MediaItemData with _$MediaItemData {
|
||||
const factory MediaItemData({
|
||||
required int sourceId,
|
||||
String? albumId,
|
||||
@MediaItemArtCacheConverter() MediaItemArtCache? artCache,
|
||||
required QueueContextType contextType,
|
||||
String? contextId,
|
||||
}) = _MediaItemData;
|
||||
|
||||
factory MediaItemData.fromJson(Map<String, dynamic> json) =>
|
||||
_$MediaItemDataFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MediaItemArtCache with _$MediaItemArtCache {
|
||||
const factory MediaItemArtCache({
|
||||
required Uri fullArtUri,
|
||||
required String fullArtCacheKey,
|
||||
required Uri thumbnailArtUri,
|
||||
required String thumbnailArtCacheKey,
|
||||
}) = _MediaItemArtCache;
|
||||
|
||||
factory MediaItemArtCache.fromJson(Map<String, dynamic> json) =>
|
||||
_$MediaItemArtCacheFromJson(json);
|
||||
}
|
||||
|
||||
class MediaItemArtCacheConverter
|
||||
implements JsonConverter<MediaItemArtCache, Map<String, dynamic>> {
|
||||
const MediaItemArtCacheConverter();
|
||||
|
||||
@override
|
||||
MediaItemArtCache fromJson(Map<String, dynamic> json) =>
|
||||
MediaItemArtCache.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(MediaItemArtCache object) => object.toJson();
|
||||
}
|
||||
|
||||
extension MediaItemPlus on MediaItem {
|
||||
MediaItemData get data => MediaItemData.fromJson(extras!['data']);
|
||||
set data(MediaItemData data) {
|
||||
extras!['data'] = data.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ListDownloadStatus with _$ListDownloadStatus {
|
||||
const factory ListDownloadStatus({
|
||||
required int total,
|
||||
required int downloaded,
|
||||
required int downloading,
|
||||
}) = _ListDownloadStatus;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MultiChoiceOption with _$MultiChoiceOption {
|
||||
const factory MultiChoiceOption({
|
||||
required String title,
|
||||
}) = _MultiChoiceOption;
|
||||
|
||||
factory MultiChoiceOption.int({
|
||||
required String title,
|
||||
required int option,
|
||||
}) = _MultiChoiceOptionInt;
|
||||
|
||||
factory MultiChoiceOption.string({
|
||||
required String title,
|
||||
required String option,
|
||||
}) = _MultiChoiceOptionString;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,83 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'support.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_QueueItemState _$$_QueueItemStateFromJson(Map<String, dynamic> json) =>
|
||||
_$_QueueItemState(
|
||||
id: json['id'] as String,
|
||||
contextType: $enumDecode(_$QueueContextTypeEnumMap, json['contextType']),
|
||||
contextId: json['contextId'] as String?,
|
||||
contextTitle: json['contextTitle'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_QueueItemStateToJson(_$_QueueItemState instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'contextType': _$QueueContextTypeEnumMap[instance.contextType]!,
|
||||
'contextId': instance.contextId,
|
||||
'contextTitle': instance.contextTitle,
|
||||
};
|
||||
|
||||
const _$QueueContextTypeEnumMap = {
|
||||
QueueContextType.song: 'song',
|
||||
QueueContextType.album: 'album',
|
||||
QueueContextType.playlist: 'playlist',
|
||||
QueueContextType.library: 'library',
|
||||
QueueContextType.genre: 'genre',
|
||||
QueueContextType.artist: 'artist',
|
||||
};
|
||||
|
||||
_$_MediaItemData _$$_MediaItemDataFromJson(Map<String, dynamic> json) =>
|
||||
_$_MediaItemData(
|
||||
sourceId: json['sourceId'] as int,
|
||||
albumId: json['albumId'] as String?,
|
||||
artCache:
|
||||
_$JsonConverterFromJson<Map<String, dynamic>, MediaItemArtCache>(
|
||||
json['artCache'], const MediaItemArtCacheConverter().fromJson),
|
||||
contextType: $enumDecode(_$QueueContextTypeEnumMap, json['contextType']),
|
||||
contextId: json['contextId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_MediaItemDataToJson(_$_MediaItemData instance) =>
|
||||
<String, dynamic>{
|
||||
'sourceId': instance.sourceId,
|
||||
'albumId': instance.albumId,
|
||||
'artCache':
|
||||
_$JsonConverterToJson<Map<String, dynamic>, MediaItemArtCache>(
|
||||
instance.artCache, const MediaItemArtCacheConverter().toJson),
|
||||
'contextType': _$QueueContextTypeEnumMap[instance.contextType]!,
|
||||
'contextId': instance.contextId,
|
||||
};
|
||||
|
||||
Value? _$JsonConverterFromJson<Json, Value>(
|
||||
Object? json,
|
||||
Value? Function(Json json) fromJson,
|
||||
) =>
|
||||
json == null ? null : fromJson(json as Json);
|
||||
|
||||
Json? _$JsonConverterToJson<Json, Value>(
|
||||
Value? value,
|
||||
Json? Function(Value value) toJson,
|
||||
) =>
|
||||
value == null ? null : toJson(value);
|
||||
|
||||
_$_MediaItemArtCache _$$_MediaItemArtCacheFromJson(Map<String, dynamic> json) =>
|
||||
_$_MediaItemArtCache(
|
||||
fullArtUri: Uri.parse(json['fullArtUri'] as String),
|
||||
fullArtCacheKey: json['fullArtCacheKey'] as String,
|
||||
thumbnailArtUri: Uri.parse(json['thumbnailArtUri'] as String),
|
||||
thumbnailArtCacheKey: json['thumbnailArtCacheKey'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_MediaItemArtCacheToJson(
|
||||
_$_MediaItemArtCache instance) =>
|
||||
<String, dynamic>{
|
||||
'fullArtUri': instance.fullArtUri.toString(),
|
||||
'fullArtCacheKey': instance.fullArtCacheKey,
|
||||
'thumbnailArtUri': instance.thumbnailArtUri.toString(),
|
||||
'thumbnailArtCacheKey': instance.thumbnailArtCacheKey,
|
||||
};
|
||||
@@ -1,703 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:pool/pool.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
import '../cache/image_cache.dart';
|
||||
import '../database/database.dart';
|
||||
import '../log.dart';
|
||||
import '../models/music.dart';
|
||||
import '../models/query.dart';
|
||||
import '../models/support.dart';
|
||||
import '../sources/music_source.dart';
|
||||
import '../state/settings.dart';
|
||||
import 'cache_service.dart';
|
||||
import 'settings_service.dart';
|
||||
|
||||
part 'audio_service.g.dart';
|
||||
|
||||
class QueueSourceItem {
|
||||
final MediaItem mediaItem;
|
||||
final UriAudioSource audioSource;
|
||||
final QueueData queueData;
|
||||
|
||||
const QueueSourceItem({
|
||||
required this.mediaItem,
|
||||
required this.audioSource,
|
||||
required this.queueData,
|
||||
});
|
||||
}
|
||||
|
||||
class QueueSlice {
|
||||
final QueueSourceItem? prev;
|
||||
final QueueSourceItem? current;
|
||||
final QueueSourceItem? next;
|
||||
|
||||
const QueueSlice(this.prev, this.current, this.next);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
FutureOr<AudioControl> audioControlInit(AudioControlInitRef ref) async {
|
||||
final imageCache = ref.watch(imageCacheProvider);
|
||||
|
||||
return AudioService.init(
|
||||
builder: () => AudioControl(AudioPlayer(), ref),
|
||||
config: const AudioServiceConfig(
|
||||
androidNotificationChannelId: 'com.subtracks2.channel.audio',
|
||||
androidNotificationChannelName: 'Music playback',
|
||||
androidNotificationIcon: 'drawable/ic_stat_name',
|
||||
),
|
||||
cacheManager: imageCache,
|
||||
cacheKeyResolver: (mediaItem) =>
|
||||
mediaItem.data.artCache?.thumbnailArtCacheKey ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AudioControl audioControl(AudioControlRef ref) {
|
||||
return ref.watch(audioControlInitProvider).requireValue;
|
||||
}
|
||||
|
||||
class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler {
|
||||
static const radioLength = 10;
|
||||
|
||||
Stream<Duration> get position => _player.positionStream;
|
||||
|
||||
BehaviorSubject<QueueMode> queueMode = BehaviorSubject.seeded(QueueMode.user);
|
||||
BehaviorSubject<List<int>?> shuffleIndicies = BehaviorSubject.seeded(null);
|
||||
BehaviorSubject<AudioServiceRepeatMode> repeatMode =
|
||||
BehaviorSubject.seeded(AudioServiceRepeatMode.none);
|
||||
|
||||
final Ref _ref;
|
||||
final AudioPlayer _player;
|
||||
|
||||
int? _queueLength;
|
||||
int? _previousCurrentTrackIndex;
|
||||
|
||||
final _syncPool = Pool(1);
|
||||
final _playLock = Lock();
|
||||
final _currentIndexIgnore = <int?>[null, 0];
|
||||
|
||||
final ConcatenatingAudioSource _audioSource =
|
||||
ConcatenatingAudioSource(children: []);
|
||||
|
||||
SubtracksDatabase get _db => _ref.read(databaseProvider);
|
||||
CacheService get _cache => _ref.read(cacheServiceProvider);
|
||||
MusicSource get _source => _ref.read(musicSourceProvider);
|
||||
int get _sourceId => _ref.read(sourceIdProvider);
|
||||
|
||||
AudioControl(this._player, this._ref) {
|
||||
_player.playbackEventStream.listen((PlaybackEvent event) {
|
||||
final playing = _player.playing;
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
if (playing) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.stop,
|
||||
MediaControl.skipToNext,
|
||||
],
|
||||
systemActions: const {
|
||||
MediaAction.seek,
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 3],
|
||||
processingState: const {
|
||||
ProcessingState.idle: AudioProcessingState.idle,
|
||||
ProcessingState.loading: AudioProcessingState.loading,
|
||||
ProcessingState.buffering: AudioProcessingState.buffering,
|
||||
ProcessingState.ready: AudioProcessingState.ready,
|
||||
ProcessingState.completed: AudioProcessingState.completed,
|
||||
}[_player.processingState]!,
|
||||
playing: playing,
|
||||
updatePosition: _player.position,
|
||||
bufferedPosition: _player.bufferedPosition,
|
||||
queueIndex: event.currentIndex,
|
||||
));
|
||||
});
|
||||
|
||||
_player.playbackEventStream.doOnError((e, st) async {
|
||||
log.warning('playbackEventStream', e, st);
|
||||
});
|
||||
|
||||
shuffleIndicies.listen((value) {
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
shuffleMode: value != null
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
));
|
||||
});
|
||||
|
||||
repeatMode.listen((value) {
|
||||
playbackState.add(playbackState.value.copyWith(repeatMode: value));
|
||||
});
|
||||
|
||||
_player.processingStateStream.listen((event) async {
|
||||
if (event == ProcessingState.completed) {
|
||||
if (_audioSource.length > 0) {
|
||||
log.fine('completed');
|
||||
await stop();
|
||||
await seek(Duration.zero);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_ref.listen(sourceIdProvider, (_, __) async {
|
||||
await _clearAudioSource(true);
|
||||
await _db.clearQueue();
|
||||
});
|
||||
|
||||
_ref.listen(maxBitrateProvider, (prev, next) async {
|
||||
if (prev?.valueOrNull != next.valueOrNull) {
|
||||
await _resyncQueue(true);
|
||||
}
|
||||
});
|
||||
|
||||
_ref.listen(
|
||||
settingsServiceProvider.select((value) => value.app.streamFormat),
|
||||
(prev, next) async {
|
||||
await _resyncQueue(true);
|
||||
});
|
||||
|
||||
_player.durationStream.listen((duration) {
|
||||
if (mediaItem.valueOrNull == null) return;
|
||||
|
||||
final index = queue.valueOrNull?.indexOf(mediaItem.value!);
|
||||
|
||||
final updated = mediaItem.value!.copyWith(duration: duration);
|
||||
mediaItem.add(updated);
|
||||
|
||||
if (index != null) {
|
||||
queue.add(queue.value..replaceRange(index, index + 1, [updated]));
|
||||
}
|
||||
});
|
||||
|
||||
_player.currentIndexStream.listen((index) async {
|
||||
if (_currentIndexIgnore.contains(index)) {
|
||||
_currentIndexIgnore.remove(index);
|
||||
return;
|
||||
}
|
||||
if (index == null || index >= _audioSource.sequence.length) return;
|
||||
|
||||
final queueIndex = _audioSource.sequence[index].tag;
|
||||
if (queueIndex != null) {
|
||||
await _db.setCurrentTrack(queueIndex);
|
||||
}
|
||||
});
|
||||
|
||||
_db.currentTrackIndex().watchSingleOrNull().listen((index) {
|
||||
// distict() except for when in loop one mode
|
||||
if (repeatMode.value != AudioServiceRepeatMode.one &&
|
||||
_previousCurrentTrackIndex == index) {
|
||||
return;
|
||||
}
|
||||
|
||||
_previousCurrentTrackIndex = index;
|
||||
_syncPool.withResource(() => _syncQueue(index));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
await _player.setAudioSource(_audioSource, preload: false);
|
||||
|
||||
final last = await _db.getLastAudioState().getSingleOrNull();
|
||||
if (last == null) return;
|
||||
|
||||
_queueLength = await _db.queueLength().getSingleOrNull();
|
||||
final repeat = {
|
||||
RepeatMode.none: AudioServiceRepeatMode.none,
|
||||
RepeatMode.all: AudioServiceRepeatMode.all,
|
||||
RepeatMode.one: AudioServiceRepeatMode.one,
|
||||
}[last.repeat]!;
|
||||
repeatMode.add(repeat);
|
||||
await repeatMode.firstWhere((e) => e == repeat);
|
||||
|
||||
queueMode.add(last.queueMode);
|
||||
await queueMode.firstWhere((e) => e == last.queueMode);
|
||||
|
||||
if (last.shuffleIndicies != null) {
|
||||
final indicies = last.shuffleIndicies!.unlock;
|
||||
shuffleIndicies.add(indicies);
|
||||
await shuffleIndicies.firstWhere((e) => e == indicies);
|
||||
}
|
||||
|
||||
final startIndex = await _db.currentTrackIndex().getSingleOrNull();
|
||||
|
||||
if (startIndex != null && _queueLength != null) {
|
||||
await _preparePlayer(startIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> playSongs({
|
||||
QueueMode mode = QueueMode.user,
|
||||
required QueueContextType context,
|
||||
String? contextId,
|
||||
required ListQuery query,
|
||||
required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
|
||||
int? startIndex,
|
||||
bool? shuffle,
|
||||
}) async {
|
||||
if (!_playLock.locked) {
|
||||
return _playLock.synchronized(
|
||||
() => _playSongs(
|
||||
mode: mode,
|
||||
context: context,
|
||||
contextId: contextId,
|
||||
query: query,
|
||||
getSongs: getSongs,
|
||||
startIndex: startIndex,
|
||||
shuffle: shuffle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> playRadio({
|
||||
required QueueContextType context,
|
||||
String? contextId,
|
||||
ListQuery query = const ListQuery(),
|
||||
required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
|
||||
}) async {
|
||||
await playSongs(
|
||||
mode: QueueMode.radio,
|
||||
context: QueueContextType.library,
|
||||
contextId: contextId,
|
||||
query: query.copyWith(
|
||||
sort: SortBy(
|
||||
column: 'SIN(songs.ROWID + ${Random().nextInt(10000)})',
|
||||
),
|
||||
),
|
||||
getSongs: getSongs,
|
||||
startIndex: 0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _playSongs({
|
||||
QueueMode mode = QueueMode.user,
|
||||
required QueueContextType context,
|
||||
String? contextId,
|
||||
required ListQuery query,
|
||||
required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
|
||||
int? startIndex,
|
||||
bool? shuffle,
|
||||
}) async {
|
||||
shuffle = shuffle ?? shuffleIndicies.valueOrNull != null;
|
||||
queueMode.add(mode);
|
||||
|
||||
if (mode == QueueMode.radio) {
|
||||
if (repeatMode.value != AudioServiceRepeatMode.none) {
|
||||
await _loop(AudioServiceRepeatMode.none);
|
||||
}
|
||||
if (shuffleIndicies.value != null) {
|
||||
await _shuffle(unshuffle: true);
|
||||
}
|
||||
}
|
||||
|
||||
await _clearAudioSource();
|
||||
|
||||
const limit = 500;
|
||||
_queueLength = 0;
|
||||
|
||||
if ((startIndex == null || startIndex >= limit) && shuffle) {
|
||||
startIndex = Random().nextInt(limit);
|
||||
} else {
|
||||
startIndex ??= 0;
|
||||
}
|
||||
|
||||
// clear the queue and load the initial songs only up to the startIndex
|
||||
await _db.transaction(() async {
|
||||
await _db.clearQueue();
|
||||
|
||||
while (_queueLength! <= startIndex!) {
|
||||
final songs = await getSongs(query.copyWith(
|
||||
page: Pagination(limit: limit, offset: _queueLength!),
|
||||
));
|
||||
await _loadQueueSongs(songs, _queueLength!, context, contextId);
|
||||
if (songs.length < limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// if there are less songs than the limit and we're shuffling,
|
||||
// choose a new random startIndex
|
||||
if (startIndex >= _queueLength!) {
|
||||
startIndex = Random().nextInt(_queueLength!);
|
||||
}
|
||||
|
||||
await _preparePlayer(startIndex, shuffle);
|
||||
await _db.setCurrentTrack(startIndex);
|
||||
play();
|
||||
|
||||
const maxLength = 10000;
|
||||
|
||||
// no need to do extra loading if we've already loaded everything
|
||||
if (_queueLength! < limit) return;
|
||||
while (true) {
|
||||
final songs = await getSongs(query.copyWith(
|
||||
page: Pagination(limit: limit, offset: _queueLength!),
|
||||
));
|
||||
await _loadQueueSongs(songs, _queueLength!, context, contextId);
|
||||
if (songs.length < limit || _queueLength! >= maxLength) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadQueueSongs(
|
||||
Iterable<Song> songs,
|
||||
int total,
|
||||
QueueContextType context,
|
||||
String? contextId,
|
||||
) async {
|
||||
await _db.insertQueue(songs.mapIndexed(
|
||||
(i, song) => QueueCompanion.insert(
|
||||
index: Value(i + (_queueLength ?? 0)),
|
||||
sourceId: song.sourceId,
|
||||
id: song.id,
|
||||
context: context,
|
||||
contextId: Value(contextId),
|
||||
),
|
||||
));
|
||||
|
||||
_queueLength = (_queueLength ?? 0) + songs.length;
|
||||
if (shuffleIndicies.valueOrNull != null) {
|
||||
await _generateShuffleIndicies(startIndex: _player.currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _preparePlayer(int startIndex, [bool? shuffle]) async {
|
||||
if (shuffle == true) {
|
||||
await _shuffle(startIndex: startIndex);
|
||||
} else if (shuffle == false) {
|
||||
await _shuffle(unshuffle: true);
|
||||
}
|
||||
|
||||
final slice = await _getQueueSlice(startIndex);
|
||||
if (slice == null) {
|
||||
throw StateError('Could not get queue slice!');
|
||||
}
|
||||
|
||||
final list =
|
||||
[slice.prev, slice.current, slice.next].whereNotNull().toList();
|
||||
|
||||
mediaItem.add(slice.current!.mediaItem);
|
||||
queue.add(list.map((e) => e.mediaItem).toList());
|
||||
|
||||
log.fine('addAll');
|
||||
await _audioSource.addAll(list.map((e) => e.audioSource).toList());
|
||||
await _player.seek(Duration.zero, index: list.indexOf(slice.current!));
|
||||
}
|
||||
|
||||
Future<void> _syncQueue(int? index) async {
|
||||
if (index == null || _queueLength == null) return;
|
||||
|
||||
final slice = await _getQueueSlice(index);
|
||||
if (slice == null || slice.current == null) return;
|
||||
|
||||
mediaItem.add(slice.current!.mediaItem);
|
||||
queue.add(
|
||||
[slice.prev, slice.current, slice.next]
|
||||
.map((e) => e?.mediaItem)
|
||||
.whereNotNull()
|
||||
.toList(),
|
||||
);
|
||||
|
||||
final sourceIndex = _player.currentIndex;
|
||||
final sourceNeedsNext = sourceIndex == _audioSource.length - 1;
|
||||
final sourceNeedsPrev = sourceIndex == 0;
|
||||
|
||||
if (sourceNeedsNext && slice.next != null) {
|
||||
log.fine('add');
|
||||
await _audioSource.add(slice.next!.audioSource);
|
||||
}
|
||||
if (sourceNeedsPrev && slice.prev != null) {
|
||||
await _insertFirstAudioSource(slice.prev!.audioSource);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loop(AudioServiceRepeatMode mode) async {
|
||||
repeatMode.add(mode);
|
||||
await repeatMode.firstWhere((e) => e == mode);
|
||||
|
||||
await _resyncQueue();
|
||||
}
|
||||
|
||||
Future<void> _generateShuffleIndicies({
|
||||
bool unshuffle = false,
|
||||
int? startIndex,
|
||||
}) async {
|
||||
final indicies = unshuffle
|
||||
? null
|
||||
: (List.generate(_queueLength!, (i) => i + 1)
|
||||
..insert(0, 0)
|
||||
..removeLast()
|
||||
..shuffle());
|
||||
|
||||
if (indicies != null && startIndex != null) {
|
||||
indicies.removeAt(indicies.indexOf(startIndex));
|
||||
indicies.insert(0, startIndex);
|
||||
}
|
||||
shuffleIndicies.add(indicies);
|
||||
await shuffleIndicies.firstWhere((e) => e == indicies);
|
||||
}
|
||||
|
||||
Future<void> _shuffle({bool unshuffle = false, int? startIndex}) async {
|
||||
await _generateShuffleIndicies(
|
||||
unshuffle: unshuffle,
|
||||
startIndex: startIndex,
|
||||
);
|
||||
|
||||
await _resyncQueue();
|
||||
}
|
||||
|
||||
Future<void> _resyncQueue([bool reloadCurrent = false]) async {
|
||||
return _syncPool.withResource(() async {
|
||||
final currentSource =
|
||||
_player.sequenceState?.currentSource as UriAudioSource?;
|
||||
if (currentSource == null) return;
|
||||
|
||||
final currentSourceIndex = _player.sequence!.indexOf(currentSource);
|
||||
await _pruneAudioSources(currentSourceIndex);
|
||||
|
||||
if (reloadCurrent && !currentSource.uri.isScheme('file')) {
|
||||
final position = _player.position;
|
||||
|
||||
final item = (await _getQueueItems([currentSource.tag]))[0];
|
||||
|
||||
await _audioSource.clear();
|
||||
await _audioSource.add(item.audioSource);
|
||||
await seek(position);
|
||||
}
|
||||
|
||||
await _syncQueue(currentSource.tag);
|
||||
});
|
||||
}
|
||||
|
||||
int _realIndex(int index) {
|
||||
if (shuffleIndicies.valueOrNull == null) {
|
||||
return index;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= shuffleIndicies.value!.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return shuffleIndicies.value![index];
|
||||
}
|
||||
|
||||
int _effectiveIndex(int index) {
|
||||
if (shuffleIndicies.valueOrNull == null) {
|
||||
return index;
|
||||
}
|
||||
|
||||
return shuffleIndicies.value!.indexOf(index);
|
||||
}
|
||||
|
||||
Future<void> _insertFirstAudioSource(AudioSource source) {
|
||||
log.fine('insert');
|
||||
final wait = _audioSource.insert(0, source);
|
||||
_currentIndexIgnore.add(1);
|
||||
return wait;
|
||||
}
|
||||
|
||||
Future<void> _pruneAudioSources(int keepIndex) async {
|
||||
if (keepIndex > 0) {
|
||||
log.fine('removeRange 0');
|
||||
final wait = _audioSource.removeRange(0, keepIndex);
|
||||
_currentIndexIgnore.add(0);
|
||||
await wait;
|
||||
}
|
||||
if (_audioSource.length > 1) {
|
||||
log.fine('removeRange 1');
|
||||
await _audioSource.removeRange(1, _audioSource.length);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearAudioSource([bool clearMetadata = false]) async {
|
||||
// await _player.stop();
|
||||
log.fine('_clearAudioSource');
|
||||
await _audioSource.clear();
|
||||
|
||||
if (clearMetadata) {
|
||||
mediaItem.add(null);
|
||||
queue.add([]);
|
||||
queueTitle.add('');
|
||||
}
|
||||
}
|
||||
|
||||
Future<QueueSlice?> _getQueueSlice(int index) async {
|
||||
if (_queueLength == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final effectiveIndex = _effectiveIndex(index);
|
||||
|
||||
int nextIndex;
|
||||
int prevIndex;
|
||||
if (repeatMode.value == AudioServiceRepeatMode.none) {
|
||||
nextIndex = _realIndex(effectiveIndex + 1);
|
||||
prevIndex = _realIndex(effectiveIndex - 1);
|
||||
} else if (repeatMode.value == AudioServiceRepeatMode.one) {
|
||||
nextIndex = index;
|
||||
prevIndex = index;
|
||||
} else {
|
||||
nextIndex = _realIndex(
|
||||
effectiveIndex + 1 >= _queueLength! ? 0 : effectiveIndex + 1,
|
||||
);
|
||||
prevIndex = _realIndex(
|
||||
effectiveIndex - 1 < 0 ? _queueLength! - 1 : effectiveIndex - 1,
|
||||
);
|
||||
}
|
||||
|
||||
final slice = await _getQueueItems([prevIndex, index, nextIndex]);
|
||||
final current = slice.firstWhereOrNull(
|
||||
(e) => e.queueData.index == index,
|
||||
);
|
||||
final next = slice.firstWhereOrNull((e) => e.queueData.index == nextIndex);
|
||||
final prev = slice.firstWhereOrNull((e) => e.queueData.index == prevIndex);
|
||||
|
||||
return QueueSlice(prev, current, next);
|
||||
}
|
||||
|
||||
Future<List<QueueSourceItem>> _getQueueItems(List<int> indexes) async {
|
||||
final slice = await _db.queueInIndicies(indexes).get();
|
||||
final songs =
|
||||
await _db.songsInIds(_sourceId, slice.map((e) => e.id).toList()).get();
|
||||
final songMap = {for (var song in songs) song.id: song};
|
||||
|
||||
final albumIds = songs.map((e) => e.albumId).whereNotNull().toSet();
|
||||
final albums = await _db.albumsInIds(_sourceId, albumIds.toList()).get();
|
||||
final albumArtMap = {
|
||||
for (var album in albums) album.id: _mapArtCache(album)
|
||||
};
|
||||
|
||||
final queueItems = slice.map(
|
||||
(item) => _mapSong(
|
||||
songMap[item.id]!,
|
||||
MediaItemData(
|
||||
sourceId: item.sourceId,
|
||||
contextType: item.context,
|
||||
contextId: item.contextId,
|
||||
artCache: albumArtMap[songMap[item.id]!.albumId],
|
||||
),
|
||||
item,
|
||||
),
|
||||
);
|
||||
|
||||
return queueItems.toList();
|
||||
}
|
||||
|
||||
QueueSourceItem _mapSong(Song song, MediaItemData data, QueueData queueData) {
|
||||
final item = MediaItem(
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
album: song.album,
|
||||
duration: song.duration,
|
||||
artUri: data.artCache?.thumbnailArtUri ?? _cache.placeholderThumbImageUri,
|
||||
extras: {},
|
||||
);
|
||||
item.data = data;
|
||||
|
||||
return QueueSourceItem(
|
||||
mediaItem: item,
|
||||
audioSource: song.downloadFilePath != null
|
||||
? AudioSource.file(song.downloadFilePath!, tag: queueData.index)
|
||||
: AudioSource.uri(_source.streamUri(song.id), tag: queueData.index),
|
||||
queueData: queueData,
|
||||
);
|
||||
}
|
||||
|
||||
MediaItemArtCache _mapArtCache(Album album) {
|
||||
final full = _cache.albumArt(album, thumbnail: false);
|
||||
final thumbnail = _cache.albumArt(album, thumbnail: true);
|
||||
return MediaItemArtCache(
|
||||
fullArtUri: full.uri,
|
||||
fullArtCacheKey: full.cacheKey,
|
||||
thumbnailArtUri: thumbnail.uri,
|
||||
thumbnailArtCacheKey: thumbnail.cacheKey,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
/// AudioHandler
|
||||
///
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
await _player.play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
await _player.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
await _player.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async {
|
||||
await _player.seek(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToNext() => _player.seekToNext();
|
||||
|
||||
@override
|
||||
Future<void> skipToPrevious() => _player.seekToPrevious();
|
||||
|
||||
@override
|
||||
Future<void> skipToQueueItem(int index) async {
|
||||
if (_player.effectiveIndices == null || _player.effectiveIndices!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
index = _player.effectiveIndices![index];
|
||||
if (index < 0 || index >= queue.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _player.seek(Duration.zero, index: index);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||
if (queueMode.value == QueueMode.radio) {
|
||||
switch (repeatMode) {
|
||||
case AudioServiceRepeatMode.all:
|
||||
case AudioServiceRepeatMode.group:
|
||||
case AudioServiceRepeatMode.one:
|
||||
return _loop(AudioServiceRepeatMode.one);
|
||||
default:
|
||||
return _loop(AudioServiceRepeatMode.none);
|
||||
}
|
||||
}
|
||||
|
||||
return _loop(repeatMode);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||
if (queueMode.value == QueueMode.radio) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (shuffleMode) {
|
||||
case AudioServiceShuffleMode.all:
|
||||
case AudioServiceShuffleMode.group:
|
||||
return _shuffle(startIndex: _player.sequenceState?.currentSource?.tag);
|
||||
case AudioServiceShuffleMode.none:
|
||||
return _shuffle(unshuffle: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'audio_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$audioControlInitHash() => r'ecde06a9f4f7be5ca28e1f5f3c1f3e7fb2ce8dc5';
|
||||
|
||||
/// See also [audioControlInit].
|
||||
@ProviderFor(audioControlInit)
|
||||
final audioControlInitProvider = FutureProvider<AudioControl>.internal(
|
||||
audioControlInit,
|
||||
name: r'audioControlInitProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$audioControlInitHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef AudioControlInitRef = FutureProviderRef<AudioControl>;
|
||||
String _$audioControlHash() => r'ea50108f29366182238a5e68d6cdcd1d874e4ba2';
|
||||
|
||||
/// See also [audioControl].
|
||||
@ProviderFor(audioControl)
|
||||
final audioControlProvider = Provider<AudioControl>.internal(
|
||||
audioControl,
|
||||
name: r'audioControlProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$audioControlHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef AudioControlRef = ProviderRef<AudioControl>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,112 +0,0 @@
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../cache/image_cache.dart';
|
||||
import '../models/music.dart';
|
||||
import '../models/support.dart';
|
||||
import '../sources/music_source.dart';
|
||||
import '../state/init.dart';
|
||||
import '../state/settings.dart';
|
||||
|
||||
part 'cache_service.g.dart';
|
||||
|
||||
String _cacheKey(
|
||||
String type,
|
||||
int sourceId,
|
||||
String albumId, [
|
||||
bool thumbnail = false,
|
||||
]) {
|
||||
return '$type${thumbnail ? 'Thumb' : ''}-$sourceId-$albumId';
|
||||
}
|
||||
|
||||
String albumArtCacheKey(
|
||||
int sourceId,
|
||||
String albumId,
|
||||
bool thumbnail,
|
||||
) {
|
||||
return _cacheKey('albumArt', sourceId, albumId, thumbnail);
|
||||
}
|
||||
|
||||
String playlistArtCacheKey(
|
||||
int sourceId,
|
||||
String albumId,
|
||||
bool thumbnail,
|
||||
) {
|
||||
return _cacheKey('playlistArt', sourceId, albumId, thumbnail);
|
||||
}
|
||||
|
||||
String artistArtCacheKey(
|
||||
int sourceId,
|
||||
String albumId,
|
||||
bool thumbnail,
|
||||
) {
|
||||
return _cacheKey('artistArt', sourceId, albumId, thumbnail);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
CacheService cacheService(CacheServiceRef ref) {
|
||||
final imageCache = ref.watch(imageCacheProvider);
|
||||
final source = ref.watch(musicSourceProvider);
|
||||
final placeholderImageUri =
|
||||
ref.watch(placeholderImageUriProvider).requireValue;
|
||||
final placeholderThumbImageUri =
|
||||
ref.watch(placeholderThumbImageUriProvider).requireValue;
|
||||
|
||||
return CacheService(
|
||||
imageCache: imageCache,
|
||||
source: source,
|
||||
placeholderImageUri: placeholderImageUri,
|
||||
placeholderThumbImageUri: placeholderThumbImageUri,
|
||||
);
|
||||
}
|
||||
|
||||
class CacheService {
|
||||
final CacheManager imageCache;
|
||||
final MusicSource source;
|
||||
final Uri placeholderImageUri;
|
||||
final Uri placeholderThumbImageUri;
|
||||
|
||||
CacheService({
|
||||
required this.imageCache,
|
||||
required this.source,
|
||||
required this.placeholderImageUri,
|
||||
required this.placeholderThumbImageUri,
|
||||
});
|
||||
|
||||
UriCacheInfo albumArt(Album album, {bool thumbnail = true}) {
|
||||
final id = album.coverArt ?? album.id;
|
||||
return UriCacheInfo(
|
||||
uri: source.coverArtUri(id, thumbnail: thumbnail),
|
||||
cacheKey: albumArtCacheKey(source.id, album.id, thumbnail),
|
||||
cacheManager: imageCache,
|
||||
);
|
||||
}
|
||||
|
||||
UriCacheInfo playlistArt(Playlist playlist, {bool thumbnail = true}) {
|
||||
final id = playlist.coverArt ?? playlist.id;
|
||||
return UriCacheInfo(
|
||||
uri: source.coverArtUri(id),
|
||||
cacheKey: playlistArtCacheKey(source.id, playlist.id, thumbnail),
|
||||
cacheManager: imageCache);
|
||||
}
|
||||
|
||||
UriCacheInfo placeholder({bool thumbnail = true}) {
|
||||
final uri = thumbnail ? placeholderThumbImageUri : placeholderImageUri;
|
||||
return UriCacheInfo(
|
||||
uri: uri,
|
||||
cacheKey: uri.toString(),
|
||||
cacheManager: imageCache,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uri?> artistArtUri(String artistId, {bool thumbnail = true}) {
|
||||
return source.artistArtUri(artistId, thumbnail: thumbnail);
|
||||
}
|
||||
|
||||
CacheInfo artistArtCacheInfo(String artistId, {bool thumbnail = true}) {
|
||||
return CacheInfo(
|
||||
cacheKey: artistArtCacheKey(source.id, artistId, thumbnail),
|
||||
cacheManager: imageCache,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cache_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$cacheServiceHash() => r'5e83011fbdfc5a962d43e3311b666dde2c455e24';
|
||||
|
||||
/// See also [cacheService].
|
||||
@ProviderFor(cacheService)
|
||||
final cacheServiceProvider = Provider<CacheService>.internal(
|
||||
cacheService,
|
||||
name: r'cacheServiceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$cacheServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef CacheServiceRef = ProviderRef<CacheService>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,589 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../http/client.dart';
|
||||
import '../models/music.dart';
|
||||
import '../models/query.dart';
|
||||
import '../models/support.dart';
|
||||
import '../state/music.dart';
|
||||
import '../state/settings.dart';
|
||||
import 'cache_service.dart';
|
||||
|
||||
part 'download_service.freezed.dart';
|
||||
part 'download_service.g.dart';
|
||||
|
||||
@freezed
|
||||
class DownloadState with _$DownloadState {
|
||||
const factory DownloadState({
|
||||
@Default(IListConst([])) IList<Download> downloads,
|
||||
@Default(IListConst([])) IList<SourceId> deletes,
|
||||
@Default(IListConst([])) IList<SourceId> listDownloads,
|
||||
@Default(IListConst([])) IList<SourceId> listCancels,
|
||||
required String saveDir,
|
||||
}) = _DownloadState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Download with _$Download {
|
||||
const Download._();
|
||||
|
||||
const factory Download({
|
||||
required String taskId,
|
||||
required DownloadTaskStatus status,
|
||||
required int progress,
|
||||
required String url,
|
||||
required String? filename,
|
||||
required String savedDir,
|
||||
required int timeCreated,
|
||||
required bool allowCellular,
|
||||
}) = _Download;
|
||||
|
||||
factory Download.fromTask(DownloadTask task) {
|
||||
return Download(
|
||||
taskId: task.taskId,
|
||||
status: task.status,
|
||||
progress: task.progress,
|
||||
url: task.url,
|
||||
filename: task.filename,
|
||||
savedDir: task.savedDir,
|
||||
timeCreated: task.timeCreated,
|
||||
allowCellular: task.allowCellular,
|
||||
);
|
||||
}
|
||||
|
||||
DownloadTask toTask() {
|
||||
return DownloadTask(
|
||||
taskId: taskId,
|
||||
status: status,
|
||||
progress: progress,
|
||||
url: url,
|
||||
filename: filename,
|
||||
savedDir: savedDir,
|
||||
timeCreated: timeCreated,
|
||||
allowCellular: allowCellular,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum SongDownloadState {
|
||||
none,
|
||||
inProgress,
|
||||
completed,
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class DownloadService extends _$DownloadService {
|
||||
static final ReceivePort _port = ReceivePort();
|
||||
|
||||
@override
|
||||
DownloadState build() {
|
||||
return const DownloadState(saveDir: '');
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
await FlutterDownloader.initialize(
|
||||
// debug: true,
|
||||
ignoreSsl: true,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
saveDir: path.join(
|
||||
(await getApplicationDocumentsDirectory()).path,
|
||||
'downloads',
|
||||
),
|
||||
);
|
||||
|
||||
_bindBackgroundIsolate();
|
||||
await _syncDownloadTasks();
|
||||
|
||||
FlutterDownloader.registerCallback(downloadCallback, step: 1);
|
||||
}
|
||||
|
||||
Future<void> downloadAlbum(Album album) async {
|
||||
return _downloadList(album, () async {
|
||||
final cache = ref.read(cacheServiceProvider);
|
||||
await Future.wait([
|
||||
_cacheArtistArt(album.sourceId, album.artistId, false),
|
||||
_cacheArtistArt(album.sourceId, album.artistId, true),
|
||||
_cacheImage(cache.albumArt(album, thumbnail: false)),
|
||||
_cacheImage(cache.albumArt(album, thumbnail: true)),
|
||||
]);
|
||||
if (_isCanceled(album)) return;
|
||||
|
||||
final songs = await _albumSongs(album, SongDownloadState.none);
|
||||
if (_isCanceled(album)) return;
|
||||
|
||||
for (var song in songs) {
|
||||
await _downloadSong(song);
|
||||
if (_isCanceled(album)) return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> downloadPlaylist(Playlist playlist) async {
|
||||
return _downloadList(playlist, () async {
|
||||
final songs = await _playlistSongs(playlist, SongDownloadState.none);
|
||||
if (_isCanceled(playlist)) return;
|
||||
|
||||
final albumIds = songs.map((e) => e.albumId).whereNotNull().toSet();
|
||||
final albums =
|
||||
await ref.read(albumsInIdsProvider(albumIds.toIList()).future);
|
||||
final artistIds = albums.map((e) => e.artistId).whereNotNull().toSet();
|
||||
|
||||
final cache = ref.read(cacheServiceProvider);
|
||||
await Future.wait([
|
||||
_cacheImage(cache.playlistArt(playlist, thumbnail: true)),
|
||||
_cacheImage(cache.playlistArt(playlist, thumbnail: false)),
|
||||
...albums.map((a) => _cacheImage(cache.albumArt(a, thumbnail: true))),
|
||||
...albums.map((a) => _cacheImage(cache.albumArt(a, thumbnail: false))),
|
||||
...artistIds.map(
|
||||
(artistId) => _cacheArtistArt(playlist.sourceId, artistId, true),
|
||||
),
|
||||
...artistIds.map(
|
||||
(artistId) => _cacheArtistArt(playlist.sourceId, artistId, false),
|
||||
),
|
||||
]);
|
||||
if (_isCanceled(playlist)) return;
|
||||
|
||||
for (var song in songs) {
|
||||
await _downloadSong(song);
|
||||
if (_isCanceled(playlist)) return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cancelAlbum(Album album) async {
|
||||
return _cancelList(album, () async {
|
||||
final songs = await _albumSongs(album, SongDownloadState.inProgress);
|
||||
for (var song in songs) {
|
||||
try {
|
||||
await FlutterDownloader.cancel(taskId: song.downloadTaskId!);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cancelPlaylist(Playlist playlist) async {
|
||||
return _cancelList(playlist, () async {
|
||||
final songs =
|
||||
await _playlistSongs(playlist, SongDownloadState.inProgress);
|
||||
for (var song in songs) {
|
||||
await FlutterDownloader.cancel(taskId: song.downloadTaskId!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteAlbum(Album album) async {
|
||||
return _deleteList(album, () async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final songs = await _albumSongs(album, SongDownloadState.completed);
|
||||
|
||||
for (var song in songs) {
|
||||
await _tryDeleteFile(song.downloadFilePath!);
|
||||
await db.deleteSongDownloadFile(song.sourceId, song.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deletePlaylist(Playlist playlist) async {
|
||||
return _deleteList(playlist, () async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final songs = await _playlistSongs(playlist, SongDownloadState.completed);
|
||||
|
||||
for (var song in songs) {
|
||||
if (await _tryDeleteFile(song.downloadFilePath!)) {
|
||||
await db.deleteSongDownloadFile(song.sourceId, song.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteAll(int sourceId) async {
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
final albumIds = await db.albumIdsWithDownloadStatus(sourceId).get();
|
||||
for (var id in albumIds) {
|
||||
await deleteAlbum(await (db.albumById(sourceId, id)).getSingle());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadList(
|
||||
SourceIdentifiable list,
|
||||
Future<void> Function() callback,
|
||||
) async {
|
||||
final sourceId = SourceId.from(list);
|
||||
if (state.listDownloads.contains(sourceId)) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(listDownloads: state.listDownloads.add(sourceId));
|
||||
|
||||
try {
|
||||
await callback();
|
||||
} finally {
|
||||
state = state.copyWith(
|
||||
listDownloads: state.listDownloads.remove(sourceId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cancelList(
|
||||
SourceIdentifiable list,
|
||||
Future<void> Function() callback,
|
||||
) async {
|
||||
final sourceId = SourceId.from(list);
|
||||
if (state.listCancels.contains(sourceId)) return;
|
||||
state = state.copyWith(
|
||||
listCancels: state.listCancels.add(sourceId),
|
||||
);
|
||||
|
||||
if (state.listDownloads.contains(sourceId)) {
|
||||
var tries = 0;
|
||||
while (tries < 60) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (!state.listDownloads.contains(sourceId)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await callback();
|
||||
} finally {
|
||||
state = state.copyWith(
|
||||
listCancels: state.listCancels.remove(sourceId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteList(
|
||||
SourceIdentifiable list,
|
||||
Future<void> Function() callback,
|
||||
) async {
|
||||
final sourceId = SourceId.from(list);
|
||||
if (state.deletes.contains(sourceId)) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(deletes: state.deletes.add(sourceId));
|
||||
|
||||
try {
|
||||
await callback();
|
||||
} finally {
|
||||
state = state.copyWith(deletes: state.deletes.remove(sourceId));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadSong(Song song) async {
|
||||
if (song.downloadFilePath != null || song.downloadTaskId != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final source = ref.read(musicSourceProvider);
|
||||
final db = ref.read(databaseProvider);
|
||||
final http = ref.read(httpClientProvider);
|
||||
|
||||
final uri = source.downloadUri(song.id);
|
||||
final head = await http.head(uri);
|
||||
|
||||
final contentType = head.headers['content-type'];
|
||||
if (contentType == null) {
|
||||
throw StateError('Bad HTTP response from HEAD during download');
|
||||
}
|
||||
|
||||
final mime = contentType.split(';').first.toLowerCase();
|
||||
if (!mime.startsWith('audio') && !mime.startsWith('application')) {
|
||||
throw StateError('Download error: MIME-type $mime is not audio');
|
||||
}
|
||||
|
||||
String? ext = extensionFromMime(mime);
|
||||
if (ext == mime) {
|
||||
ext = null;
|
||||
}
|
||||
|
||||
final saveDir = Directory(
|
||||
path.join(state.saveDir, song.sourceId.toString()),
|
||||
);
|
||||
await saveDir.create(recursive: true);
|
||||
|
||||
final taskId = await FlutterDownloader.enqueue(
|
||||
url: source.downloadUri(song.id).toString(),
|
||||
savedDir: saveDir.path,
|
||||
fileName: ext != null ? '${song.id}.$ext' : song.id,
|
||||
headers: subtracksHeaders,
|
||||
openFileFromNotification: false,
|
||||
showNotification: false,
|
||||
);
|
||||
await db.updateSongDownloadTask(taskId, song.sourceId, song.id);
|
||||
}
|
||||
|
||||
Future<void> _cacheImage(UriCacheInfo cache) async {
|
||||
final cachedFile = await cache.cacheManager.getFileFromCache(
|
||||
cache.cacheKey,
|
||||
ignoreMemCache: true,
|
||||
);
|
||||
if (cachedFile == null) {
|
||||
try {
|
||||
await cache.cacheManager.getSingleFile(
|
||||
cache.uri.toString(),
|
||||
key: cache.cacheKey,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cacheArtistArt(
|
||||
int sourceId,
|
||||
String? artistId,
|
||||
bool thumbnail,
|
||||
) async {
|
||||
if (artistId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final cache = ref.read(cacheServiceProvider);
|
||||
try {
|
||||
final uri = await cache.artistArtUri(artistId, thumbnail: thumbnail);
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _cacheImage(UriCacheInfo(
|
||||
uri: uri,
|
||||
cacheKey:
|
||||
cache.artistArtCacheInfo(artistId, thumbnail: thumbnail).cacheKey,
|
||||
cacheManager: cache.imageCache,
|
||||
));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
bool _isCanceled(SourceIdentifiable item) {
|
||||
return state.listCancels.contains(SourceId.from(item));
|
||||
}
|
||||
|
||||
List<FilterWith> _downloadFilters(SongDownloadState state) {
|
||||
switch (state) {
|
||||
case SongDownloadState.none:
|
||||
return [
|
||||
const FilterWith.isNull(column: 'download_task_id'),
|
||||
const FilterWith.isNull(column: 'download_file_path'),
|
||||
];
|
||||
case SongDownloadState.completed:
|
||||
return [
|
||||
const FilterWith.isNull(column: 'download_file_path', invert: true),
|
||||
];
|
||||
case SongDownloadState.inProgress:
|
||||
return [
|
||||
const FilterWith.isNull(column: 'download_task_id', invert: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Song>> _albumSongs(
|
||||
Album album,
|
||||
SongDownloadState state,
|
||||
) {
|
||||
return ref
|
||||
.read(databaseProvider)
|
||||
.albumSongsList(
|
||||
SourceId.from(album),
|
||||
ListQuery(
|
||||
sort: const SortBy(column: 'disc, track'),
|
||||
filters: _downloadFilters(state).lock,
|
||||
),
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<Song>> _playlistSongs(
|
||||
Playlist playlist,
|
||||
SongDownloadState state,
|
||||
) {
|
||||
return ref
|
||||
.read(databaseProvider)
|
||||
.playlistSongsList(
|
||||
SourceId.from(playlist),
|
||||
ListQuery(
|
||||
sort: const SortBy(column: 'playlist_songs.position'),
|
||||
filters: _downloadFilters(state).lock,
|
||||
),
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<void> _syncDownloadTasks() async {
|
||||
final tasks = await FlutterDownloader.loadTasks() ?? [];
|
||||
final downloads = tasks.map((e) => Download.fromTask(e)).toIList();
|
||||
|
||||
state = state.copyWith(downloads: downloads);
|
||||
|
||||
final db = ref.read(databaseProvider);
|
||||
final songs = await db.songsWithDownloadTasks().get();
|
||||
|
||||
await _deleteTasksNotIn(songs.map((e) => e.downloadTaskId!).toList());
|
||||
|
||||
final deleteTaskStatus = [
|
||||
DownloadTaskStatus.canceled,
|
||||
DownloadTaskStatus.failed,
|
||||
DownloadTaskStatus.undefined,
|
||||
];
|
||||
|
||||
for (var song in songs) {
|
||||
final download = downloads.firstWhereOrNull(
|
||||
(t) => t.taskId == song.downloadTaskId,
|
||||
);
|
||||
|
||||
if (download == null) {
|
||||
await db.clearSongDownloadTaskBySong(song.sourceId, song.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deleteTaskStatus.anyIs(download.status)) {
|
||||
await _clearFailedDownload(download);
|
||||
} else if (download.status == DownloadTaskStatus.complete) {
|
||||
await _completeDownload(download);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _tryDeleteFile(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
await file.delete();
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteTasksNotIn(List<String> taskIds) async {
|
||||
if (taskIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: 'DELETE FROM task WHERE task_id NOT IN '
|
||||
'(${taskIds.map((e) => "'$e'").join(',')})',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteTask(String taskId) async {
|
||||
await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: 'DELETE FROM task WHERE task_id = \'$taskId\'',
|
||||
);
|
||||
}
|
||||
|
||||
Future<DownloadTask?> _getTask(String taskId) async {
|
||||
return (await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: 'SELECT * FROM task WHERE task_id = \'$taskId\'',
|
||||
))
|
||||
?.firstOrNull;
|
||||
}
|
||||
|
||||
Future<void> _completeDownload(Download download) async {
|
||||
final db = ref.read(databaseProvider);
|
||||
await db.completeSongDownload(
|
||||
path.join(download.savedDir, download.filename),
|
||||
download.taskId,
|
||||
);
|
||||
await _deleteTask(download.taskId);
|
||||
|
||||
state = state.copyWith(
|
||||
downloads: state.downloads.removeWhere(
|
||||
(d) => d.taskId == download.taskId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _clearFailedDownload(Download download) async {
|
||||
final db = ref.read(databaseProvider);
|
||||
await db.clearSongDownloadTask(download.taskId);
|
||||
await _deleteTask(download.taskId);
|
||||
await _tryDeleteFile(path.join(download.savedDir, download.filename));
|
||||
|
||||
state = state.copyWith(
|
||||
downloads: state.downloads.removeWhere(
|
||||
(d) => d.taskId == download.taskId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _bindBackgroundIsolate([retry = 0]) {
|
||||
final isSuccess = IsolateNameServer.registerPortWithName(
|
||||
_port.sendPort,
|
||||
'downloader_send_port',
|
||||
);
|
||||
|
||||
if (!isSuccess && retry < 100) {
|
||||
_unbindBackgroundIsolate();
|
||||
_bindBackgroundIsolate(retry + 1);
|
||||
return;
|
||||
} else if (retry >= 100) {
|
||||
throw StateError('Could not bind background isolate for downloads');
|
||||
}
|
||||
|
||||
_port.asyncMap((dynamic data) async {
|
||||
final taskId = (data as List<dynamic>)[0] as String;
|
||||
final status = DownloadTaskStatus(data[1] as int);
|
||||
final progress = data[2] as int;
|
||||
|
||||
var download = state.downloads.firstWhereOrNull(
|
||||
(task) => task.taskId == taskId,
|
||||
);
|
||||
if (download == null) {
|
||||
final task = await _getTask(taskId);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
download = Download.fromTask(task);
|
||||
}
|
||||
|
||||
download = download.copyWith(status: status, progress: progress);
|
||||
|
||||
state = state.copyWith(
|
||||
downloads: state.downloads.replaceFirstWhere(
|
||||
(d) => d.taskId == taskId,
|
||||
(d) => download!,
|
||||
addIfNotFound: true,
|
||||
),
|
||||
);
|
||||
|
||||
final deleteTaskStatus = [
|
||||
DownloadTaskStatus.canceled,
|
||||
DownloadTaskStatus.failed,
|
||||
DownloadTaskStatus.undefined,
|
||||
];
|
||||
|
||||
if (status == DownloadTaskStatus.complete) {
|
||||
await _completeDownload(download);
|
||||
} else if (deleteTaskStatus.anyIs(status)) {
|
||||
await _clearFailedDownload(download);
|
||||
}
|
||||
}).listen((_) {});
|
||||
}
|
||||
|
||||
void _unbindBackgroundIsolate() {
|
||||
IsolateNameServer.removePortNameMapping('downloader_send_port');
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
static void downloadCallback(
|
||||
String id,
|
||||
DownloadTaskStatus status,
|
||||
int progress,
|
||||
) {
|
||||
IsolateNameServer.lookupPortByName('downloader_send_port')?.send(
|
||||
[id, status.value, progress],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'download_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$DownloadState {
|
||||
IList<Download> get downloads => throw _privateConstructorUsedError;
|
||||
IList<SourceId> get deletes => throw _privateConstructorUsedError;
|
||||
IList<SourceId> get listDownloads => throw _privateConstructorUsedError;
|
||||
IList<SourceId> get listCancels => throw _privateConstructorUsedError;
|
||||
String get saveDir => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$DownloadStateCopyWith<DownloadState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $DownloadStateCopyWith<$Res> {
|
||||
factory $DownloadStateCopyWith(
|
||||
DownloadState value, $Res Function(DownloadState) then) =
|
||||
_$DownloadStateCopyWithImpl<$Res, DownloadState>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{IList<Download> downloads,
|
||||
IList<SourceId> deletes,
|
||||
IList<SourceId> listDownloads,
|
||||
IList<SourceId> listCancels,
|
||||
String saveDir});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$DownloadStateCopyWithImpl<$Res, $Val extends DownloadState>
|
||||
implements $DownloadStateCopyWith<$Res> {
|
||||
_$DownloadStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? downloads = null,
|
||||
Object? deletes = null,
|
||||
Object? listDownloads = null,
|
||||
Object? listCancels = null,
|
||||
Object? saveDir = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
downloads: null == downloads
|
||||
? _value.downloads
|
||||
: downloads // ignore: cast_nullable_to_non_nullable
|
||||
as IList<Download>,
|
||||
deletes: null == deletes
|
||||
? _value.deletes
|
||||
: deletes // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceId>,
|
||||
listDownloads: null == listDownloads
|
||||
? _value.listDownloads
|
||||
: listDownloads // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceId>,
|
||||
listCancels: null == listCancels
|
||||
? _value.listCancels
|
||||
: listCancels // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceId>,
|
||||
saveDir: null == saveDir
|
||||
? _value.saveDir
|
||||
: saveDir // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_DownloadStateCopyWith<$Res>
|
||||
implements $DownloadStateCopyWith<$Res> {
|
||||
factory _$$_DownloadStateCopyWith(
|
||||
_$_DownloadState value, $Res Function(_$_DownloadState) then) =
|
||||
__$$_DownloadStateCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{IList<Download> downloads,
|
||||
IList<SourceId> deletes,
|
||||
IList<SourceId> listDownloads,
|
||||
IList<SourceId> listCancels,
|
||||
String saveDir});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_DownloadStateCopyWithImpl<$Res>
|
||||
extends _$DownloadStateCopyWithImpl<$Res, _$_DownloadState>
|
||||
implements _$$_DownloadStateCopyWith<$Res> {
|
||||
__$$_DownloadStateCopyWithImpl(
|
||||
_$_DownloadState _value, $Res Function(_$_DownloadState) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? downloads = null,
|
||||
Object? deletes = null,
|
||||
Object? listDownloads = null,
|
||||
Object? listCancels = null,
|
||||
Object? saveDir = null,
|
||||
}) {
|
||||
return _then(_$_DownloadState(
|
||||
downloads: null == downloads
|
||||
? _value.downloads
|
||||
: downloads // ignore: cast_nullable_to_non_nullable
|
||||
as IList<Download>,
|
||||
deletes: null == deletes
|
||||
? _value.deletes
|
||||
: deletes // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceId>,
|
||||
listDownloads: null == listDownloads
|
||||
? _value.listDownloads
|
||||
: listDownloads // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceId>,
|
||||
listCancels: null == listCancels
|
||||
? _value.listCancels
|
||||
: listCancels // ignore: cast_nullable_to_non_nullable
|
||||
as IList<SourceId>,
|
||||
saveDir: null == saveDir
|
||||
? _value.saveDir
|
||||
: saveDir // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_DownloadState implements _DownloadState {
|
||||
const _$_DownloadState(
|
||||
{this.downloads = const IListConst([]),
|
||||
this.deletes = const IListConst([]),
|
||||
this.listDownloads = const IListConst([]),
|
||||
this.listCancels = const IListConst([]),
|
||||
required this.saveDir});
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final IList<Download> downloads;
|
||||
@override
|
||||
@JsonKey()
|
||||
final IList<SourceId> deletes;
|
||||
@override
|
||||
@JsonKey()
|
||||
final IList<SourceId> listDownloads;
|
||||
@override
|
||||
@JsonKey()
|
||||
final IList<SourceId> listCancels;
|
||||
@override
|
||||
final String saveDir;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DownloadState(downloads: $downloads, deletes: $deletes, listDownloads: $listDownloads, listCancels: $listCancels, saveDir: $saveDir)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_DownloadState &&
|
||||
const DeepCollectionEquality().equals(other.downloads, downloads) &&
|
||||
const DeepCollectionEquality().equals(other.deletes, deletes) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.listDownloads, listDownloads) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.listCancels, listCancels) &&
|
||||
(identical(other.saveDir, saveDir) || other.saveDir == saveDir));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(downloads),
|
||||
const DeepCollectionEquality().hash(deletes),
|
||||
const DeepCollectionEquality().hash(listDownloads),
|
||||
const DeepCollectionEquality().hash(listCancels),
|
||||
saveDir);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_DownloadStateCopyWith<_$_DownloadState> get copyWith =>
|
||||
__$$_DownloadStateCopyWithImpl<_$_DownloadState>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _DownloadState implements DownloadState {
|
||||
const factory _DownloadState(
|
||||
{final IList<Download> downloads,
|
||||
final IList<SourceId> deletes,
|
||||
final IList<SourceId> listDownloads,
|
||||
final IList<SourceId> listCancels,
|
||||
required final String saveDir}) = _$_DownloadState;
|
||||
|
||||
@override
|
||||
IList<Download> get downloads;
|
||||
@override
|
||||
IList<SourceId> get deletes;
|
||||
@override
|
||||
IList<SourceId> get listDownloads;
|
||||
@override
|
||||
IList<SourceId> get listCancels;
|
||||
@override
|
||||
String get saveDir;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_DownloadStateCopyWith<_$_DownloadState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Download {
|
||||
String get taskId => throw _privateConstructorUsedError;
|
||||
DownloadTaskStatus get status => throw _privateConstructorUsedError;
|
||||
int get progress => throw _privateConstructorUsedError;
|
||||
String get url => throw _privateConstructorUsedError;
|
||||
String? get filename => throw _privateConstructorUsedError;
|
||||
String get savedDir => throw _privateConstructorUsedError;
|
||||
int get timeCreated => throw _privateConstructorUsedError;
|
||||
bool get allowCellular => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$DownloadCopyWith<Download> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $DownloadCopyWith<$Res> {
|
||||
factory $DownloadCopyWith(Download value, $Res Function(Download) then) =
|
||||
_$DownloadCopyWithImpl<$Res, Download>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String taskId,
|
||||
DownloadTaskStatus status,
|
||||
int progress,
|
||||
String url,
|
||||
String? filename,
|
||||
String savedDir,
|
||||
int timeCreated,
|
||||
bool allowCellular});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$DownloadCopyWithImpl<$Res, $Val extends Download>
|
||||
implements $DownloadCopyWith<$Res> {
|
||||
_$DownloadCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? taskId = null,
|
||||
Object? status = null,
|
||||
Object? progress = null,
|
||||
Object? url = null,
|
||||
Object? filename = freezed,
|
||||
Object? savedDir = null,
|
||||
Object? timeCreated = null,
|
||||
Object? allowCellular = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
taskId: null == taskId
|
||||
? _value.taskId
|
||||
: taskId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
status: null == status
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as DownloadTaskStatus,
|
||||
progress: null == progress
|
||||
? _value.progress
|
||||
: progress // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
filename: freezed == filename
|
||||
? _value.filename
|
||||
: filename // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
savedDir: null == savedDir
|
||||
? _value.savedDir
|
||||
: savedDir // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
timeCreated: null == timeCreated
|
||||
? _value.timeCreated
|
||||
: timeCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
allowCellular: null == allowCellular
|
||||
? _value.allowCellular
|
||||
: allowCellular // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_DownloadCopyWith<$Res> implements $DownloadCopyWith<$Res> {
|
||||
factory _$$_DownloadCopyWith(
|
||||
_$_Download value, $Res Function(_$_Download) then) =
|
||||
__$$_DownloadCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String taskId,
|
||||
DownloadTaskStatus status,
|
||||
int progress,
|
||||
String url,
|
||||
String? filename,
|
||||
String savedDir,
|
||||
int timeCreated,
|
||||
bool allowCellular});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_DownloadCopyWithImpl<$Res>
|
||||
extends _$DownloadCopyWithImpl<$Res, _$_Download>
|
||||
implements _$$_DownloadCopyWith<$Res> {
|
||||
__$$_DownloadCopyWithImpl(
|
||||
_$_Download _value, $Res Function(_$_Download) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? taskId = null,
|
||||
Object? status = null,
|
||||
Object? progress = null,
|
||||
Object? url = null,
|
||||
Object? filename = freezed,
|
||||
Object? savedDir = null,
|
||||
Object? timeCreated = null,
|
||||
Object? allowCellular = null,
|
||||
}) {
|
||||
return _then(_$_Download(
|
||||
taskId: null == taskId
|
||||
? _value.taskId
|
||||
: taskId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
status: null == status
|
||||
? _value.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as DownloadTaskStatus,
|
||||
progress: null == progress
|
||||
? _value.progress
|
||||
: progress // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
url: null == url
|
||||
? _value.url
|
||||
: url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
filename: freezed == filename
|
||||
? _value.filename
|
||||
: filename // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
savedDir: null == savedDir
|
||||
? _value.savedDir
|
||||
: savedDir // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
timeCreated: null == timeCreated
|
||||
? _value.timeCreated
|
||||
: timeCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
allowCellular: null == allowCellular
|
||||
? _value.allowCellular
|
||||
: allowCellular // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_Download extends _Download {
|
||||
const _$_Download(
|
||||
{required this.taskId,
|
||||
required this.status,
|
||||
required this.progress,
|
||||
required this.url,
|
||||
required this.filename,
|
||||
required this.savedDir,
|
||||
required this.timeCreated,
|
||||
required this.allowCellular})
|
||||
: super._();
|
||||
|
||||
@override
|
||||
final String taskId;
|
||||
@override
|
||||
final DownloadTaskStatus status;
|
||||
@override
|
||||
final int progress;
|
||||
@override
|
||||
final String url;
|
||||
@override
|
||||
final String? filename;
|
||||
@override
|
||||
final String savedDir;
|
||||
@override
|
||||
final int timeCreated;
|
||||
@override
|
||||
final bool allowCellular;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Download(taskId: $taskId, status: $status, progress: $progress, url: $url, filename: $filename, savedDir: $savedDir, timeCreated: $timeCreated, allowCellular: $allowCellular)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_Download &&
|
||||
(identical(other.taskId, taskId) || other.taskId == taskId) &&
|
||||
(identical(other.status, status) || other.status == status) &&
|
||||
(identical(other.progress, progress) ||
|
||||
other.progress == progress) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.filename, filename) ||
|
||||
other.filename == filename) &&
|
||||
(identical(other.savedDir, savedDir) ||
|
||||
other.savedDir == savedDir) &&
|
||||
(identical(other.timeCreated, timeCreated) ||
|
||||
other.timeCreated == timeCreated) &&
|
||||
(identical(other.allowCellular, allowCellular) ||
|
||||
other.allowCellular == allowCellular));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, taskId, status, progress, url,
|
||||
filename, savedDir, timeCreated, allowCellular);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$_DownloadCopyWith<_$_Download> get copyWith =>
|
||||
__$$_DownloadCopyWithImpl<_$_Download>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _Download extends Download {
|
||||
const factory _Download(
|
||||
{required final String taskId,
|
||||
required final DownloadTaskStatus status,
|
||||
required final int progress,
|
||||
required final String url,
|
||||
required final String? filename,
|
||||
required final String savedDir,
|
||||
required final int timeCreated,
|
||||
required final bool allowCellular}) = _$_Download;
|
||||
const _Download._() : super._();
|
||||
|
||||
@override
|
||||
String get taskId;
|
||||
@override
|
||||
DownloadTaskStatus get status;
|
||||
@override
|
||||
int get progress;
|
||||
@override
|
||||
String get url;
|
||||
@override
|
||||
String? get filename;
|
||||
@override
|
||||
String get savedDir;
|
||||
@override
|
||||
int get timeCreated;
|
||||
@override
|
||||
bool get allowCellular;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_DownloadCopyWith<_$_Download> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'download_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$downloadServiceHash() => r'c72c49f980e307f3013467e76b6564d14a34a736';
|
||||
|
||||
/// See also [DownloadService].
|
||||
@ProviderFor(DownloadService)
|
||||
final downloadServiceProvider =
|
||||
NotifierProvider<DownloadService, DownloadState>.internal(
|
||||
DownloadService.new,
|
||||
name: r'downloadServiceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$downloadServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$DownloadService = Notifier<DownloadState>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,129 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../http/client.dart';
|
||||
import '../models/settings.dart';
|
||||
import '../sources/subsonic/client.dart';
|
||||
import '../state/init.dart';
|
||||
import 'download_service.dart';
|
||||
|
||||
part 'settings_service.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SettingsService extends _$SettingsService {
|
||||
SubtracksDatabase get _db => ref.read(databaseProvider);
|
||||
|
||||
@override
|
||||
Settings build() {
|
||||
return const Settings();
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final sources = await _db.allSubsonicSources().get();
|
||||
final settings = await _db.getAppSettings().getSingleOrNull();
|
||||
|
||||
state = Settings(
|
||||
sources: sources
|
||||
.sorted((a, b) => a.createdAt.compareTo(b.createdAt))
|
||||
.toIList(),
|
||||
activeSource: sources.singleWhereOrNull((e) => e.isActive == true),
|
||||
app: settings ?? const AppSettings(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> createSource(
|
||||
SourcesCompanion source,
|
||||
SubsonicSourcesCompanion subsonic,
|
||||
) async {
|
||||
final client = SubsonicClient(
|
||||
SubsonicSettings(
|
||||
id: 1,
|
||||
name: source.name.value,
|
||||
address: source.address.value,
|
||||
features: IList(),
|
||||
username: subsonic.username.value,
|
||||
password: subsonic.password.value,
|
||||
useTokenAuth: subsonic.useTokenAuth.value,
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
ref.read(httpClientProvider),
|
||||
);
|
||||
|
||||
await client.test();
|
||||
|
||||
final features = IList([
|
||||
if (await client.testFeature(SubsonicFeature.emptyQuerySearch))
|
||||
SubsonicFeature.emptyQuerySearch,
|
||||
]);
|
||||
|
||||
await _db.createSource(
|
||||
source,
|
||||
subsonic.copyWith(features: Value(features)),
|
||||
);
|
||||
await init();
|
||||
}
|
||||
|
||||
Future<void> updateSource(SubsonicSettings source) async {
|
||||
final client = SubsonicClient(source, ref.read(httpClientProvider));
|
||||
|
||||
await client.test();
|
||||
|
||||
await _db.updateSource(source);
|
||||
await init();
|
||||
}
|
||||
|
||||
Future<void> deleteSource(int sourceId) async {
|
||||
await ref.read(downloadServiceProvider.notifier).deleteAll(sourceId);
|
||||
await _db.deleteSource(sourceId);
|
||||
await init();
|
||||
}
|
||||
|
||||
Future<void> setActiveSource(int id) async {
|
||||
await _db.setActiveSource(id);
|
||||
await init();
|
||||
}
|
||||
|
||||
Future<void> addTestSource(String prefix) async {
|
||||
final env = ref.read(envProvider).requireValue;
|
||||
|
||||
await createSource(
|
||||
SourcesCompanion.insert(
|
||||
name: env['${prefix}_SERVER_NAME']!,
|
||||
address: Uri.parse(env['${prefix}_SERVER_URL']!),
|
||||
),
|
||||
SubsonicSourcesCompanion.insert(
|
||||
features: IList(),
|
||||
username: env['${prefix}_SERVER_USERNAME']!,
|
||||
password: env['${prefix}_SERVER_PASSWORD']!,
|
||||
useTokenAuth: const Value(true),
|
||||
),
|
||||
);
|
||||
|
||||
await init();
|
||||
}
|
||||
|
||||
Future<void> setMaxBitrateWifi(int bitrate) async {
|
||||
await _db.updateSettings(
|
||||
state.app.copyWith(maxBitrateWifi: bitrate).toCompanion(),
|
||||
);
|
||||
await init();
|
||||
}
|
||||
|
||||
Future<void> setMaxBitrateMobile(int bitrate) async {
|
||||
await _db.updateSettings(
|
||||
state.app.copyWith(maxBitrateMobile: bitrate).toCompanion(),
|
||||
);
|
||||
await init();
|
||||
}
|
||||
|
||||
Future<void> setStreamFormat(String? streamFormat) async {
|
||||
await _db.updateSettings(
|
||||
state.app.copyWith(streamFormat: streamFormat).toCompanion(),
|
||||
);
|
||||
await init();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'settings_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$settingsServiceHash() => r'85f2bd5eedc3f791fe03a6707748bc95277c6aaf';
|
||||
|
||||
/// See also [SettingsService].
|
||||
@ProviderFor(SettingsService)
|
||||
final settingsServiceProvider =
|
||||
NotifierProvider<SettingsService, Settings>.internal(
|
||||
SettingsService.new,
|
||||
name: r'settingsServiceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$settingsServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SettingsService = Notifier<Settings>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,105 +0,0 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../state/settings.dart';
|
||||
|
||||
part 'sync_service.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SyncService extends _$SyncService {
|
||||
@override
|
||||
DateTime build() {
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
Future<void> syncAll() async {
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
await db.transaction(() async {
|
||||
await Future.wait([
|
||||
db.transaction(_syncAllArtists),
|
||||
db.transaction(_syncAllAlbums),
|
||||
db.transaction(_syncAllPlaylists),
|
||||
db.transaction(_syncAllSongs),
|
||||
]);
|
||||
});
|
||||
|
||||
state = DateTime.now();
|
||||
}
|
||||
|
||||
Future<void> _syncAllArtists() async {
|
||||
final source = ref.read(musicSourceProvider);
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
final ids = <String>{};
|
||||
await for (var artists in source.allArtists()) {
|
||||
ids.addAll(artists.map((e) => e.id.value));
|
||||
await db.saveArtists(artists);
|
||||
}
|
||||
|
||||
await db.deleteArtistsNotIn(source.id, ids);
|
||||
}
|
||||
|
||||
Future<void> _syncAllAlbums() async {
|
||||
final source = ref.read(musicSourceProvider);
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
final ids = <String>{};
|
||||
await for (var albums in source.allAlbums()) {
|
||||
ids.addAll(albums.map((e) => e.id.value));
|
||||
await db.saveAlbums(albums);
|
||||
}
|
||||
|
||||
await db.deleteAlbumsNotIn(source.id, ids);
|
||||
}
|
||||
|
||||
Future<void> _syncAllPlaylists() async {
|
||||
final source = ref.read(musicSourceProvider);
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
final ids = <String>{};
|
||||
await for (var playlists in source.allPlaylists()) {
|
||||
ids.addAll(playlists.map((e) => e.playist.id.value));
|
||||
await db.savePlaylists(playlists);
|
||||
}
|
||||
|
||||
await db.deletePlaylistsNotIn(source.id, ids);
|
||||
}
|
||||
|
||||
Future<void> _syncAllSongs() async {
|
||||
final source = ref.read(musicSourceProvider);
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
final ids = <String>{};
|
||||
await for (var songs in source.allSongs()) {
|
||||
ids.addAll(songs.map((e) => e.id.value));
|
||||
await db.saveSongs(songs);
|
||||
}
|
||||
|
||||
await db.deleteSongsNotIn(source.id, ids);
|
||||
}
|
||||
|
||||
// Future<void> syncArtist(String id) async {
|
||||
// final source = ref.read(musicSourceProvider);
|
||||
// final db = ref.read(databaseProvider);
|
||||
|
||||
// final artist = await source.artist(id);
|
||||
// await saveArtist(db, artist);
|
||||
// }
|
||||
|
||||
// Future<void> syncAlbum(String id) async {
|
||||
// final source = ref.read(musicSourceProvider);
|
||||
// final db = ref.read(databaseProvider);
|
||||
|
||||
// final album = await source.album(id);
|
||||
// await saveAlbum(db, album);
|
||||
// }
|
||||
|
||||
// Future<void> syncPlaylist(String id) async {
|
||||
// final source = ref.read(musicSourceProvider);
|
||||
// final db = ref.read(databaseProvider);
|
||||
|
||||
// final playlist = await source.playlist(id);
|
||||
// await savePlaylist(db, playlist);
|
||||
// }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sync_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$syncServiceHash() => r'58ebee4e6f055b64ee6789ae43d63c0e15c679e0';
|
||||
|
||||
/// See also [SyncService].
|
||||
@ProviderFor(SyncService)
|
||||
final syncServiceProvider = NotifierProvider<SyncService, DateTime>.internal(
|
||||
SyncService.new,
|
||||
name: r'syncServiceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$syncServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SyncService = Notifier<DateTime>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,95 +0,0 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../log.dart';
|
||||
import '../state/settings.dart';
|
||||
|
||||
abstract class BaseMusicSource {
|
||||
int get id;
|
||||
|
||||
Future<void> ping();
|
||||
|
||||
Stream<Iterable<AlbumsCompanion>> allAlbums();
|
||||
Stream<Iterable<ArtistsCompanion>> allArtists();
|
||||
Stream<Iterable<PlaylistWithSongsCompanion>> allPlaylists();
|
||||
Stream<Iterable<SongsCompanion>> allSongs();
|
||||
|
||||
Uri streamUri(String songId);
|
||||
Uri downloadUri(String songId);
|
||||
|
||||
Uri coverArtUri(String coverArtId, {bool thumbnail = true});
|
||||
Future<Uri?> artistArtUri(String artistId, {bool thumbnail = true});
|
||||
}
|
||||
|
||||
class OfflineException implements Exception {
|
||||
@override
|
||||
String toString() => 'OfflineException';
|
||||
}
|
||||
|
||||
class MusicSource implements BaseMusicSource {
|
||||
final BaseMusicSource _source;
|
||||
final Ref _ref;
|
||||
|
||||
const MusicSource(this._source, this._ref);
|
||||
|
||||
void _testOnline() {
|
||||
if (_ref.read(offlineModeProvider)) {
|
||||
throw OfflineException();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<AlbumsCompanion>> allAlbums() {
|
||||
_testOnline();
|
||||
return _source
|
||||
.allAlbums()
|
||||
.doOnError((e, st) => log.severe('allAlbums', e, st));
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<ArtistsCompanion>> allArtists() {
|
||||
_testOnline();
|
||||
return _source
|
||||
.allArtists()
|
||||
.doOnError((e, st) => log.severe('allArtists', e, st));
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<PlaylistWithSongsCompanion>> allPlaylists() {
|
||||
_testOnline();
|
||||
return _source
|
||||
.allPlaylists()
|
||||
.doOnError((e, st) => log.severe('allPlaylists', e, st));
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<SongsCompanion>> allSongs() {
|
||||
_testOnline();
|
||||
return _source
|
||||
.allSongs()
|
||||
.doOnError((e, st) => log.severe('allSongs', e, st));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uri?> artistArtUri(String artistId, {bool thumbnail = true}) {
|
||||
_testOnline();
|
||||
return _source.artistArtUri(artistId, thumbnail: thumbnail);
|
||||
}
|
||||
|
||||
@override
|
||||
Uri coverArtUri(String coverArtId, {bool thumbnail = true}) =>
|
||||
_source.coverArtUri(coverArtId, thumbnail: thumbnail);
|
||||
|
||||
@override
|
||||
Uri downloadUri(String songId) => _source.downloadUri(songId);
|
||||
|
||||
@override
|
||||
int get id => _source.id;
|
||||
|
||||
@override
|
||||
Future<void> ping() => _source.ping();
|
||||
|
||||
@override
|
||||
Uri streamUri(String songId) => _source.streamUri(songId);
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../../log.dart';
|
||||
import '../../models/settings.dart';
|
||||
import 'xml.dart';
|
||||
|
||||
class SubsonicException implements Exception {
|
||||
final XmlElement xml;
|
||||
|
||||
late int code;
|
||||
late String message;
|
||||
|
||||
SubsonicException(this.xml) {
|
||||
try {
|
||||
final error = xml.getElement('error')!;
|
||||
code = int.parse(error.getAttribute('code')!);
|
||||
message = error.getAttribute('message')!;
|
||||
} catch (err) {
|
||||
code = -1;
|
||||
message = 'Unknown error.';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'SubsonicException [$code]: $message';
|
||||
}
|
||||
|
||||
class SubsonicClient {
|
||||
final SubsonicSettings opt;
|
||||
final BaseClient http;
|
||||
|
||||
SubsonicClient(this.opt, this.http);
|
||||
|
||||
String _salt() {
|
||||
final r = Random();
|
||||
return String.fromCharCodes(
|
||||
List.generate(4, (index) => r.nextInt(92) + 33),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _params() {
|
||||
final Map<String, String> p = {};
|
||||
p['v'] = '1.13.0';
|
||||
p['c'] = 'subtracks';
|
||||
p['u'] = opt.username;
|
||||
|
||||
if (opt.useTokenAuth) {
|
||||
p['s'] = _salt();
|
||||
p['t'] = md5.convert(utf8.encode(opt.password + p['s']!)).toString();
|
||||
} else {
|
||||
p['p'] = opt.password;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
Uri uri(
|
||||
String method, [
|
||||
Map<String, String?>? extraParams,
|
||||
]) {
|
||||
final pathSegments = [...opt.address.pathSegments, 'rest', '$method.view'];
|
||||
|
||||
_removeIdPrefix(extraParams);
|
||||
extraParams?.removeWhere((key, value) => value == null);
|
||||
final queryParameters = {
|
||||
..._params(),
|
||||
...(extraParams ?? {}),
|
||||
};
|
||||
|
||||
return Uri(
|
||||
scheme: opt.address.scheme,
|
||||
host: opt.address.host,
|
||||
port: opt.address.hasPort ? opt.address.port : null,
|
||||
pathSegments: pathSegments,
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SubsonicResponse> get(
|
||||
String method, [
|
||||
Map<String, String?>? extraParams,
|
||||
]) async {
|
||||
final res = await http.get(uri(method, extraParams));
|
||||
final subsonicResponse =
|
||||
SubsonicResponse(XmlDocument.parse(utf8.decode(res.bodyBytes)));
|
||||
if (subsonicResponse.status == Status.failed) {
|
||||
final error = SubsonicException(subsonicResponse.xml);
|
||||
log.severe('Subsonic error', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return subsonicResponse;
|
||||
}
|
||||
|
||||
Future<void> test() => get('ping');
|
||||
|
||||
Future<bool> testFeature(SubsonicFeature feature) async {
|
||||
switch (feature) {
|
||||
case SubsonicFeature.emptyQuerySearch:
|
||||
final res = await get(
|
||||
'search3',
|
||||
{'query': '""', 'songCount': '1'},
|
||||
);
|
||||
return res.xml.findAllElements('song').isNotEmpty;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static const _idsWithPrefix = {
|
||||
'id',
|
||||
'playlistId',
|
||||
'songIdToAdd',
|
||||
'albumId',
|
||||
'artistId',
|
||||
};
|
||||
|
||||
static const _idPrefixMatch =
|
||||
r'(artist\.|album\.|playlist\.|song\.|coverArt\.)';
|
||||
|
||||
void _removeIdPrefix(Map<String, String?>? params) {
|
||||
if (params == null) return;
|
||||
|
||||
for (var key in params.keys) {
|
||||
if (!_idsWithPrefix.contains(key)) continue;
|
||||
if (params[key] == null) continue;
|
||||
|
||||
final hasPrefix = params[key]!.startsWith(RegExp(_idPrefixMatch));
|
||||
if (!hasPrefix) continue;
|
||||
|
||||
params[key] = params[key]?.split('.').slice(1).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:http/http.dart';
|
||||
import 'package:pool/pool.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
import 'package:xml/xml_events.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../database/util.dart';
|
||||
import '../../models/settings.dart';
|
||||
import '../music_source.dart';
|
||||
import 'client.dart';
|
||||
|
||||
class SubsonicSource implements MusicSource {
|
||||
final SubsonicSettings opt;
|
||||
final BaseClient http;
|
||||
final int maxBitrate;
|
||||
final String? streamFormat;
|
||||
|
||||
late final SubsonicClient client;
|
||||
|
||||
final _pool = Pool(10, timeout: const Duration(seconds: 60));
|
||||
|
||||
SubsonicSource({
|
||||
required this.opt,
|
||||
required this.http,
|
||||
required this.maxBitrate,
|
||||
this.streamFormat,
|
||||
}) {
|
||||
client = SubsonicClient(opt, http);
|
||||
}
|
||||
|
||||
@override
|
||||
int get id => opt.id;
|
||||
|
||||
@override
|
||||
Future<void> ping() async {
|
||||
await client.get('ping');
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<ArtistsCompanion>> allArtists() async* {
|
||||
final res = await client.get('getArtists');
|
||||
for (var artists in res.xml.findAllElements('artist').slices(200)) {
|
||||
yield artists.map(_mapArtist);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<AlbumsCompanion>> allAlbums() async* {
|
||||
final extras = await Future.wait([
|
||||
_albumList('frequent')
|
||||
.flatten()
|
||||
.map((element) => element.getAttribute('id')!)
|
||||
.toList(),
|
||||
_albumList('recent')
|
||||
.flatten()
|
||||
.map((element) => element.getAttribute('id')!)
|
||||
.toList(),
|
||||
]);
|
||||
|
||||
final frequentlyPlayed = {
|
||||
for (var i = 0; i < extras[0].length; i++) extras[0][i]: i
|
||||
};
|
||||
final recentlyPlayed = {
|
||||
for (var i = 0; i < extras[1].length; i++) extras[1][i]: i
|
||||
};
|
||||
|
||||
await for (var albums in _albumList('newest')) {
|
||||
yield albums.map(
|
||||
(e) => _mapAlbum(
|
||||
e,
|
||||
frequentRank: Value(frequentlyPlayed[e.getAttribute('id')!]),
|
||||
recentRank: Value(recentlyPlayed[e.getAttribute('id')!]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<PlaylistWithSongsCompanion>> allPlaylists() async* {
|
||||
final allPlaylists = await client.get('getPlaylists');
|
||||
|
||||
yield* _pool.forEach(allPlaylists.xml.findAllElements('playlist'),
|
||||
(playlist) async {
|
||||
final res = await client.get(
|
||||
'getPlaylist',
|
||||
{'id': playlist.getAttribute('id')},
|
||||
);
|
||||
return [
|
||||
PlaylistWithSongsCompanion(
|
||||
_mapPlaylist(res.xml.getElement('playlist')!),
|
||||
res.xml.findAllElements('entry').mapIndexed(_mapPlaylistSong),
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Iterable<SongsCompanion>> allSongs() async* {
|
||||
if (opt.features.contains(SubsonicFeature.emptyQuerySearch)) {
|
||||
await for (var songs in _songSearch()) {
|
||||
yield songs.map(_mapSong);
|
||||
}
|
||||
} else {
|
||||
await for (var albumsList in _albumList('alphabeticalByName')) {
|
||||
yield* _pool.forEach(albumsList, (album) async {
|
||||
final albums = await client.get('getAlbum', {
|
||||
'id': album.getAttribute('id')!,
|
||||
});
|
||||
return albums.xml.findAllElements('song').map(_mapSong);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Uri streamUri(String songId) {
|
||||
return client.uri('stream', {
|
||||
'id': songId,
|
||||
'estimateContentLength': true.toString(),
|
||||
'maxBitRate': maxBitrate.toString(),
|
||||
'format': streamFormat?.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Uri downloadUri(String songId) {
|
||||
return client.uri('download', {'id': songId});
|
||||
}
|
||||
|
||||
@override
|
||||
Uri coverArtUri(String id, {bool thumbnail = true}) {
|
||||
final opts = {'id': id};
|
||||
if (thumbnail) {
|
||||
opts['size'] = 256.toString();
|
||||
}
|
||||
return client.uri('getCoverArt', opts);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uri?> artistArtUri(String artistId, {bool thumbnail = true}) async {
|
||||
final res = await client.get('getArtistInfo2', {'id': artistId});
|
||||
return Uri.tryParse(res.xml
|
||||
.getElement('artistInfo2')
|
||||
?.getElement(thumbnail ? 'smallImageUrl' : 'largeImageUrl')
|
||||
?.text ??
|
||||
'');
|
||||
}
|
||||
|
||||
Stream<Iterable<XmlElement>> _albumList(String type) async* {
|
||||
const size = 500;
|
||||
var offset = 0;
|
||||
|
||||
while (true) {
|
||||
final res = await client.get('getAlbumList2', {
|
||||
'type': type,
|
||||
'size': size.toString(),
|
||||
'offset': offset.toString(),
|
||||
});
|
||||
|
||||
final albums = res.xml.findAllElements('album');
|
||||
offset += albums.length;
|
||||
|
||||
yield albums;
|
||||
|
||||
if (albums.length < size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stream<Iterable<XmlElement>> _songSearch() async* {
|
||||
const size = 500;
|
||||
var offset = 0;
|
||||
|
||||
while (true) {
|
||||
final res = await client.get('search3', {
|
||||
'query': '""',
|
||||
'songCount': size.toString(),
|
||||
'songOffset': offset.toString(),
|
||||
'artistCount': '0',
|
||||
'albumCount': '0',
|
||||
});
|
||||
|
||||
final songs = res.xml.findAllElements('song');
|
||||
offset += songs.length;
|
||||
|
||||
yield songs;
|
||||
|
||||
if (songs.length < size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArtistsCompanion _mapArtist(XmlElement e) {
|
||||
return ArtistsCompanion.insert(
|
||||
sourceId: id,
|
||||
id: 'artist.${e.getAttribute('id')!}',
|
||||
name: e.getAttribute('name') ?? 'Artist ${e.getAttribute('id')}',
|
||||
albumCount: int.parse(e.getAttribute('albumCount')!),
|
||||
starred: Value(DateTimeExt.tryParseUtc(e.getAttribute('starred'))),
|
||||
);
|
||||
}
|
||||
|
||||
AlbumsCompanion _mapAlbum(
|
||||
XmlElement e, {
|
||||
Value<int?> frequentRank = const Value.absent(),
|
||||
Value<int?> recentRank = const Value.absent(),
|
||||
}) {
|
||||
return AlbumsCompanion.insert(
|
||||
sourceId: id,
|
||||
id: 'album.${e.getAttribute('id')!}',
|
||||
artistId: Value(e.getAttribute('artistId') != null
|
||||
? 'artist.${e.getAttribute('artistId')}'
|
||||
: null),
|
||||
name: e.getAttribute('name') ?? 'Album ${e.getAttribute('id')}',
|
||||
albumArtist: Value(e.getAttribute('artist')),
|
||||
created: DateTimeExt.parseUtc(e.getAttribute('created')!),
|
||||
coverArt: Value(e.getAttribute('coverArt') != null
|
||||
? 'coverArt.${e.getAttribute('coverArt')}'
|
||||
: null),
|
||||
year: e.getAttribute('year') != null
|
||||
? Value(int.parse(e.getAttribute('year')!))
|
||||
: const Value(null),
|
||||
starred: Value(DateTimeExt.tryParseUtc(e.getAttribute('starred'))),
|
||||
genre: Value(e.getAttribute('genre')),
|
||||
songCount: int.parse(e.getAttribute('songCount')!),
|
||||
frequentRank: frequentRank,
|
||||
recentRank: recentRank,
|
||||
);
|
||||
}
|
||||
|
||||
PlaylistsCompanion _mapPlaylist(XmlElement e) {
|
||||
return PlaylistsCompanion.insert(
|
||||
sourceId: id,
|
||||
id: 'playlist.${e.getAttribute('id')!}',
|
||||
name: e.getAttribute('name') ?? 'Playlist ${e.getAttribute('id')}',
|
||||
comment: Value(e.getAttribute('comment')),
|
||||
coverArt: Value(e.getAttribute('coverArt') != null
|
||||
? 'coverArt.${e.getAttribute('coverArt')}'
|
||||
: null),
|
||||
songCount: int.parse(e.getAttribute('songCount')!),
|
||||
created: DateTimeExt.parseUtc(e.getAttribute('created')!),
|
||||
);
|
||||
}
|
||||
|
||||
SongsCompanion _mapSong(XmlElement e) {
|
||||
return SongsCompanion.insert(
|
||||
sourceId: id,
|
||||
id: 'song.${e.getAttribute('id')!}',
|
||||
albumId: Value(e.getAttribute('albumId') != null
|
||||
? 'album.${e.getAttribute('albumId')}'
|
||||
: null),
|
||||
artistId: Value(e.getAttribute('artistId') != null
|
||||
? 'artist.${e.getAttribute('artistId')}'
|
||||
: null),
|
||||
title: e.getAttribute('title') ?? 'Song ${e.getAttribute('id')}',
|
||||
album: Value(e.getAttribute('album')),
|
||||
artist: Value(e.getAttribute('artist')),
|
||||
duration: e.getAttribute('duration') != null
|
||||
? Value(Duration(
|
||||
seconds: int.parse(e.getAttribute('duration').toString())))
|
||||
: const Value(null),
|
||||
track: e.getAttribute('track') != null
|
||||
? Value(int.parse(e.getAttribute('track')!))
|
||||
: const Value(null),
|
||||
disc: e.getAttribute('discNumber') != null
|
||||
? Value(int.parse(e.getAttribute('discNumber')!))
|
||||
: const Value(null),
|
||||
starred: Value(DateTimeExt.tryParseUtc(e.getAttribute('starred'))),
|
||||
genre: Value(e.getAttribute('genre')),
|
||||
);
|
||||
}
|
||||
|
||||
PlaylistSongsCompanion _mapPlaylistSong(int index, XmlElement e) {
|
||||
return PlaylistSongsCompanion.insert(
|
||||
sourceId: id,
|
||||
playlistId: 'playlist.${e.parentElement!.getAttribute('id')!}',
|
||||
songId: 'song.${e.getAttribute('id')!}',
|
||||
position: index,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
enum Status {
|
||||
ok('ok'),
|
||||
failed('failed');
|
||||
|
||||
const Status(this.value);
|
||||
final String value;
|
||||
}
|
||||
|
||||
class SubsonicResponse {
|
||||
late Status status;
|
||||
late XmlElement xml;
|
||||
|
||||
SubsonicResponse(XmlDocument xml) {
|
||||
this.xml = xml.getElement('subsonic-response')!;
|
||||
status = Status.values.byName(this.xml.getAttribute('status')!);
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../models/music.dart';
|
||||
import '../models/support.dart';
|
||||
import '../services/audio_service.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
part 'audio.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<MediaItem?> mediaItem(MediaItemRef ref) async* {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
await for (var item in audio.mediaItem) {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
MediaItemData? mediaItemData(MediaItemDataRef ref) {
|
||||
return ref.watch(mediaItemProvider.select(
|
||||
(value) => value.valueOrNull?.data,
|
||||
));
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<Song?> mediaItemSong(MediaItemSongRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
final id = ref.watch(mediaItemProvider.select(
|
||||
(value) => value.valueOrNull?.id,
|
||||
));
|
||||
|
||||
return db.songById(sourceId, id ?? '').watchSingleOrNull();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<PlaybackState> playbackState(PlaybackStateRef ref) async* {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
await for (var state in audio.playbackState) {
|
||||
yield state;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<QueueMode> queueMode(QueueModeRef ref) async* {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
await for (var state in audio.queueMode) {
|
||||
yield state;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<List<MediaItem>> queue(QueueRef ref) async* {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
await for (var queue in audio.queue) {
|
||||
yield queue;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<List<int>?> shuffleIndicies(ShuffleIndiciesRef ref) async* {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
await for (var indicies in audio.shuffleIndicies) {
|
||||
yield indicies;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<Duration> positionStream(PositionStreamRef ref) async* {
|
||||
final audio = ref.watch(audioControlProvider);
|
||||
await for (var state in audio.position) {
|
||||
yield state;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
bool playing(PlayingRef ref) {
|
||||
return ref.watch(playbackStateProvider.select(
|
||||
(value) => value.valueOrNull?.playing ?? false,
|
||||
));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
AudioProcessingState? processingState(ProcessingStateRef ref) {
|
||||
return ref.watch(playbackStateProvider.select(
|
||||
(value) => value.valueOrNull?.processingState,
|
||||
));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
int position(PositionRef ref) {
|
||||
return ref.watch(positionStreamProvider.select(
|
||||
(value) => value.valueOrNull?.inSeconds ?? 0,
|
||||
));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
int duration(DurationRef ref) {
|
||||
return ref.watch(mediaItemProvider.select(
|
||||
(value) => value.valueOrNull?.duration?.inSeconds ?? 0,
|
||||
));
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AudioServiceShuffleMode? shuffleMode(ShuffleModeRef ref) {
|
||||
return ref.watch(playbackStateProvider.select(
|
||||
(value) => value.valueOrNull?.shuffleMode,
|
||||
));
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AudioServiceRepeatMode repeatMode(RepeatModeRef ref) {
|
||||
return ref.watch(playbackStateProvider.select(
|
||||
(value) => value.valueOrNull?.repeatMode ?? AudioServiceRepeatMode.none,
|
||||
));
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LastAudioStateService extends _$LastAudioStateService {
|
||||
@override
|
||||
Future<void> build() async {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final queueMode = ref.watch(queueModeProvider).valueOrNull;
|
||||
final shuffleIndicies = ref.watch(shuffleIndiciesProvider).valueOrNull;
|
||||
final repeat = ref.watch(repeatModeProvider);
|
||||
|
||||
await db.saveLastAudioState(LastAudioStateCompanion.insert(
|
||||
id: const Value(1),
|
||||
queueMode: queueMode ?? QueueMode.user,
|
||||
shuffleIndicies: Value(shuffleIndicies?.lock),
|
||||
repeat: {
|
||||
AudioServiceRepeatMode.none: RepeatMode.none,
|
||||
AudioServiceRepeatMode.all: RepeatMode.all,
|
||||
AudioServiceRepeatMode.group: RepeatMode.all,
|
||||
AudioServiceRepeatMode.one: RepeatMode.one,
|
||||
}[repeat]!,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'audio.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$mediaItemHash() => r'ca8b6768872f17355f756c95cf85278127b59444';
|
||||
|
||||
/// See also [mediaItem].
|
||||
@ProviderFor(mediaItem)
|
||||
final mediaItemProvider = StreamProvider<MediaItem?>.internal(
|
||||
mediaItem,
|
||||
name: r'mediaItemProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$mediaItemHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef MediaItemRef = StreamProviderRef<MediaItem?>;
|
||||
String _$mediaItemDataHash() => r'8539c02682f0d33b584ea0437dd3774d9f321a2e';
|
||||
|
||||
/// See also [mediaItemData].
|
||||
@ProviderFor(mediaItemData)
|
||||
final mediaItemDataProvider = Provider<MediaItemData?>.internal(
|
||||
mediaItemData,
|
||||
name: r'mediaItemDataProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$mediaItemDataHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef MediaItemDataRef = ProviderRef<MediaItemData?>;
|
||||
String _$mediaItemSongHash() => r'274f43470cd993f0a2bed3d3da22d7bd41b562f1';
|
||||
|
||||
/// See also [mediaItemSong].
|
||||
@ProviderFor(mediaItemSong)
|
||||
final mediaItemSongProvider = StreamProvider<Song?>.internal(
|
||||
mediaItemSong,
|
||||
name: r'mediaItemSongProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$mediaItemSongHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef MediaItemSongRef = StreamProviderRef<Song?>;
|
||||
String _$playbackStateHash() => r'b4a9eb7f802fc8c92666c1318f789865140d6025';
|
||||
|
||||
/// See also [playbackState].
|
||||
@ProviderFor(playbackState)
|
||||
final playbackStateProvider = StreamProvider<PlaybackState>.internal(
|
||||
playbackState,
|
||||
name: r'playbackStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$playbackStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PlaybackStateRef = StreamProviderRef<PlaybackState>;
|
||||
String _$queueModeHash() => r'be0b1ff436c367e9be54c6d15fd8bac4f904fdec';
|
||||
|
||||
/// See also [queueMode].
|
||||
@ProviderFor(queueMode)
|
||||
final queueModeProvider = StreamProvider<QueueMode>.internal(
|
||||
queueMode,
|
||||
name: r'queueModeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$queueModeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef QueueModeRef = StreamProviderRef<QueueMode>;
|
||||
String _$queueHash() => r'94d86c99382f56193a11baf3f13354eab6a39fa8';
|
||||
|
||||
/// See also [queue].
|
||||
@ProviderFor(queue)
|
||||
final queueProvider = StreamProvider<List<MediaItem>>.internal(
|
||||
queue,
|
||||
name: r'queueProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$queueHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef QueueRef = StreamProviderRef<List<MediaItem>>;
|
||||
String _$shuffleIndiciesHash() => r'e5dc6879b2a7b7a501b58aace717ff36eff59995';
|
||||
|
||||
/// See also [shuffleIndicies].
|
||||
@ProviderFor(shuffleIndicies)
|
||||
final shuffleIndiciesProvider = StreamProvider<List<int>?>.internal(
|
||||
shuffleIndicies,
|
||||
name: r'shuffleIndiciesProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$shuffleIndiciesHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ShuffleIndiciesRef = StreamProviderRef<List<int>?>;
|
||||
String _$positionStreamHash() => r'5f1dc9d11e1bcce649ddb764525cc0dc79bfb6d8';
|
||||
|
||||
/// See also [positionStream].
|
||||
@ProviderFor(positionStream)
|
||||
final positionStreamProvider = AutoDisposeStreamProvider<Duration>.internal(
|
||||
positionStream,
|
||||
name: r'positionStreamProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$positionStreamHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PositionStreamRef = AutoDisposeStreamProviderRef<Duration>;
|
||||
String _$playingHash() => r'2a40fa275358918b243c8734bbe49bc9d7373f10';
|
||||
|
||||
/// See also [playing].
|
||||
@ProviderFor(playing)
|
||||
final playingProvider = AutoDisposeProvider<bool>.internal(
|
||||
playing,
|
||||
name: r'playingProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$playingHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PlayingRef = AutoDisposeProviderRef<bool>;
|
||||
String _$processingStateHash() => r'b9e59927b905384a0f1221b2adb9b681091c27d1';
|
||||
|
||||
/// See also [processingState].
|
||||
@ProviderFor(processingState)
|
||||
final processingStateProvider =
|
||||
AutoDisposeProvider<AudioProcessingState?>.internal(
|
||||
processingState,
|
||||
name: r'processingStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$processingStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ProcessingStateRef = AutoDisposeProviderRef<AudioProcessingState?>;
|
||||
String _$positionHash() => r'bfc853fe9e46bf79522fa41374763a7e1c12e739';
|
||||
|
||||
/// See also [position].
|
||||
@ProviderFor(position)
|
||||
final positionProvider = AutoDisposeProvider<int>.internal(
|
||||
position,
|
||||
name: r'positionProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$positionHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PositionRef = AutoDisposeProviderRef<int>;
|
||||
String _$durationHash() => r'bf9eb316b8401401e5862384deb1a4c1134e6dd2';
|
||||
|
||||
/// See also [duration].
|
||||
@ProviderFor(duration)
|
||||
final durationProvider = AutoDisposeProvider<int>.internal(
|
||||
duration,
|
||||
name: r'durationProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$durationHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef DurationRef = AutoDisposeProviderRef<int>;
|
||||
String _$shuffleModeHash() => r'fd26d81cb9bd5e0e1a7e9ccf1589c104d9a4eb3a';
|
||||
|
||||
/// See also [shuffleMode].
|
||||
@ProviderFor(shuffleMode)
|
||||
final shuffleModeProvider = Provider<AudioServiceShuffleMode?>.internal(
|
||||
shuffleMode,
|
||||
name: r'shuffleModeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$shuffleModeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ShuffleModeRef = ProviderRef<AudioServiceShuffleMode?>;
|
||||
String _$repeatModeHash() => r'346248bf08df65f1f69e4cb4b6ef192190d2910c';
|
||||
|
||||
/// See also [repeatMode].
|
||||
@ProviderFor(repeatMode)
|
||||
final repeatModeProvider = Provider<AudioServiceRepeatMode>.internal(
|
||||
repeatMode,
|
||||
name: r'repeatModeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$repeatModeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef RepeatModeRef = ProviderRef<AudioServiceRepeatMode>;
|
||||
String _$lastAudioStateServiceHash() =>
|
||||
r'4291b8d4a399f1b192277a3e8a93fbe7096fea32';
|
||||
|
||||
/// See also [LastAudioStateService].
|
||||
@ProviderFor(LastAudioStateService)
|
||||
final lastAudioStateServiceProvider =
|
||||
AsyncNotifierProvider<LastAudioStateService, void>.internal(
|
||||
LastAudioStateService.new,
|
||||
name: r'lastAudioStateServiceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$lastAudioStateServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LastAudioStateService = AsyncNotifier<void>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,82 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../app/app.dart';
|
||||
import '../app/app_router.dart';
|
||||
import '../app/pages/bottom_nav_page.dart';
|
||||
import '../app/pages/library_page.dart';
|
||||
import '../services/audio_service.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import 'audio.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
part 'init.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
FutureOr<Map<String, String>> env(EnvRef ref) async {
|
||||
await dotenv.load();
|
||||
return dotenv.env;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppRouter router(RouterRef ref) {
|
||||
return AppRouter();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
FutureOr<Uri> placeholderImageUri(PlaceholderImageUriRef ref) async {
|
||||
final byteData = await rootBundle.load('assets/placeholder.png');
|
||||
final docsDir = await getApplicationDocumentsDirectory();
|
||||
|
||||
return (await File('${docsDir.path}/placeholder.png').writeAsBytes(byteData
|
||||
.buffer
|
||||
.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)))
|
||||
.uri;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
FutureOr<Uri> placeholderThumbImageUri(PlaceholderThumbImageUriRef ref) async {
|
||||
final byteData = await rootBundle.load('assets/placeholder_thumb.png');
|
||||
final docsDir = await getApplicationDocumentsDirectory();
|
||||
|
||||
return (await File('${docsDir.path}/placeholder_thumb.png').writeAsBytes(
|
||||
byteData.buffer
|
||||
.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)))
|
||||
.uri;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
FutureOr<PackageInfo> packageInfo(PackageInfoRef ref) async {
|
||||
return await PackageInfo.fromPlatform();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
FutureOr<void> init(InitRef ref) async {
|
||||
ref.watch(routerProvider);
|
||||
await ref.watch(envProvider.future);
|
||||
await ref.read(packageInfoProvider.future);
|
||||
await ref.watch(placeholderImageUriProvider.future);
|
||||
await ref.watch(placeholderThumbImageUriProvider.future);
|
||||
|
||||
await ref.read(networkModeProvider.future);
|
||||
await ref.read(maxBitrateProvider.future);
|
||||
await ref.watch(settingsServiceProvider.notifier).init();
|
||||
|
||||
final audio = await ref.watch(audioControlInitProvider.future);
|
||||
await audio.init();
|
||||
ref.watch(lastAudioStateServiceProvider.notifier);
|
||||
|
||||
await ref.watch(downloadServiceProvider.notifier).init();
|
||||
|
||||
await ref.watch(lastPathProvider.notifier).init();
|
||||
ref.watch(lastBottomNavStateServiceProvider.notifier);
|
||||
ref.watch(lastLibraryStateServiceProvider.notifier);
|
||||
|
||||
await ref.watch(libraryListsProvider.notifier).init();
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'init.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$envHash() => r'183e2c6c10c5a9d3dfcaffca0f1723cd85d67a5c';
|
||||
|
||||
/// See also [env].
|
||||
@ProviderFor(env)
|
||||
final envProvider = FutureProvider<Map<String, String>>.internal(
|
||||
env,
|
||||
name: r'envProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$envHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef EnvRef = FutureProviderRef<Map<String, String>>;
|
||||
String _$routerHash() => r'3f52af31948ac5942fe4167f84785572458eeacc';
|
||||
|
||||
/// See also [router].
|
||||
@ProviderFor(router)
|
||||
final routerProvider = Provider<AppRouter>.internal(
|
||||
router,
|
||||
name: r'routerProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$routerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef RouterRef = ProviderRef<AppRouter>;
|
||||
String _$placeholderImageUriHash() =>
|
||||
r'129f37af640421ec12038d161a8b15a67536e570';
|
||||
|
||||
/// See also [placeholderImageUri].
|
||||
@ProviderFor(placeholderImageUri)
|
||||
final placeholderImageUriProvider = FutureProvider<Uri>.internal(
|
||||
placeholderImageUri,
|
||||
name: r'placeholderImageUriProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$placeholderImageUriHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PlaceholderImageUriRef = FutureProviderRef<Uri>;
|
||||
String _$placeholderThumbImageUriHash() =>
|
||||
r'8136dd4bbb3dc57fb1e6aa8db556e58620c61c85';
|
||||
|
||||
/// See also [placeholderThumbImageUri].
|
||||
@ProviderFor(placeholderThumbImageUri)
|
||||
final placeholderThumbImageUriProvider = FutureProvider<Uri>.internal(
|
||||
placeholderThumbImageUri,
|
||||
name: r'placeholderThumbImageUriProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$placeholderThumbImageUriHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PlaceholderThumbImageUriRef = FutureProviderRef<Uri>;
|
||||
String _$packageInfoHash() => r'9a2956f08c2e98b92dd8cce49cb331a127c78670';
|
||||
|
||||
/// See also [packageInfo].
|
||||
@ProviderFor(packageInfo)
|
||||
final packageInfoProvider = FutureProvider<PackageInfo>.internal(
|
||||
packageInfo,
|
||||
name: r'packageInfoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$packageInfoHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef PackageInfoRef = FutureProviderRef<PackageInfo>;
|
||||
String _$initHash() => r'10e2945eb65f51e6a904a3811fa88e63771e5b19';
|
||||
|
||||
/// See also [init].
|
||||
@ProviderFor(init)
|
||||
final initProvider = FutureProvider<void>.internal(
|
||||
init,
|
||||
name: r'initProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$initHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef InitRef = FutureProviderRef<void>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,152 +0,0 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../models/music.dart';
|
||||
import '../models/query.dart';
|
||||
import '../models/support.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
part 'music.g.dart';
|
||||
|
||||
@riverpod
|
||||
Stream<Artist> artist(ArtistRef ref, String id) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.artistById(sourceId, id).watchSingle();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<Album> album(AlbumRef ref, String id) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.albumById(sourceId, id).watchSingle();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<ListDownloadStatus> albumDownloadStatus(
|
||||
AlbumDownloadStatusRef ref,
|
||||
String id,
|
||||
) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.albumDownloadStatus(sourceId, id).watchSingle();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<ListDownloadStatus> playlistDownloadStatus(
|
||||
PlaylistDownloadStatusRef ref,
|
||||
String id,
|
||||
) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.playlistDownloadStatus(sourceId, id).watchSingle();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<Song> song(SongRef ref, String id) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.songById(sourceId, id).watchSingle();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<Song>> albumSongsList(
|
||||
AlbumSongsListRef ref,
|
||||
String id,
|
||||
ListQuery opt,
|
||||
) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.albumSongsList(SourceId(sourceId: sourceId, id: id), opt).get();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<Song>> songsByAlbumList(
|
||||
SongsByAlbumListRef ref,
|
||||
ListQuery opt,
|
||||
) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.songsByAlbumList(sourceId, opt).get();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<Playlist> playlist(PlaylistRef ref, String id) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.playlistById(sourceId, id).watchSingle();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<Song>> playlistSongsList(
|
||||
PlaylistSongsListRef ref,
|
||||
String id,
|
||||
ListQuery opt,
|
||||
) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.playlistSongsList(SourceId(sourceId: sourceId, id: id), opt).get();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<Album>> albumsInIds(AlbumsInIdsRef ref, IList<String> ids) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.albumsInIds(sourceId, ids.toList()).get();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<IList<Album>> albumsByArtistId(AlbumsByArtistIdRef ref, String id) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db
|
||||
.albumsByArtistId(sourceId, id)
|
||||
.watch()
|
||||
.map((event) => event.toIList());
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<IList<String>> albumGenres(AlbumGenresRef ref, Pagination page) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db
|
||||
.albumGenres(sourceId, page.limit, page.offset)
|
||||
.watch()
|
||||
.map((event) => event.withNullsRemoved().toIList());
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<IList<Album>> albumsByGenre(
|
||||
AlbumsByGenreRef ref,
|
||||
String genre,
|
||||
Pagination page,
|
||||
) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db
|
||||
.albumsByGenre(sourceId, genre, page.limit, page.offset)
|
||||
.watch()
|
||||
.map((event) => event.toIList());
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Stream<int> songsByGenreCount(SongsByGenreCountRef ref, String genre) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return db.songsByGenreCount(sourceId, genre).watchSingle();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../http/client.dart';
|
||||
import '../models/settings.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../sources/music_source.dart';
|
||||
import '../sources/subsonic/source.dart';
|
||||
|
||||
part 'settings.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
MusicSource musicSource(MusicSourceRef ref) {
|
||||
final settings = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.activeSource,
|
||||
)) as SubsonicSettings;
|
||||
final streamFormat = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.app.streamFormat,
|
||||
));
|
||||
final maxBitrate = ref.watch(maxBitrateProvider).requireValue;
|
||||
final http = ref.watch(httpClientProvider);
|
||||
|
||||
return MusicSource(
|
||||
SubsonicSource(
|
||||
opt: settings,
|
||||
http: http,
|
||||
maxBitrate: maxBitrate,
|
||||
streamFormat: streamFormat,
|
||||
),
|
||||
ref,
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<NetworkMode> networkMode(NetworkModeRef ref) async* {
|
||||
await for (var state in Connectivity().onConnectivityChanged) {
|
||||
switch (state) {
|
||||
case ConnectivityResult.wifi:
|
||||
case ConnectivityResult.ethernet:
|
||||
yield NetworkMode.wifi;
|
||||
break;
|
||||
default:
|
||||
yield NetworkMode.mobile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<int> maxBitrate(MaxBitrateRef ref) async {
|
||||
final settings = ref.watch(settingsServiceProvider.select(
|
||||
(value) => value.app,
|
||||
));
|
||||
final networkMode = ref.watch(networkModeProvider).requireValue;
|
||||
|
||||
return networkMode == NetworkMode.wifi
|
||||
? settings.maxBitrateWifi
|
||||
: settings.maxBitrateMobile;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
int sourceId(SourceIdRef ref) {
|
||||
return ref.watch(musicSourceProvider.select((value) => value.id));
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class OfflineMode extends _$OfflineMode {
|
||||
@override
|
||||
bool build() {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> setMode(bool value) async {
|
||||
final hasSource = ref.read(settingsServiceProvider.select(
|
||||
(value) => value.activeSource != null,
|
||||
));
|
||||
if (!hasSource) return;
|
||||
|
||||
if (value == false && state == true) {
|
||||
try {
|
||||
await ref.read(musicSourceProvider).ping();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$musicSourceHash() => r'466e2654eab8518c9e40c8c2c08a2ecb331b0a7f';
|
||||
|
||||
/// See also [musicSource].
|
||||
@ProviderFor(musicSource)
|
||||
final musicSourceProvider = Provider<MusicSource>.internal(
|
||||
musicSource,
|
||||
name: r'musicSourceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$musicSourceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef MusicSourceRef = ProviderRef<MusicSource>;
|
||||
String _$networkModeHash() => r'813a60a454c6acaefbe3b56bf0152497ab18dcce';
|
||||
|
||||
/// See also [networkMode].
|
||||
@ProviderFor(networkMode)
|
||||
final networkModeProvider = StreamProvider<NetworkMode>.internal(
|
||||
networkMode,
|
||||
name: r'networkModeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$networkModeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef NetworkModeRef = StreamProviderRef<NetworkMode>;
|
||||
String _$maxBitrateHash() => r'ec02d3ccbc9f3429acfc1b3f191cab791b1191e0';
|
||||
|
||||
/// See also [maxBitrate].
|
||||
@ProviderFor(maxBitrate)
|
||||
final maxBitrateProvider = FutureProvider<int>.internal(
|
||||
maxBitrate,
|
||||
name: r'maxBitrateProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$maxBitrateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef MaxBitrateRef = FutureProviderRef<int>;
|
||||
String _$sourceIdHash() => r'66ed4717b4a07548f5e25a42aeac2027aeab9b9c';
|
||||
|
||||
/// See also [sourceId].
|
||||
@ProviderFor(sourceId)
|
||||
final sourceIdProvider = Provider<int>.internal(
|
||||
sourceId,
|
||||
name: r'sourceIdProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$sourceIdHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef SourceIdRef = ProviderRef<int>;
|
||||
String _$offlineModeHash() => r'b84cdece48d97c69e995fbaea97febb128cfc20a';
|
||||
|
||||
/// See also [OfflineMode].
|
||||
@ProviderFor(OfflineMode)
|
||||
final offlineModeProvider = NotifierProvider<OfflineMode, bool>.internal(
|
||||
OfflineMode.new,
|
||||
name: r'offlineModeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$offlineModeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$OfflineMode = Notifier<bool>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,258 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
import '../cache/image_cache.dart';
|
||||
import '../models/support.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import 'audio.dart';
|
||||
import 'music.dart';
|
||||
|
||||
part 'theme.g.dart';
|
||||
|
||||
const kDarkBackgroundValue = 0.28;
|
||||
const kDarkerBackgroundLightness = 0.13;
|
||||
const kOnDarkerBackgroundLightness = 0.6;
|
||||
|
||||
PaletteColor? _rankedByLuminance(
|
||||
List<PaletteColor?> colors, [
|
||||
double minLuminance = 0.0,
|
||||
double maxLuminance = 1.0,
|
||||
]) {
|
||||
for (var color in colors) {
|
||||
if (color == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final luminance = color.color.computeLuminance();
|
||||
if (luminance >= minLuminance && luminance <= maxLuminance) {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
PaletteColor? _rankedWithValue(double value, List<PaletteColor?> colors) {
|
||||
for (var color in colors) {
|
||||
if (color == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final hsv = HSVColor.fromColor(color.color);
|
||||
if (hsv.value > value) {
|
||||
return PaletteColor(hsv.withValue(value).toColor(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
ColorTheme _colorTheme(_ColorThemeRef ref, Palette palette) {
|
||||
final base = ref.watch(baseThemeProvider);
|
||||
|
||||
final primary = _rankedByLuminance([
|
||||
palette.dominantColor,
|
||||
palette.vibrantColor,
|
||||
palette.mutedColor,
|
||||
palette.darkVibrantColor,
|
||||
], 0.2);
|
||||
final vibrant = _rankedByLuminance([
|
||||
palette.vibrantColor,
|
||||
palette.darkVibrantColor,
|
||||
palette.dominantColor,
|
||||
], 0.05);
|
||||
final secondary = _rankedByLuminance([
|
||||
palette.lightMutedColor,
|
||||
palette.mutedColor,
|
||||
palette.darkMutedColor,
|
||||
]);
|
||||
|
||||
final background = _rankedWithValue(0.5, [
|
||||
palette.vibrantColor,
|
||||
palette.darkVibrantColor,
|
||||
palette.darkMutedColor,
|
||||
palette.dominantColor,
|
||||
palette.mutedColor,
|
||||
palette.lightVibrantColor,
|
||||
palette.lightMutedColor,
|
||||
]);
|
||||
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
brightness: Brightness.dark,
|
||||
seedColor: background?.color ?? Colors.purple[800]!,
|
||||
background: background?.color,
|
||||
primaryContainer: primary?.color,
|
||||
onPrimaryContainer: primary?.bodyTextColor,
|
||||
secondaryContainer: secondary?.color,
|
||||
onSecondaryContainer: secondary?.bodyTextColor,
|
||||
surface: background?.color,
|
||||
surfaceTint: vibrant?.color,
|
||||
);
|
||||
|
||||
final hsv = HSVColor.fromColor(colorScheme.background);
|
||||
final hsl = HSLColor.fromColor(colorScheme.background);
|
||||
|
||||
return base.copyWith(
|
||||
theme: ThemeData(
|
||||
colorScheme: colorScheme,
|
||||
useMaterial3: base.theme.useMaterial3,
|
||||
brightness: base.theme.brightness,
|
||||
cardTheme: base.theme.cardTheme,
|
||||
),
|
||||
gradientHigh: colorScheme.background,
|
||||
darkBackground: hsv.withValue(kDarkBackgroundValue).toColor(),
|
||||
darkerBackground: hsl.withLightness(kDarkerBackgroundLightness).toColor(),
|
||||
onDarkerBackground:
|
||||
hsl.withLightness(kOnDarkerBackgroundLightness).toColor(),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
ColorTheme baseTheme(BaseThemeRef ref) {
|
||||
final theme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: Colors.purple[800],
|
||||
brightness: Brightness.dark,
|
||||
cardTheme: CardTheme(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final hsv = HSVColor.fromColor(theme.colorScheme.background);
|
||||
final hsl = HSLColor.fromColor(theme.colorScheme.background);
|
||||
|
||||
return ColorTheme(
|
||||
theme: theme,
|
||||
gradientHigh: theme.colorScheme.background,
|
||||
gradientLow: HSLColor.fromColor(theme.colorScheme.background)
|
||||
.withLightness(0.06)
|
||||
.toColor(),
|
||||
darkBackground: hsv.withValue(kDarkBackgroundValue).toColor(),
|
||||
darkerBackground: hsl.withLightness(kDarkerBackgroundLightness).toColor(),
|
||||
onDarkerBackground:
|
||||
hsl.withLightness(kOnDarkerBackgroundLightness).toColor(),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<Palette> albumArtPalette(
|
||||
AlbumArtPaletteRef ref,
|
||||
String id,
|
||||
) async {
|
||||
final album = await ref.watch(albumProvider(id).future);
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final info = cache.albumArt(album, thumbnail: true);
|
||||
|
||||
return Palette.fromPaletteGenerator(await _generateFromCache(info));
|
||||
}
|
||||
|
||||
Future<PaletteGenerator> _generateFromCache(
|
||||
UriCacheInfo cacheInfo,
|
||||
) async {
|
||||
var file = await cacheInfo.cacheManager.getSingleFile(
|
||||
cacheInfo.uri.toString(),
|
||||
key: cacheInfo.cacheKey,
|
||||
);
|
||||
|
||||
final memoryImage = ResizeImage(
|
||||
MemoryImage(await file.readAsBytes()),
|
||||
height: 32,
|
||||
width: 32,
|
||||
);
|
||||
final stream = memoryImage.resolve(const ImageConfiguration(
|
||||
devicePixelRatio: 1.0,
|
||||
));
|
||||
|
||||
Completer<ui.Image> imageCompleter = Completer<ui.Image>();
|
||||
late ImageStreamListener listener;
|
||||
listener = ImageStreamListener((ImageInfo info, bool synchronousCall) {
|
||||
stream.removeListener(listener);
|
||||
imageCompleter.complete(info.image);
|
||||
});
|
||||
|
||||
stream.addListener(listener);
|
||||
final image = await imageCompleter.future;
|
||||
final byteData = (await image.toByteData())!;
|
||||
final height = image.height;
|
||||
final width = image.width;
|
||||
|
||||
return await Executor().execute(
|
||||
arg1: byteData,
|
||||
arg2: height,
|
||||
arg3: width,
|
||||
fun3: _computePalette,
|
||||
);
|
||||
}
|
||||
|
||||
Future<PaletteGenerator> _computePalette(
|
||||
ByteData byteData,
|
||||
int height,
|
||||
int width,
|
||||
TypeSendPort port,
|
||||
) async {
|
||||
return await PaletteGenerator.fromByteData(
|
||||
EncodedImage(byteData, height: height, width: width),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<Palette> playlistArtPalette(
|
||||
PlaylistArtPaletteRef ref,
|
||||
String id,
|
||||
) async {
|
||||
final playlist = await ref.watch(playlistProvider(id).future);
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final info = cache.playlistArt(playlist, thumbnail: true);
|
||||
|
||||
return Palette.fromPaletteGenerator(await _generateFromCache(info));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<Palette> mediaItemPalette(MediaItemPaletteRef ref) async {
|
||||
final item = ref.watch(mediaItemProvider).valueOrNull;
|
||||
final itemData = ref.watch(mediaItemDataProvider);
|
||||
final imageCache = ref.watch(imageCacheProvider);
|
||||
|
||||
if (item == null || item.artUri == null || itemData == null) {
|
||||
return const Palette();
|
||||
}
|
||||
|
||||
return Palette.fromPaletteGenerator(await _generateFromCache(
|
||||
UriCacheInfo(
|
||||
uri: item.artUri!,
|
||||
cacheKey: itemData.artCache!.thumbnailArtCacheKey,
|
||||
cacheManager: imageCache,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<ColorTheme> mediaItemTheme(MediaItemThemeRef ref) async {
|
||||
final palette = await ref.watch(mediaItemPaletteProvider.future);
|
||||
return ref.watch(_colorThemeProvider(palette));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<ColorTheme> albumArtTheme(AlbumArtThemeRef ref, String id) async {
|
||||
final palette = await ref.watch(albumArtPaletteProvider(id).future);
|
||||
return ref.watch(_colorThemeProvider(palette));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<ColorTheme> playlistArtTheme(
|
||||
PlaylistArtThemeRef ref,
|
||||
String id,
|
||||
) async {
|
||||
final palette = await ref.watch(playlistArtPaletteProvider(id).future);
|
||||
return ref.watch(_colorThemeProvider(palette));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user