mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-09 22:42:44 +01:00
Compare commits
14 Commits
16a79c81cb
...
flutter-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad6d534286 | ||
|
|
2837d4576e | ||
|
|
7f6ba4776a | ||
|
|
f7874bcead | ||
|
|
ba169092fd | ||
|
|
4183e2d3b9 | ||
|
|
c3bb14edbf | ||
|
|
805e6fff7a | ||
|
|
d245fc7fef | ||
|
|
3fcb938f2b | ||
|
|
97ea3c3230 | ||
|
|
71132a1f0e | ||
|
|
f3969dc6af | ||
|
|
a4e4c6fa57 |
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
@@ -1,26 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "debug",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug"
|
||||
},
|
||||
{
|
||||
"name": "profile mode",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "release mode",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -4,9 +4,11 @@ import 'screens/album_screen.dart';
|
||||
import 'screens/artist_screen.dart';
|
||||
import 'screens/library_screen.dart';
|
||||
import 'screens/now_playing_screen.dart';
|
||||
import 'screens/playlist_screen.dart';
|
||||
import 'screens/preload_screen.dart';
|
||||
import 'screens/root_shell_screen.dart';
|
||||
import 'screens/settings_screen.dart';
|
||||
import 'screens/settings_source_screen.dart';
|
||||
|
||||
final router = GoRouter(
|
||||
initialLocation: '/preload',
|
||||
@@ -23,13 +25,19 @@ final router = GoRouter(
|
||||
builder: (context, state) => LibraryScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'album/:id',
|
||||
path: 'albums/:id',
|
||||
builder: (context, state) =>
|
||||
AlbumScreen(id: state.pathParameters['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'artist',
|
||||
builder: (context, state) => ArtistScreen(),
|
||||
path: 'artists/:id',
|
||||
builder: (context, state) =>
|
||||
ArtistScreen(id: state.pathParameters['id']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'playlists/:id',
|
||||
builder: (context, state) =>
|
||||
PlaylistScreen(id: state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -43,5 +51,12 @@ final router = GoRouter(
|
||||
path: '/settings',
|
||||
builder: (context, state) => SettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/sources/:id',
|
||||
builder: (context, state) {
|
||||
final id = state.pathParameters['id'];
|
||||
return SettingsSourceScreen(id: id == 'add' ? null : int.parse(id!));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../state/source.dart';
|
||||
import '../ui/cover_art_theme.dart';
|
||||
import '../ui/gradient.dart';
|
||||
import '../ui/lists/header.dart';
|
||||
import '../ui/lists/items.dart';
|
||||
import '../ui/lists/songs_list.dart';
|
||||
|
||||
class AlbumScreen extends HookConsumerWidget {
|
||||
@@ -40,9 +41,9 @@ class AlbumScreen extends HookConsumerWidget {
|
||||
sourceId: sourceId,
|
||||
filter: IList([SongsFilter.albumId(album.id)]),
|
||||
sort: IList([
|
||||
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.disc),
|
||||
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.track),
|
||||
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.title),
|
||||
SortingTerm.songsAsc(SongsColumn.disc),
|
||||
SortingTerm.songsAsc(SongsColumn.track),
|
||||
SortingTerm.songsAsc(SongsColumn.title),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -61,7 +62,13 @@ class AlbumScreen extends HookConsumerWidget {
|
||||
onMore: () {},
|
||||
),
|
||||
),
|
||||
SongsList(query: query),
|
||||
SongsList(
|
||||
query: query,
|
||||
itemBuilder: (context, item, index) => SongListTile(
|
||||
song: item.song,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,12 +1,124 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class ArtistScreen extends StatelessWidget {
|
||||
const ArtistScreen({super.key});
|
||||
import '../../database/query.dart';
|
||||
import '../../sources/models.dart';
|
||||
import '../state/database.dart';
|
||||
import '../state/source.dart';
|
||||
import '../ui/cover_art_theme.dart';
|
||||
import '../ui/gradient.dart';
|
||||
import '../ui/images.dart';
|
||||
import '../ui/lists/albums_list.dart';
|
||||
|
||||
class ArtistScreen extends HookConsumerWidget {
|
||||
const ArtistScreen({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(child: Text('Artist!')),
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
final getArtist = useMemoized(
|
||||
() => db.libraryDao.getArtist(sourceId, id).getSingle(),
|
||||
);
|
||||
final artist = useFuture(getArtist).data;
|
||||
|
||||
if (artist == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final query = AlbumsQuery(
|
||||
sourceId: sourceId,
|
||||
filter: IList([AlbumsFilter.artistId(artist.id)]),
|
||||
sort: IList([
|
||||
SortingTerm.albumsDesc(AlbumsColumn.year),
|
||||
SortingTerm.albumsAsc(AlbumsColumn.name),
|
||||
]),
|
||||
);
|
||||
|
||||
return CoverArtTheme(
|
||||
coverArt: artist.coverArt,
|
||||
child: Scaffold(
|
||||
body: GradientScrollView(
|
||||
slivers: [
|
||||
ArtistHeader(artist: artist),
|
||||
AlbumsList(query: query),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistHeader extends StatelessWidget {
|
||||
const ArtistHeader({
|
||||
super.key,
|
||||
required this.artist,
|
||||
});
|
||||
|
||||
final Artist artist;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = TextTheme.of(context);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
alignment: AlignmentGeometry.bottomCenter,
|
||||
children: [
|
||||
CoverArtImage(
|
||||
fit: BoxFit.cover,
|
||||
coverArt: artist.coverArt,
|
||||
thumbnail: false,
|
||||
height: 350,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: AlignmentGeometry.centerRight,
|
||||
end: AlignmentGeometry.centerLeft,
|
||||
colors: [
|
||||
colorScheme.surface.withAlpha(220),
|
||||
colorScheme.surface.withAlpha(150),
|
||||
colorScheme.surface.withAlpha(20),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsGeometry.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Text(
|
||||
artist.name,
|
||||
textAlign: TextAlign.right,
|
||||
style: textTheme.headlineLarge?.copyWith(
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 20,
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,21 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../l10n/generated/app_localizations.dart';
|
||||
import '../state/lists.dart';
|
||||
import '../state/services.dart';
|
||||
import '../ui/lists/albums_grid.dart';
|
||||
import '../ui/lists/artists_list.dart';
|
||||
import '../ui/lists/items.dart';
|
||||
import '../ui/lists/playlists_list.dart';
|
||||
import '../ui/lists/songs_list.dart';
|
||||
import '../ui/menus.dart';
|
||||
import '../util/custom_scroll_fix.dart';
|
||||
|
||||
const kIconSize = 26.0;
|
||||
const kTabHeight = 36.0;
|
||||
|
||||
enum LibraryTab {
|
||||
home(Icon(Symbols.home_rounded)),
|
||||
// home(Icon(Symbols.home_rounded)),
|
||||
albums(Icon(Symbols.album_rounded)),
|
||||
artists(Icon(Symbols.person_rounded)),
|
||||
songs(Icon(Symbols.music_note_rounded)),
|
||||
@@ -35,7 +40,7 @@ class LibraryScreen extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(
|
||||
initialLength: LibraryTab.values.length,
|
||||
initialIndex: 1,
|
||||
initialIndex: 0,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
@@ -51,20 +56,7 @@ class LibraryScreen extends HookConsumerWidget {
|
||||
builder: (context) => CustomScrollProvider(
|
||||
tabController: tabController,
|
||||
parent: PrimaryScrollController.of(context),
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: LibraryTab.values
|
||||
.map(
|
||||
(tab) => TabScrollView(
|
||||
index: LibraryTab.values.indexOf(tab),
|
||||
sliver: switch (tab) {
|
||||
LibraryTab.albums => AlbumsGrid(),
|
||||
_ => ArtistsList(),
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
child: LibraryTabBarView(tabController: tabController),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -72,6 +64,53 @@ class LibraryScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryTabBarView extends HookConsumerWidget {
|
||||
const LibraryTabBarView({
|
||||
super.key,
|
||||
required this.tabController,
|
||||
});
|
||||
|
||||
final TabController tabController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final songsQuery = ref.watch(songsQueryProvider);
|
||||
|
||||
return TabBarView(
|
||||
controller: tabController,
|
||||
children: LibraryTab.values
|
||||
.map(
|
||||
(tab) => TabScrollView(
|
||||
index: LibraryTab.values.indexOf(tab),
|
||||
sliver: switch (tab) {
|
||||
LibraryTab.albums => AlbumsGrid(),
|
||||
LibraryTab.artists => ArtistsList(),
|
||||
LibraryTab.playlists => PlaylistsList(),
|
||||
LibraryTab.songs => SongsList(
|
||||
query: songsQuery,
|
||||
itemBuilder: (context, item, index) => SongListTile(
|
||||
song: item.song,
|
||||
coverArt: item.albumCoverArt,
|
||||
showLeading: true,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
// _ => SliverToBoxAdapter(child: Container()),
|
||||
},
|
||||
menuBuilder: switch (tab) {
|
||||
LibraryTab.albums => (_) => AlbumsGridFilters(),
|
||||
// LibraryTab.artists => (_) => AlbumsGridFilters(),
|
||||
// LibraryTab.playlists => (_) => AlbumsGridFilters(),
|
||||
// LibraryTab.songs => (_) => AlbumsGridFilters(),
|
||||
_ => null,
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryTabsHeader extends HookConsumerWidget {
|
||||
const LibraryTabsHeader({
|
||||
super.key,
|
||||
@@ -165,7 +204,7 @@ class TabTitleText extends HookConsumerWidget {
|
||||
|
||||
String tabLocalization(LibraryTab tab) => switch (tab) {
|
||||
LibraryTab.albums => l.navigationTabsAlbums,
|
||||
LibraryTab.home => l.navigationTabsHome,
|
||||
// LibraryTab.home => l.navigationTabsHome,
|
||||
LibraryTab.artists => l.navigationTabsArtists,
|
||||
LibraryTab.songs => l.navigationTabsSongs,
|
||||
LibraryTab.playlists => l.navigationTabsPlaylists,
|
||||
@@ -189,10 +228,12 @@ class TabScrollView extends HookConsumerWidget {
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.sliver,
|
||||
this.menuBuilder,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final Widget sliver;
|
||||
final WidgetBuilder? menuBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -200,14 +241,24 @@ class TabScrollView extends HookConsumerWidget {
|
||||
|
||||
final scrollProvider = CustomScrollProviderData.of(context);
|
||||
|
||||
return CustomScrollView(
|
||||
controller: scrollProvider.scrollControllers[index],
|
||||
slivers: <Widget>[
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
sliver,
|
||||
],
|
||||
final listBuilder = menuBuilder;
|
||||
final floatingActionButton = listBuilder != null
|
||||
? FabFilter(
|
||||
listBuilder: listBuilder,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Scaffold(
|
||||
floatingActionButton: floatingActionButton,
|
||||
body: CustomScrollView(
|
||||
controller: scrollProvider.scrollControllers[index],
|
||||
slivers: <Widget>[
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
sliver,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
77
lib/app/screens/playlist_screen.dart
Normal file
77
lib/app/screens/playlist_screen.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/query.dart';
|
||||
import '../../l10n/generated/app_localizations.dart';
|
||||
import '../state/database.dart';
|
||||
import '../state/source.dart';
|
||||
import '../ui/cover_art_theme.dart';
|
||||
import '../ui/gradient.dart';
|
||||
import '../ui/lists/header.dart';
|
||||
import '../ui/lists/items.dart';
|
||||
import '../ui/lists/songs_list.dart';
|
||||
|
||||
class PlaylistScreen extends HookConsumerWidget {
|
||||
const PlaylistScreen({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
final db = ref.watch(databaseProvider);
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
final getPlaylist = useMemoized(
|
||||
() => db.libraryDao.getPlaylist(sourceId, id).getSingle(),
|
||||
);
|
||||
final playlist = useFuture(getPlaylist).data;
|
||||
|
||||
if (playlist == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final query = SongsQuery(
|
||||
sourceId: sourceId,
|
||||
filter: IList([SongsFilter.playlistId(playlist.id)]),
|
||||
sort: IList([
|
||||
SortingTerm.songsAsc(SongsColumn.playlistPosition),
|
||||
]),
|
||||
);
|
||||
|
||||
return CoverArtTheme(
|
||||
coverArt: playlist.coverArt,
|
||||
child: Scaffold(
|
||||
body: GradientScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SongsListHeader(
|
||||
title: playlist.name,
|
||||
// subtitle: playlist.albumArtist,
|
||||
coverArt: playlist.coverArt,
|
||||
playText: l.resourcesPlaylistActionsPlay,
|
||||
onPlay: () {},
|
||||
onMore: () {},
|
||||
),
|
||||
),
|
||||
SongsList(
|
||||
query: query,
|
||||
itemBuilder: (context, item, index) => SongListTile(
|
||||
song: item.song,
|
||||
coverArt: item.albumCoverArt,
|
||||
showLeading: true,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../l10n/generated/app_localizations.dart';
|
||||
@@ -14,15 +15,14 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
final text = TextTheme.of(context);
|
||||
final textTheme = TextTheme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l.navigationTabsSettings, style: text.headlineLarge),
|
||||
title: Text(l.navigationTabsSettings, style: textTheme.headlineLarge),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
// const SizedBox(height: 96),
|
||||
_SectionHeader(l.settingsServersName),
|
||||
const _Sources(),
|
||||
// _SectionHeader(l.settingsNetworkName),
|
||||
@@ -36,7 +36,9 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
class _Section extends StatelessWidget {
|
||||
const _Section({required this.children});
|
||||
const _Section({
|
||||
required this.children,
|
||||
});
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
@@ -46,14 +48,15 @@ class _Section extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...children,
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader(this.title);
|
||||
const _SectionHeader(
|
||||
this.title,
|
||||
);
|
||||
|
||||
final String title;
|
||||
|
||||
@@ -61,17 +64,14 @@ class _SectionHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final text = TextTheme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding),
|
||||
child: Text(title, style: text.headlineMedium),
|
||||
),
|
||||
),
|
||||
],
|
||||
return Padding(
|
||||
padding: EdgeInsetsGeometry.directional(
|
||||
start: kHorizontalPadding,
|
||||
end: kHorizontalPadding,
|
||||
top: 32,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Text(title, style: text.headlineMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -376,12 +376,12 @@ class _Sources extends HookConsumerWidget {
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
for (final (source, settings) in sources)
|
||||
for (final (:source, :subsonicSetting) in sources)
|
||||
RadioListTile<int>(
|
||||
value: source.id,
|
||||
title: Text(source.name),
|
||||
subtitle: Text(
|
||||
settings.address.toString(),
|
||||
subsonicSetting.address.toString(),
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
@@ -389,7 +389,7 @@ class _Sources extends HookConsumerWidget {
|
||||
secondary: IconButton(
|
||||
icon: const Icon(Icons.edit_rounded),
|
||||
onPressed: () {
|
||||
// context.pushRoute(SourceRoute(id: source.id));
|
||||
context.push('/sources/${source.id}');
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -404,7 +404,7 @@ class _Sources extends HookConsumerWidget {
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: Text(l.settingsServersActionsAdd),
|
||||
onPressed: () {
|
||||
// context.pushRoute(SourceRoute());
|
||||
context.push('/sources/add');
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
309
lib/app/screens/settings_source_screen.dart
Normal file
309
lib/app/screens/settings_source_screen.dart
Normal file
@@ -0,0 +1,309 @@
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/dao/sources_dao.dart';
|
||||
import '../../database/database.dart';
|
||||
import '../../l10n/generated/app_localizations.dart';
|
||||
import '../../util/logger.dart';
|
||||
import '../state/database.dart';
|
||||
import '../ui/menus.dart';
|
||||
|
||||
class SettingsSourceScreen extends HookConsumerWidget {
|
||||
const SettingsSourceScreen({
|
||||
super.key,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
|
||||
final getSource = useMemoized(
|
||||
() async => id != null
|
||||
? (result: await db.sourcesDao.getSource(id!).getSingle())
|
||||
: await Future.value((result: null)),
|
||||
[id],
|
||||
);
|
||||
final sourceResult = useFuture(getSource).data;
|
||||
|
||||
if (sourceResult == null) {
|
||||
return Scaffold();
|
||||
}
|
||||
|
||||
return _SettingsSourceScreen(source: sourceResult.result);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsSourceScreen extends HookConsumerWidget {
|
||||
const _SettingsSourceScreen({
|
||||
required this.source,
|
||||
});
|
||||
|
||||
final SourceSetting? source;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
final textTheme = TextTheme.of(context);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
|
||||
final form = useState(GlobalKey<FormState>()).value;
|
||||
final isSaving = useState(false);
|
||||
final isDeleting = useState(false);
|
||||
|
||||
final nameController = useTextEditingController(text: source?.source.name);
|
||||
final addressController = useTextEditingController(
|
||||
text: source?.subsonicSetting.address.toString(),
|
||||
);
|
||||
final usernameController = useTextEditingController(
|
||||
text: source?.subsonicSetting.username,
|
||||
);
|
||||
final passwordController = useTextEditingController(
|
||||
text: source?.subsonicSetting.password,
|
||||
);
|
||||
final forcePlaintextPassword = useState(
|
||||
!(source?.subsonicSetting.useTokenAuth ?? true),
|
||||
);
|
||||
|
||||
final name = LabeledTextField(
|
||||
label: l.settingsServersFieldsName,
|
||||
controller: nameController,
|
||||
required: true,
|
||||
);
|
||||
|
||||
final address = LabeledTextField(
|
||||
label: l.settingsServersFieldsAddress,
|
||||
controller: addressController,
|
||||
keyboardType: TextInputType.url,
|
||||
autofillHints: const [AutofillHints.url],
|
||||
required: true,
|
||||
validator: (value, label) {
|
||||
final uri = Uri.tryParse(value ?? '');
|
||||
if (uri?.isAbsolute != true || uri?.host.isNotEmpty != true) {
|
||||
return '$label must be a valid URL';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
final username = LabeledTextField(
|
||||
label: l.settingsServersFieldsUsername,
|
||||
controller: usernameController,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
required: true,
|
||||
);
|
||||
|
||||
final password = LabeledTextField(
|
||||
label: l.settingsServersFieldsPassword,
|
||||
controller: passwordController,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
obscureText: true,
|
||||
required: 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 PopScope(
|
||||
canPop: !isSaving.value && !isDeleting.value,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
source == null
|
||||
? l.settingsServersActionsAdd
|
||||
: l.settingsServersActionsEdit,
|
||||
style: textTheme.headlineLarge,
|
||||
),
|
||||
),
|
||||
floatingActionButton: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (source != null && source?.source.isActive != true)
|
||||
FloatingActionButton(
|
||||
backgroundColor: colorScheme.tertiaryContainer,
|
||||
foregroundColor: colorScheme.onTertiaryContainer,
|
||||
onPressed: !isSaving.value && !isDeleting.value
|
||||
? () async {
|
||||
try {
|
||||
isDeleting.value = true;
|
||||
await ref
|
||||
.read(databaseProvider)
|
||||
.sourcesDao
|
||||
.deleteSource(source!.source.id);
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: isDeleting.value
|
||||
? SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: 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 {
|
||||
if (!form.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var error = false;
|
||||
try {
|
||||
isSaving.value = true;
|
||||
if (source != null) {
|
||||
await ref
|
||||
.read(databaseProvider)
|
||||
.sourcesDao
|
||||
.updateSource(
|
||||
source!.source.copyWith(
|
||||
name: nameController.text,
|
||||
),
|
||||
source!.subsonicSetting.copyWith(
|
||||
address: Uri.parse(addressController.text),
|
||||
username: usernameController.text,
|
||||
password: passwordController.text,
|
||||
useTokenAuth: !forcePlaintextPassword.value,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await ref
|
||||
.read(databaseProvider)
|
||||
.sourcesDao
|
||||
.createSource(
|
||||
SourcesCompanion.insert(
|
||||
name: nameController.text,
|
||||
),
|
||||
SubsonicSettingsCompanion.insert(
|
||||
address: Uri.parse(addressController.text),
|
||||
username: usernameController.text,
|
||||
password: passwordController.text,
|
||||
useTokenAuth: Value(
|
||||
!forcePlaintextPassword.value,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e, _) {
|
||||
// showErrorSnackbar(context, e.toString());
|
||||
// log.severe('Saving source', e, st);
|
||||
logger.w('fuck');
|
||||
error = true;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
|
||||
if (!error && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: form,
|
||||
child: AutofillGroup(
|
||||
child: ListView(
|
||||
children: [
|
||||
name,
|
||||
address,
|
||||
username,
|
||||
password,
|
||||
const SizedBox(height: 24),
|
||||
forcePlaintextSwitch,
|
||||
const FabPadding(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LabeledTextField extends HookConsumerWidget {
|
||||
const LabeledTextField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.controller,
|
||||
this.obscureText = false,
|
||||
this.keyboardType,
|
||||
this.validator,
|
||||
this.autofillHints,
|
||||
this.required = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final TextEditingController controller;
|
||||
final bool obscureText;
|
||||
final bool required;
|
||||
final TextInputType? keyboardType;
|
||||
final Iterable<String>? autofillHints;
|
||||
final String? Function(String? value, String label)? validator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final textTheme = TextTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
Text(label, style: textTheme.titleMedium),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
autofillHints: autofillHints,
|
||||
validator: (value) {
|
||||
if (required) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$label is required';
|
||||
}
|
||||
}
|
||||
|
||||
if (validator != null) {
|
||||
return validator!(value, label);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:drift/drift.dart' show InsertMode, Value;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
@@ -6,46 +6,40 @@ import '../../database/database.dart';
|
||||
final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
||||
final db = SubtracksDatabase();
|
||||
|
||||
await db
|
||||
.into(db.sources)
|
||||
.insertOnConflictUpdate(
|
||||
await db.batch((batch) {
|
||||
batch.insertAll(
|
||||
db.sources,
|
||||
[
|
||||
SourcesCompanion.insert(
|
||||
id: Value(1),
|
||||
name: 'test subsonic',
|
||||
// isActive: Value(true),
|
||||
isActive: Value(true),
|
||||
),
|
||||
);
|
||||
await db
|
||||
.into(db.subsonicSettings)
|
||||
.insertOnConflictUpdate(
|
||||
SubsonicSettingsCompanion.insert(
|
||||
sourceId: Value(1),
|
||||
address: Uri.parse('http://demo.subsonic.org'),
|
||||
username: 'guest1',
|
||||
password: 'guest',
|
||||
useTokenAuth: Value(true),
|
||||
),
|
||||
);
|
||||
await db
|
||||
.into(db.sources)
|
||||
.insertOnConflictUpdate(
|
||||
SourcesCompanion.insert(
|
||||
id: Value(2),
|
||||
name: 'test navidrome',
|
||||
// isActive: Value(null),
|
||||
isActive: Value(null),
|
||||
),
|
||||
);
|
||||
await db
|
||||
.into(db.subsonicSettings)
|
||||
.insertOnConflictUpdate(
|
||||
SubsonicSettingsCompanion.insert(
|
||||
sourceId: Value(2),
|
||||
address: Uri.parse('http://10.0.2.2:4533'),
|
||||
username: 'admin',
|
||||
password: 'password',
|
||||
useTokenAuth: Value(true),
|
||||
),
|
||||
);
|
||||
],
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
batch.insertAllOnConflictUpdate(db.subsonicSettings, [
|
||||
SubsonicSettingsCompanion.insert(
|
||||
sourceId: Value(1),
|
||||
address: Uri.parse('http://demo.subsonic.org'),
|
||||
username: 'guest1',
|
||||
password: 'guest',
|
||||
useTokenAuth: Value(true),
|
||||
),
|
||||
SubsonicSettingsCompanion.insert(
|
||||
sourceId: Value(2),
|
||||
address: Uri.parse('http://10.0.2.2:4533'),
|
||||
username: 'admin',
|
||||
password: 'password',
|
||||
useTokenAuth: Value(true),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
return db;
|
||||
});
|
||||
|
||||
31
lib/app/state/lists.dart
Normal file
31
lib/app/state/lists.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../database/query.dart';
|
||||
import 'source.dart';
|
||||
|
||||
final albumsQueryProvider = Provider<AlbumsQuery>((ref) {
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return AlbumsQuery(
|
||||
sourceId: sourceId,
|
||||
sort: IList([
|
||||
SortingTerm.albumsDesc(AlbumsColumn.created),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
final songsQueryProvider = Provider<SongsQuery>((ref) {
|
||||
final sourceId = ref.watch(sourceIdProvider);
|
||||
|
||||
return SongsQuery(
|
||||
sourceId: sourceId,
|
||||
sort: IList([
|
||||
SortingTerm.songsAsc(SongsColumn.albumArtist),
|
||||
SortingTerm.songsAsc(SongsColumn.album),
|
||||
SortingTerm.songsAsc(SongsColumn.disc),
|
||||
SortingTerm.songsAsc(SongsColumn.track),
|
||||
SortingTerm.songsAsc(SongsColumn.title),
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../util/logger.dart';
|
||||
import '../state/source.dart';
|
||||
import '../util/color_scheme.dart';
|
||||
import 'theme.dart';
|
||||
@@ -36,8 +37,12 @@ class CoverArtTheme extends HookConsumerWidget {
|
||||
: 'https://placehold.net/400x400.png',
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
} catch (error, stackTrace) {
|
||||
logger.w(
|
||||
'Could not create color scheme from image provider',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
@@ -46,11 +51,11 @@ class CoverArtTheme extends HookConsumerWidget {
|
||||
|
||||
final colorScheme = useFuture(getColorScheme).data;
|
||||
|
||||
return colorScheme != null
|
||||
? Theme(
|
||||
data: subtracksTheme(colorScheme),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
return Theme(
|
||||
data: colorScheme == null
|
||||
? Theme.of(context)
|
||||
: subtracksTheme(colorScheme),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ class CoverArtImage extends HookConsumerWidget {
|
||||
super.key,
|
||||
this.coverArt,
|
||||
this.thumbnail = true,
|
||||
this.fit,
|
||||
this.fit = BoxFit.cover,
|
||||
this.height,
|
||||
this.width,
|
||||
});
|
||||
|
||||
final String? coverArt;
|
||||
final bool thumbnail;
|
||||
final BoxFit? fit;
|
||||
final BoxFit fit;
|
||||
final double? height;
|
||||
final double? width;
|
||||
|
||||
@@ -37,7 +37,7 @@ class CoverArtImage extends HookConsumerWidget {
|
||||
cacheKey: '$sourceId$coverArt$thumbnail',
|
||||
placeholder: (context, url) => Icon(Symbols.cached_rounded),
|
||||
errorWidget: (context, url, error) => Icon(Icons.error),
|
||||
fit: BoxFit.cover,
|
||||
fit: fit,
|
||||
fadeOutDuration: Duration(milliseconds: 100),
|
||||
fadeInDuration: Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../../../database/query.dart';
|
||||
import '../../../sources/models.dart';
|
||||
import '../../hooks/use_on_source.dart';
|
||||
import '../../hooks/use_paging_controller.dart';
|
||||
import '../../state/database.dart';
|
||||
import '../../state/lists.dart';
|
||||
import '../../state/source.dart';
|
||||
import '../menus.dart';
|
||||
import 'items.dart';
|
||||
|
||||
const kPageSize = 60;
|
||||
@@ -20,26 +21,22 @@ class AlbumsGrid extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final query = ref.watch(albumsQueryProvider);
|
||||
|
||||
final controller = usePagingController<int, Album>(
|
||||
getNextPageKey: (state) =>
|
||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||
fetchPage: (pageKey) => db.libraryDao.listAlbums(
|
||||
AlbumsQuery(
|
||||
query.copyWith(
|
||||
sourceId: ref.read(sourceIdProvider),
|
||||
sort: IList([
|
||||
AlbumsSortingTerm(
|
||||
dir: SortDirection.desc,
|
||||
by: AlbumsColumn.created,
|
||||
),
|
||||
]),
|
||||
limit: kPageSize,
|
||||
offset: (pageKey - 1) * kPageSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
useOnSourceChange(ref, (_) => controller.refresh());
|
||||
useOnSourceSync(ref, controller.refresh);
|
||||
useValueChanged(query, (_, _) => controller.refresh());
|
||||
|
||||
return PagingListener(
|
||||
controller: controller,
|
||||
@@ -52,11 +49,13 @@ class AlbumsGrid extends HookConsumerWidget {
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
),
|
||||
showNoMoreItemsIndicatorAsGridChild: false,
|
||||
builderDelegate: PagedChildBuilderDelegate<Album>(
|
||||
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||
itemBuilder: (context, item, index) => AlbumGridTile(
|
||||
album: item,
|
||||
onTap: () async {
|
||||
context.push('/album/${item.id}');
|
||||
context.push('/albums/${item.id}');
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -66,3 +65,14 @@ class AlbumsGrid extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumsGridFilters extends HookConsumerWidget {
|
||||
const AlbumsGridFilters({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView(
|
||||
children: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
92
lib/app/ui/lists/albums_list.dart
Normal file
92
lib/app/ui/lists/albums_list.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../../../database/query.dart';
|
||||
import '../../../sources/models.dart';
|
||||
import '../../hooks/use_on_source.dart';
|
||||
import '../../hooks/use_paging_controller.dart';
|
||||
import '../../state/database.dart';
|
||||
import '../../state/source.dart';
|
||||
import '../menus.dart';
|
||||
import 'items.dart';
|
||||
|
||||
const kPageSize = 30;
|
||||
|
||||
class AlbumsList extends HookConsumerWidget {
|
||||
const AlbumsList({
|
||||
super.key,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final AlbumsQuery query;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final controller = usePagingController<int, Album>(
|
||||
getNextPageKey: (state) =>
|
||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||
fetchPage: (pageKey) => db.libraryDao.listAlbums(
|
||||
query.copyWith(
|
||||
sourceId: ref.read(sourceIdProvider),
|
||||
limit: kPageSize,
|
||||
offset: (pageKey - 1) * kPageSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
useOnSourceChange(ref, (_) => controller.refresh());
|
||||
useOnSourceSync(ref, controller.refresh);
|
||||
|
||||
return PagingListener(
|
||||
controller: controller,
|
||||
builder: (context, state, fetchNextPage) {
|
||||
return PagedSliverList(
|
||||
state: state,
|
||||
fetchNextPage: fetchNextPage,
|
||||
builderDelegate: PagedChildBuilderDelegate<Album>(
|
||||
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||
itemBuilder: (context, item, index) {
|
||||
final tile = AlbumListTile(
|
||||
album: item,
|
||||
onTap: () {
|
||||
context.push('/albums/${item.id}');
|
||||
},
|
||||
);
|
||||
|
||||
final currentItemYear = item.year;
|
||||
final previousItemYear = index == 0
|
||||
? currentItemYear
|
||||
: controller.items?.elementAtOrNull(index - 1)?.year;
|
||||
|
||||
if (index == 0 || currentItemYear != previousItemYear) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 24,
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
child: Text(
|
||||
item.year?.toString() ?? 'Unknown year',
|
||||
style: TextTheme.of(context).headlineMedium,
|
||||
),
|
||||
),
|
||||
tile,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return tile;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../../hooks/use_on_source.dart';
|
||||
import '../../hooks/use_paging_controller.dart';
|
||||
import '../../state/database.dart';
|
||||
import '../../state/source.dart';
|
||||
import '../menus.dart';
|
||||
import 'items.dart';
|
||||
|
||||
const kPageSize = 30;
|
||||
@@ -27,10 +28,7 @@ class ArtistsList extends HookConsumerWidget {
|
||||
ArtistsQuery(
|
||||
sourceId: ref.read(sourceIdProvider),
|
||||
sort: IList([
|
||||
ArtistsSortingTerm(
|
||||
dir: SortDirection.asc,
|
||||
by: ArtistsColumn.name,
|
||||
),
|
||||
SortingTerm.artistsAsc(ArtistsColumn.name),
|
||||
]),
|
||||
limit: kPageSize,
|
||||
offset: (pageKey - 1) * kPageSize,
|
||||
@@ -48,6 +46,7 @@ class ArtistsList extends HookConsumerWidget {
|
||||
state: state,
|
||||
fetchNextPage: fetchNextPage,
|
||||
builderDelegate: PagedChildBuilderDelegate<AristListItem>(
|
||||
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||
itemBuilder: (context, item, index) {
|
||||
final (:artist, :albumCount) = item;
|
||||
|
||||
@@ -55,7 +54,7 @@ class ArtistsList extends HookConsumerWidget {
|
||||
artist: artist,
|
||||
albumCount: albumCount,
|
||||
onTap: () async {
|
||||
context.push('/artist/${artist.id}');
|
||||
context.push('/artists/${artist.id}');
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -36,6 +36,8 @@ class SongsListHeader extends HookConsumerWidget {
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
height: 300,
|
||||
width: 300,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -48,7 +50,6 @@ class SongsListHeader extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
child: CoverArtImage(
|
||||
height: 300,
|
||||
thumbnail: false,
|
||||
coverArt: coverArt,
|
||||
fit: BoxFit.contain,
|
||||
@@ -62,11 +63,12 @@ class SongsListHeader extends HookConsumerWidget {
|
||||
style: theme.textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
subtitle ?? '',
|
||||
style: theme.textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
@@ -38,12 +38,12 @@ class ArtistListTile extends StatelessWidget {
|
||||
const ArtistListTile({
|
||||
super.key,
|
||||
required this.artist,
|
||||
this.albumCount,
|
||||
required this.albumCount,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Artist artist;
|
||||
final int? albumCount;
|
||||
final int albumCount;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
@@ -56,7 +56,92 @@ class ArtistListTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
title: Text(artist.name),
|
||||
subtitle: albumCount != null ? Text('$albumCount albums') : null,
|
||||
subtitle: Text('$albumCount albums'),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumListTile extends StatelessWidget {
|
||||
const AlbumListTile({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = TextTheme.of(context);
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 18),
|
||||
child: RoundedBoxClip(
|
||||
child: CoverArtImage(
|
||||
coverArt: album.coverArt,
|
||||
thumbnail: true,
|
||||
width: 80,
|
||||
height: 80,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
album.name,
|
||||
style: textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
Text(
|
||||
album.albumArtist ?? 'Unknown album artist',
|
||||
style: textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistListTile extends StatelessWidget {
|
||||
const PlaylistListTile({
|
||||
super.key,
|
||||
required this.playlist,
|
||||
this.albumCount,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Playlist playlist;
|
||||
final int? albumCount;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: RoundedBoxClip(
|
||||
child: CoverArtImage(
|
||||
coverArt: playlist.coverArt,
|
||||
thumbnail: true,
|
||||
),
|
||||
),
|
||||
title: Text(playlist.name),
|
||||
subtitle: Text(playlist.comment ?? ''),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
@@ -66,19 +151,27 @@ class SongListTile extends StatelessWidget {
|
||||
const SongListTile({
|
||||
super.key,
|
||||
required this.song,
|
||||
this.coverArt,
|
||||
this.showLeading = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Song song;
|
||||
final String? coverArt;
|
||||
final bool showLeading;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
// leading: CoverArtImage(
|
||||
// coverArt: song.coverArt,
|
||||
// thumbnail: true,
|
||||
// ),
|
||||
leading: showLeading
|
||||
? RoundedBoxClip(
|
||||
child: CoverArtImage(
|
||||
coverArt: coverArt,
|
||||
thumbnail: true,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
title: Text(song.title),
|
||||
subtitle: Text(song.artist ?? ''),
|
||||
onTap: onTap,
|
||||
|
||||
63
lib/app/ui/lists/playlists_list.dart
Normal file
63
lib/app/ui/lists/playlists_list.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../../../database/query.dart';
|
||||
import '../../../sources/models.dart';
|
||||
import '../../hooks/use_on_source.dart';
|
||||
import '../../hooks/use_paging_controller.dart';
|
||||
import '../../state/database.dart';
|
||||
import '../../state/source.dart';
|
||||
import '../menus.dart';
|
||||
import 'items.dart';
|
||||
|
||||
const kPageSize = 30;
|
||||
|
||||
class PlaylistsList extends HookConsumerWidget {
|
||||
const PlaylistsList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final controller = usePagingController<int, Playlist>(
|
||||
getNextPageKey: (state) =>
|
||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||
fetchPage: (pageKey) => db.libraryDao.listPlaylists(
|
||||
PlaylistsQuery(
|
||||
sourceId: ref.read(sourceIdProvider),
|
||||
sort: IList([
|
||||
SortingTerm.playlistsDesc(PlaylistsColumn.created),
|
||||
]),
|
||||
limit: kPageSize,
|
||||
offset: (pageKey - 1) * kPageSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
useOnSourceChange(ref, (_) => controller.refresh());
|
||||
useOnSourceSync(ref, controller.refresh);
|
||||
|
||||
return PagingListener(
|
||||
controller: controller,
|
||||
builder: (context, state, fetchNextPage) {
|
||||
return PagedSliverList(
|
||||
state: state,
|
||||
fetchNextPage: fetchNextPage,
|
||||
builderDelegate: PagedChildBuilderDelegate<Playlist>(
|
||||
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||
itemBuilder: (context, item, index) {
|
||||
return PlaylistListTile(
|
||||
playlist: item,
|
||||
onTap: () {
|
||||
context.push('/playlists/${item.id}');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../../../database/dao/library_dao.dart';
|
||||
import '../../../database/query.dart';
|
||||
import '../../../sources/models.dart';
|
||||
import '../../hooks/use_on_source.dart';
|
||||
import '../../hooks/use_paging_controller.dart';
|
||||
import '../../state/database.dart';
|
||||
import 'items.dart';
|
||||
import '../../state/source.dart';
|
||||
import '../menus.dart';
|
||||
|
||||
const kPageSize = 30;
|
||||
|
||||
@@ -15,26 +17,30 @@ class SongsList extends HookConsumerWidget {
|
||||
const SongsList({
|
||||
super.key,
|
||||
required this.query,
|
||||
required this.itemBuilder,
|
||||
});
|
||||
|
||||
final SongsQuery query;
|
||||
final Widget Function(BuildContext context, SongListItem item, int index)
|
||||
itemBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final controller = usePagingController<int, Song>(
|
||||
final controller = usePagingController<int, SongListItem>(
|
||||
getNextPageKey: (state) =>
|
||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||
fetchPage: (pageKey) => db.libraryDao.listSongs(
|
||||
query.copyWith(
|
||||
sourceId: ref.read(sourceIdProvider),
|
||||
limit: kPageSize,
|
||||
offset: (pageKey - 1) * kPageSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
useOnSourceChange(ref, (_) => controller.refresh());
|
||||
useOnSourceSync(ref, controller.refresh);
|
||||
useValueChanged(query, (_, _) => controller.refresh());
|
||||
|
||||
return PagingListener(
|
||||
controller: controller,
|
||||
@@ -42,13 +48,9 @@ class SongsList extends HookConsumerWidget {
|
||||
return PagedSliverList(
|
||||
state: state,
|
||||
fetchNextPage: fetchNextPage,
|
||||
builderDelegate: PagedChildBuilderDelegate<Song>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return SongListTile(
|
||||
song: item,
|
||||
onTap: () async {},
|
||||
);
|
||||
},
|
||||
builderDelegate: PagedChildBuilderDelegate<SongListItem>(
|
||||
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||
itemBuilder: itemBuilder,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
76
lib/app/ui/menus.dart
Normal file
76
lib/app/ui/menus.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
Future<void> showContextMenu({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder listBuilder,
|
||||
}) => showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => DraggableScrollableSheet(
|
||||
expand: false,
|
||||
snap: true,
|
||||
initialChildSize: 0.3,
|
||||
minChildSize: 0.3,
|
||||
maxChildSize: 0.4,
|
||||
builder: (context, scrollController) => PrimaryScrollController(
|
||||
controller: scrollController,
|
||||
child: listBuilder(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
class ContextMenuList extends StatelessWidget {
|
||||
const ContextMenuList({
|
||||
super.key,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FabFilter extends StatelessWidget {
|
||||
const FabFilter({
|
||||
super.key,
|
||||
required this.listBuilder,
|
||||
});
|
||||
|
||||
final WidgetBuilder listBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton(
|
||||
onPressed: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
listBuilder: listBuilder,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Symbols.filter_list_rounded,
|
||||
weight: 500,
|
||||
opticalSize: 28,
|
||||
size: 28,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FabPadding extends StatelessWidget {
|
||||
const FabPadding({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(height: 86);
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,19 @@ ThemeData subtracksTheme([ColorScheme? colorScheme]) {
|
||||
useMaterial3: true,
|
||||
);
|
||||
|
||||
final text = theme.textTheme;
|
||||
return theme.copyWith(
|
||||
textTheme: text.copyWith(
|
||||
headlineLarge: text.headlineLarge?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
headlineMedium: text.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
headlineSmall: text.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
final text = theme.textTheme.copyWith(
|
||||
headlineLarge: theme.textTheme.headlineLarge?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
headlineMedium: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
headlineSmall: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
|
||||
return theme.copyWith(
|
||||
textTheme: text,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,3 +19,21 @@ class CircleClip extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoundedBoxClip extends StatelessWidget {
|
||||
const RoundedBoxClip({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: BorderRadiusGeometry.circular(3),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,15 @@ import '../query.dart';
|
||||
|
||||
part 'library_dao.g.dart';
|
||||
|
||||
typedef AristListItem = ({models.Artist artist, int? albumCount});
|
||||
typedef AristListItem = ({
|
||||
models.Artist artist,
|
||||
int albumCount,
|
||||
});
|
||||
|
||||
typedef SongListItem = ({
|
||||
models.Song song,
|
||||
String? albumCoverArt,
|
||||
});
|
||||
|
||||
extension on SortDirection {
|
||||
OrderingMode toMode() => switch (this) {
|
||||
@@ -111,7 +119,7 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<models.Song>> listSongs(SongsQuery q) {
|
||||
Future<List<SongListItem>> listSongs(SongsQuery q) {
|
||||
var joinPlaylistSongs = false;
|
||||
var filter = songs.sourceId.equals(q.sourceId);
|
||||
|
||||
@@ -127,6 +135,11 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
|
||||
final query =
|
||||
songs.select().join([
|
||||
leftOuterJoin(
|
||||
albums,
|
||||
albums.id.equalsExp(songs.albumId) &
|
||||
albums.sourceId.equals(q.sourceId),
|
||||
),
|
||||
if (joinPlaylistSongs)
|
||||
leftOuterJoin(
|
||||
playlistSongs,
|
||||
@@ -134,6 +147,9 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
playlistSongs.songId.equalsExp(songs.id),
|
||||
),
|
||||
])
|
||||
..addColumns([
|
||||
albums.coverArt,
|
||||
])
|
||||
..where(filter)
|
||||
..orderBy(
|
||||
q.sort
|
||||
@@ -144,6 +160,10 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
SongsColumn.starred => songs.starred,
|
||||
SongsColumn.disc => songs.disc,
|
||||
SongsColumn.track => songs.track,
|
||||
SongsColumn.album => songs.album,
|
||||
SongsColumn.artist => songs.artist,
|
||||
SongsColumn.albumArtist => albums.albumArtist,
|
||||
SongsColumn.playlistPosition => playlistSongs.position,
|
||||
},
|
||||
mode: sort.dir.toMode(),
|
||||
),
|
||||
@@ -153,7 +173,50 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
|
||||
_limitQuery(query: query, limit: q.limit, offset: q.offset);
|
||||
|
||||
return query.map((row) => (row.readTable(songs))).get();
|
||||
return query
|
||||
.map(
|
||||
(row) => (
|
||||
song: row.readTable(songs),
|
||||
albumCoverArt: row.read(albums.coverArt),
|
||||
),
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<models.Playlist>> listPlaylists(PlaylistsQuery q) {
|
||||
final query = playlists.select()
|
||||
..where((playlists) {
|
||||
var filter = playlists.sourceId.equals(q.sourceId);
|
||||
for (final queryFilter in q.filter) {
|
||||
filter &= switch (queryFilter) {
|
||||
PlaylistsFilterPublic(:final public) => playlists.public.equals(
|
||||
public,
|
||||
),
|
||||
PlaylistsFilterNameSearch() => CustomExpression(''),
|
||||
_ => CustomExpression(''),
|
||||
};
|
||||
}
|
||||
|
||||
return filter;
|
||||
})
|
||||
..orderBy(
|
||||
q.sort
|
||||
.map(
|
||||
(sort) =>
|
||||
(albums) => OrderingTerm(
|
||||
expression: switch (sort.by) {
|
||||
PlaylistsColumn.name => playlists.name,
|
||||
PlaylistsColumn.created => playlists.created,
|
||||
},
|
||||
mode: sort.dir.toMode(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
_limitQuery(query: query, limit: q.limit, offset: q.offset);
|
||||
|
||||
return query.get();
|
||||
}
|
||||
|
||||
Selectable<models.Album> getAlbum(int sourceId, String id) {
|
||||
@@ -162,6 +225,18 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<models.Artist> getArtist(int sourceId, String id) {
|
||||
return db.managers.artists.filter(
|
||||
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<models.Playlist> getPlaylist(int sourceId, String id) {
|
||||
return db.managers.playlists.filter(
|
||||
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
|
||||
);
|
||||
}
|
||||
|
||||
void _limitQuery({
|
||||
required LimitContainerMixin query,
|
||||
required int? limit,
|
||||
|
||||
@@ -4,6 +4,8 @@ import '../database.dart';
|
||||
|
||||
part 'sources_dao.g.dart';
|
||||
|
||||
typedef SourceSetting = ({Source source, SubsonicSetting subsonicSetting});
|
||||
|
||||
@DriftAccessor(include: {'../tables.drift'})
|
||||
class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
with _$SourcesDaoMixin {
|
||||
@@ -16,7 +18,7 @@ class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
.map((row) => row.read(sources.id));
|
||||
}
|
||||
|
||||
Stream<List<(Source, SubsonicSetting)>> listSources() {
|
||||
Stream<List<SourceSetting>> listSources() {
|
||||
final query = select(sources).join([
|
||||
innerJoin(
|
||||
subsonicSettings,
|
||||
@@ -24,18 +26,56 @@ class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
||||
),
|
||||
]);
|
||||
|
||||
return query.watch().map(
|
||||
(rows) => rows
|
||||
.map(
|
||||
(row) => (
|
||||
row.readTable(sources),
|
||||
row.readTable(subsonicSettings),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
return query
|
||||
.map(
|
||||
(row) => (
|
||||
source: row.readTable(sources),
|
||||
subsonicSetting: row.readTable(subsonicSettings),
|
||||
),
|
||||
)
|
||||
.watch();
|
||||
}
|
||||
|
||||
Selectable<SourceSetting> getSource(int id) {
|
||||
final query = select(sources).join([
|
||||
innerJoin(
|
||||
subsonicSettings,
|
||||
sources.id.equalsExp(subsonicSettings.sourceId),
|
||||
),
|
||||
])..where(sources.id.equals(id));
|
||||
|
||||
return query.map(
|
||||
(row) => (
|
||||
source: row.readTable(sources),
|
||||
subsonicSetting: row.readTable(subsonicSettings),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteSource(int id) async {
|
||||
await sources.deleteWhere((f) => f.id.equals(id));
|
||||
}
|
||||
|
||||
Future<void> updateSource(Source source, SubsonicSetting subsonic) async {
|
||||
await db.transaction(() async {
|
||||
await sources.update().replace(source);
|
||||
await subsonicSettings.update().replace(subsonic);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> createSource(
|
||||
SourcesCompanion source,
|
||||
SubsonicSettingsCompanion subsonic,
|
||||
) async {
|
||||
await db.transaction(() async {
|
||||
final sourceId = await sources.insertOnConflictUpdate(source);
|
||||
|
||||
await subsonicSettings.insertOnConflictUpdate(
|
||||
subsonic.copyWith(sourceId: Value(sourceId)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setActiveSource(int id) async {
|
||||
await transaction(() async {
|
||||
await db.managers.sources.update((o) => o(isActive: Value(null)));
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../sources/models.dart' as models;
|
||||
import 'converters.dart';
|
||||
import 'dao/library_dao.dart';
|
||||
import 'dao/sources_dao.dart';
|
||||
import 'log_interceptor.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@@ -27,14 +28,14 @@ class SubtracksDatabase extends _$SubtracksDatabase {
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
name: 'my_database',
|
||||
name: 'subtracks_database',
|
||||
native: DriftNativeOptions(
|
||||
databasePath: () async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
return path.join(directory.absolute.path, 'subtracks.sqlite');
|
||||
},
|
||||
),
|
||||
);
|
||||
).interceptWith(LogInterceptor());
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1471,6 +1471,15 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
static const VerificationMeta _publicMeta = const VerificationMeta('public');
|
||||
late final GeneratedColumn<bool> public = GeneratedColumn<bool>(
|
||||
'public',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '',
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
sourceId,
|
||||
@@ -1480,6 +1489,7 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
||||
coverArt,
|
||||
created,
|
||||
changed,
|
||||
public,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -1542,6 +1552,12 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
||||
} else if (isInserting) {
|
||||
context.missing(_changedMeta);
|
||||
}
|
||||
if (data.containsKey('public')) {
|
||||
context.handle(
|
||||
_publicMeta,
|
||||
public.isAcceptableOrUnknown(data['public']!, _publicMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1575,6 +1591,10 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}cover_art'],
|
||||
),
|
||||
public: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool,
|
||||
data['${effectivePrefix}public'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1600,6 +1620,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
final Value<String?> coverArt;
|
||||
final Value<DateTime> created;
|
||||
final Value<DateTime> changed;
|
||||
final Value<bool?> public;
|
||||
final Value<int> rowid;
|
||||
const PlaylistsCompanion({
|
||||
this.sourceId = const Value.absent(),
|
||||
@@ -1609,6 +1630,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
this.coverArt = const Value.absent(),
|
||||
this.created = const Value.absent(),
|
||||
this.changed = const Value.absent(),
|
||||
this.public = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
PlaylistsCompanion.insert({
|
||||
@@ -1619,6 +1641,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
this.coverArt = const Value.absent(),
|
||||
required DateTime created,
|
||||
required DateTime changed,
|
||||
this.public = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : sourceId = Value(sourceId),
|
||||
id = Value(id),
|
||||
@@ -1633,6 +1656,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
Expression<String>? coverArt,
|
||||
Expression<DateTime>? created,
|
||||
Expression<DateTime>? changed,
|
||||
Expression<bool>? public,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
@@ -1643,6 +1667,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
if (coverArt != null) 'cover_art': coverArt,
|
||||
if (created != null) 'created': created,
|
||||
if (changed != null) 'changed': changed,
|
||||
if (public != null) 'public': public,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
@@ -1655,6 +1680,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
Value<String?>? coverArt,
|
||||
Value<DateTime>? created,
|
||||
Value<DateTime>? changed,
|
||||
Value<bool?>? public,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return PlaylistsCompanion(
|
||||
@@ -1665,6 +1691,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
coverArt: coverArt ?? this.coverArt,
|
||||
created: created ?? this.created,
|
||||
changed: changed ?? this.changed,
|
||||
public: public ?? this.public,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
@@ -1693,6 +1720,9 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
if (changed.present) {
|
||||
map['changed'] = Variable<DateTime>(changed.value);
|
||||
}
|
||||
if (public.present) {
|
||||
map['public'] = Variable<bool>(public.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
@@ -1709,6 +1739,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
||||
..write('coverArt: $coverArt, ')
|
||||
..write('created: $created, ')
|
||||
..write('changed: $changed, ')
|
||||
..write('public: $public, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
@@ -3432,6 +3463,7 @@ typedef $PlaylistsCreateCompanionBuilder =
|
||||
Value<String?> coverArt,
|
||||
required DateTime created,
|
||||
required DateTime changed,
|
||||
Value<bool?> public,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $PlaylistsUpdateCompanionBuilder =
|
||||
@@ -3443,6 +3475,7 @@ typedef $PlaylistsUpdateCompanionBuilder =
|
||||
Value<String?> coverArt,
|
||||
Value<DateTime> created,
|
||||
Value<DateTime> changed,
|
||||
Value<bool?> public,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
@@ -3489,6 +3522,11 @@ class $PlaylistsFilterComposer
|
||||
column: $table.changed,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> get public => $composableBuilder(
|
||||
column: $table.public,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $PlaylistsOrderingComposer
|
||||
@@ -3534,6 +3572,11 @@ class $PlaylistsOrderingComposer
|
||||
column: $table.changed,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<bool> get public => $composableBuilder(
|
||||
column: $table.public,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $PlaylistsAnnotationComposer
|
||||
@@ -3565,6 +3608,9 @@ class $PlaylistsAnnotationComposer
|
||||
|
||||
GeneratedColumn<DateTime> get changed =>
|
||||
$composableBuilder(column: $table.changed, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<bool> get public =>
|
||||
$composableBuilder(column: $table.public, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $PlaylistsTableManager
|
||||
@@ -3605,6 +3651,7 @@ class $PlaylistsTableManager
|
||||
Value<String?> coverArt = const Value.absent(),
|
||||
Value<DateTime> created = const Value.absent(),
|
||||
Value<DateTime> changed = const Value.absent(),
|
||||
Value<bool?> public = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => PlaylistsCompanion(
|
||||
sourceId: sourceId,
|
||||
@@ -3614,6 +3661,7 @@ class $PlaylistsTableManager
|
||||
coverArt: coverArt,
|
||||
created: created,
|
||||
changed: changed,
|
||||
public: public,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
@@ -3625,6 +3673,7 @@ class $PlaylistsTableManager
|
||||
Value<String?> coverArt = const Value.absent(),
|
||||
required DateTime created,
|
||||
required DateTime changed,
|
||||
Value<bool?> public = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => PlaylistsCompanion.insert(
|
||||
sourceId: sourceId,
|
||||
@@ -3634,6 +3683,7 @@ class $PlaylistsTableManager
|
||||
coverArt: coverArt,
|
||||
created: created,
|
||||
changed: changed,
|
||||
public: public,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
|
||||
120
lib/database/log_interceptor.dart
Normal file
120
lib/database/log_interceptor.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../util/logger.dart';
|
||||
|
||||
/// https://drift.simonbinder.eu/examples/tracing/
|
||||
class LogInterceptor extends QueryInterceptor {
|
||||
Future<T> _run<T>(
|
||||
String description,
|
||||
FutureOr<T> Function() operation,
|
||||
) async {
|
||||
final trace = logger.level >= Level.trace;
|
||||
final stopwatch = trace ? (Stopwatch()..start()) : null;
|
||||
|
||||
logger.t('Running $description');
|
||||
|
||||
try {
|
||||
final result = await operation();
|
||||
if (trace) {
|
||||
logger.t(' => succeeded after ${stopwatch!.elapsedMilliseconds}ms');
|
||||
}
|
||||
return result;
|
||||
} on Object catch (e, st) {
|
||||
if (trace) {
|
||||
logger.t(' => failed after ${stopwatch!.elapsedMilliseconds}ms');
|
||||
}
|
||||
logger.e('Query failed', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction(QueryExecutor parent) {
|
||||
logger.t('begin');
|
||||
return super.beginTransaction(parent);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commitTransaction(TransactionExecutor inner) {
|
||||
return _run('commit', () => inner.send());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rollbackTransaction(TransactionExecutor inner) {
|
||||
return _run('rollback', () => inner.rollback());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(
|
||||
QueryExecutor executor,
|
||||
BatchedStatements statements,
|
||||
) {
|
||||
return _run(
|
||||
'batch with $statements',
|
||||
() => executor.runBatched(statements),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run(
|
||||
'$statement with $args',
|
||||
() => executor.runInsert(statement, args),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run(
|
||||
'$statement with $args',
|
||||
() => executor.runUpdate(statement, args),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run(
|
||||
'$statement with $args',
|
||||
() => executor.runDelete(statement, args),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run(
|
||||
'$statement with $args',
|
||||
() => executor.runCustom(statement, args),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run(
|
||||
'$statement with $args',
|
||||
() => executor.runSelect(statement, args),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,15 @@ enum SongsColumn {
|
||||
starred,
|
||||
disc,
|
||||
track,
|
||||
album,
|
||||
artist,
|
||||
albumArtist,
|
||||
playlistPosition,
|
||||
}
|
||||
|
||||
enum PlaylistsColumn {
|
||||
name,
|
||||
created,
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -36,16 +45,53 @@ abstract class SortingTerm with _$SortingTerm {
|
||||
required AlbumsColumn by,
|
||||
}) = AlbumsSortingTerm;
|
||||
|
||||
static AlbumsSortingTerm albumsAsc(AlbumsColumn by) {
|
||||
return AlbumsSortingTerm(dir: SortDirection.asc, by: by);
|
||||
}
|
||||
|
||||
static AlbumsSortingTerm albumsDesc(AlbumsColumn by) {
|
||||
return AlbumsSortingTerm(dir: SortDirection.desc, by: by);
|
||||
}
|
||||
|
||||
const factory SortingTerm.artists({
|
||||
required SortDirection dir,
|
||||
required ArtistsColumn by,
|
||||
}) = ArtistsSortingTerm;
|
||||
|
||||
static ArtistsSortingTerm artistsAsc(ArtistsColumn by) {
|
||||
return ArtistsSortingTerm(dir: SortDirection.asc, by: by);
|
||||
}
|
||||
|
||||
static ArtistsSortingTerm artistsDesc(ArtistsColumn by) {
|
||||
return ArtistsSortingTerm(dir: SortDirection.desc, by: by);
|
||||
}
|
||||
|
||||
const factory SortingTerm.songs({
|
||||
required SortDirection dir,
|
||||
required SongsColumn by,
|
||||
}) = SongsSortingTerm;
|
||||
|
||||
static SongsSortingTerm songsAsc(SongsColumn by) {
|
||||
return SongsSortingTerm(dir: SortDirection.asc, by: by);
|
||||
}
|
||||
|
||||
static SongsSortingTerm songsDesc(SongsColumn by) {
|
||||
return SongsSortingTerm(dir: SortDirection.desc, by: by);
|
||||
}
|
||||
|
||||
const factory SortingTerm.playlists({
|
||||
required SortDirection dir,
|
||||
required PlaylistsColumn by,
|
||||
}) = PlaylistsSortingTerm;
|
||||
|
||||
static PlaylistsSortingTerm playlistsAsc(PlaylistsColumn by) {
|
||||
return PlaylistsSortingTerm(dir: SortDirection.asc, by: by);
|
||||
}
|
||||
|
||||
static PlaylistsSortingTerm playlistsDesc(PlaylistsColumn by) {
|
||||
return PlaylistsSortingTerm(dir: SortDirection.desc, by: by);
|
||||
}
|
||||
|
||||
factory SortingTerm.fromJson(Map<String, Object?> json) =>
|
||||
_$SortingTermFromJson(json);
|
||||
}
|
||||
@@ -119,3 +165,27 @@ abstract class SongsFilter with _$SongsFilter {
|
||||
factory SongsFilter.fromJson(Map<String, Object?> json) =>
|
||||
_$SongsFilterFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PlaylistsQuery with _$PlaylistsQuery {
|
||||
const factory PlaylistsQuery({
|
||||
required int sourceId,
|
||||
@Default(IListConst([])) IList<PlaylistsFilter> filter,
|
||||
required IList<PlaylistsSortingTerm> sort,
|
||||
int? limit,
|
||||
int? offset,
|
||||
}) = _PlaylistsQuery;
|
||||
|
||||
factory PlaylistsQuery.fromJson(Map<String, Object?> json) =>
|
||||
_$PlaylistsQueryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PlaylistsFilter with _$PlaylistsFilter {
|
||||
const factory PlaylistsFilter.nameSearch(String name) =
|
||||
PlaylistsFilterNameSearch;
|
||||
const factory PlaylistsFilter.public(bool public) = PlaylistsFilterPublic;
|
||||
|
||||
factory PlaylistsFilter.fromJson(Map<String, Object?> json) =>
|
||||
_$PlaylistsFilterFromJson(json);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ SortingTerm _$SortingTermFromJson(
|
||||
return SongsSortingTerm.fromJson(
|
||||
json
|
||||
);
|
||||
case 'playlists':
|
||||
return PlaylistsSortingTerm.fromJson(
|
||||
json
|
||||
);
|
||||
|
||||
default:
|
||||
throw CheckedFromJsonException(
|
||||
@@ -116,13 +120,14 @@ extension SortingTermPatterns on SortingTerm {
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( AlbumsSortingTerm value)? albums,TResult Function( ArtistsSortingTerm value)? artists,TResult Function( SongsSortingTerm value)? songs,required TResult orElse(),}){
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( AlbumsSortingTerm value)? albums,TResult Function( ArtistsSortingTerm value)? artists,TResult Function( SongsSortingTerm value)? songs,TResult Function( PlaylistsSortingTerm value)? playlists,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AlbumsSortingTerm() when albums != null:
|
||||
return albums(_that);case ArtistsSortingTerm() when artists != null:
|
||||
return artists(_that);case SongsSortingTerm() when songs != null:
|
||||
return songs(_that);case _:
|
||||
return songs(_that);case PlaylistsSortingTerm() when playlists != null:
|
||||
return playlists(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -140,13 +145,14 @@ return songs(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( AlbumsSortingTerm value) albums,required TResult Function( ArtistsSortingTerm value) artists,required TResult Function( SongsSortingTerm value) songs,}){
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( AlbumsSortingTerm value) albums,required TResult Function( ArtistsSortingTerm value) artists,required TResult Function( SongsSortingTerm value) songs,required TResult Function( PlaylistsSortingTerm value) playlists,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AlbumsSortingTerm():
|
||||
return albums(_that);case ArtistsSortingTerm():
|
||||
return artists(_that);case SongsSortingTerm():
|
||||
return songs(_that);case _:
|
||||
return songs(_that);case PlaylistsSortingTerm():
|
||||
return playlists(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -163,13 +169,14 @@ return songs(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( AlbumsSortingTerm value)? albums,TResult? Function( ArtistsSortingTerm value)? artists,TResult? Function( SongsSortingTerm value)? songs,}){
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( AlbumsSortingTerm value)? albums,TResult? Function( ArtistsSortingTerm value)? artists,TResult? Function( SongsSortingTerm value)? songs,TResult? Function( PlaylistsSortingTerm value)? playlists,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AlbumsSortingTerm() when albums != null:
|
||||
return albums(_that);case ArtistsSortingTerm() when artists != null:
|
||||
return artists(_that);case SongsSortingTerm() when songs != null:
|
||||
return songs(_that);case _:
|
||||
return songs(_that);case PlaylistsSortingTerm() when playlists != null:
|
||||
return playlists(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -186,12 +193,13 @@ return songs(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( SortDirection dir, AlbumsColumn by)? albums,TResult Function( SortDirection dir, ArtistsColumn by)? artists,TResult Function( SortDirection dir, SongsColumn by)? songs,required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( SortDirection dir, AlbumsColumn by)? albums,TResult Function( SortDirection dir, ArtistsColumn by)? artists,TResult Function( SortDirection dir, SongsColumn by)? songs,TResult Function( SortDirection dir, PlaylistsColumn by)? playlists,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AlbumsSortingTerm() when albums != null:
|
||||
return albums(_that.dir,_that.by);case ArtistsSortingTerm() when artists != null:
|
||||
return artists(_that.dir,_that.by);case SongsSortingTerm() when songs != null:
|
||||
return songs(_that.dir,_that.by);case _:
|
||||
return songs(_that.dir,_that.by);case PlaylistsSortingTerm() when playlists != null:
|
||||
return playlists(_that.dir,_that.by);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -209,12 +217,13 @@ return songs(_that.dir,_that.by);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( SortDirection dir, AlbumsColumn by) albums,required TResult Function( SortDirection dir, ArtistsColumn by) artists,required TResult Function( SortDirection dir, SongsColumn by) songs,}) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( SortDirection dir, AlbumsColumn by) albums,required TResult Function( SortDirection dir, ArtistsColumn by) artists,required TResult Function( SortDirection dir, SongsColumn by) songs,required TResult Function( SortDirection dir, PlaylistsColumn by) playlists,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AlbumsSortingTerm():
|
||||
return albums(_that.dir,_that.by);case ArtistsSortingTerm():
|
||||
return artists(_that.dir,_that.by);case SongsSortingTerm():
|
||||
return songs(_that.dir,_that.by);case _:
|
||||
return songs(_that.dir,_that.by);case PlaylistsSortingTerm():
|
||||
return playlists(_that.dir,_that.by);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -231,12 +240,13 @@ return songs(_that.dir,_that.by);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( SortDirection dir, AlbumsColumn by)? albums,TResult? Function( SortDirection dir, ArtistsColumn by)? artists,TResult? Function( SortDirection dir, SongsColumn by)? songs,}) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( SortDirection dir, AlbumsColumn by)? albums,TResult? Function( SortDirection dir, ArtistsColumn by)? artists,TResult? Function( SortDirection dir, SongsColumn by)? songs,TResult? Function( SortDirection dir, PlaylistsColumn by)? playlists,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AlbumsSortingTerm() when albums != null:
|
||||
return albums(_that.dir,_that.by);case ArtistsSortingTerm() when artists != null:
|
||||
return artists(_that.dir,_that.by);case SongsSortingTerm() when songs != null:
|
||||
return songs(_that.dir,_that.by);case _:
|
||||
return songs(_that.dir,_that.by);case PlaylistsSortingTerm() when playlists != null:
|
||||
return playlists(_that.dir,_that.by);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -467,6 +477,81 @@ as SongsColumn,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class PlaylistsSortingTerm implements SortingTerm {
|
||||
const PlaylistsSortingTerm({required this.dir, required this.by, final String? $type}): $type = $type ?? 'playlists';
|
||||
factory PlaylistsSortingTerm.fromJson(Map<String, dynamic> json) => _$PlaylistsSortingTermFromJson(json);
|
||||
|
||||
@override final SortDirection dir;
|
||||
@override final PlaylistsColumn by;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of SortingTerm
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PlaylistsSortingTermCopyWith<PlaylistsSortingTerm> get copyWith => _$PlaylistsSortingTermCopyWithImpl<PlaylistsSortingTerm>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PlaylistsSortingTermToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PlaylistsSortingTerm&&(identical(other.dir, dir) || other.dir == dir)&&(identical(other.by, by) || other.by == by));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,dir,by);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SortingTerm.playlists(dir: $dir, by: $by)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PlaylistsSortingTermCopyWith<$Res> implements $SortingTermCopyWith<$Res> {
|
||||
factory $PlaylistsSortingTermCopyWith(PlaylistsSortingTerm value, $Res Function(PlaylistsSortingTerm) _then) = _$PlaylistsSortingTermCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
SortDirection dir, PlaylistsColumn by
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PlaylistsSortingTermCopyWithImpl<$Res>
|
||||
implements $PlaylistsSortingTermCopyWith<$Res> {
|
||||
_$PlaylistsSortingTermCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PlaylistsSortingTerm _self;
|
||||
final $Res Function(PlaylistsSortingTerm) _then;
|
||||
|
||||
/// Create a copy of SortingTerm
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? dir = null,Object? by = null,}) {
|
||||
return _then(PlaylistsSortingTerm(
|
||||
dir: null == dir ? _self.dir : dir // ignore: cast_nullable_to_non_nullable
|
||||
as SortDirection,by: null == by ? _self.by : by // ignore: cast_nullable_to_non_nullable
|
||||
as PlaylistsColumn,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2306,6 +2391,619 @@ as String,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PlaylistsQuery {
|
||||
|
||||
int get sourceId; IList<PlaylistsFilter> get filter; IList<PlaylistsSortingTerm> get sort; int? get limit; int? get offset;
|
||||
/// Create a copy of PlaylistsQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PlaylistsQueryCopyWith<PlaylistsQuery> get copyWith => _$PlaylistsQueryCopyWithImpl<PlaylistsQuery>(this as PlaylistsQuery, _$identity);
|
||||
|
||||
/// Serializes this PlaylistsQuery to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PlaylistsQuery&&(identical(other.sourceId, sourceId) || other.sourceId == sourceId)&&const DeepCollectionEquality().equals(other.filter, filter)&&const DeepCollectionEquality().equals(other.sort, sort)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,sourceId,const DeepCollectionEquality().hash(filter),const DeepCollectionEquality().hash(sort),limit,offset);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaylistsQuery(sourceId: $sourceId, filter: $filter, sort: $sort, limit: $limit, offset: $offset)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PlaylistsQueryCopyWith<$Res> {
|
||||
factory $PlaylistsQueryCopyWith(PlaylistsQuery value, $Res Function(PlaylistsQuery) _then) = _$PlaylistsQueryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int sourceId, IList<PlaylistsFilter> filter, IList<PlaylistsSortingTerm> sort, int? limit, int? offset
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PlaylistsQueryCopyWithImpl<$Res>
|
||||
implements $PlaylistsQueryCopyWith<$Res> {
|
||||
_$PlaylistsQueryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PlaylistsQuery _self;
|
||||
final $Res Function(PlaylistsQuery) _then;
|
||||
|
||||
/// Create a copy of PlaylistsQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? sourceId = null,Object? filter = null,Object? sort = null,Object? limit = freezed,Object? offset = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
sourceId: null == sourceId ? _self.sourceId : sourceId // ignore: cast_nullable_to_non_nullable
|
||||
as int,filter: null == filter ? _self.filter : filter // ignore: cast_nullable_to_non_nullable
|
||||
as IList<PlaylistsFilter>,sort: null == sort ? _self.sort : sort // ignore: cast_nullable_to_non_nullable
|
||||
as IList<PlaylistsSortingTerm>,limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable
|
||||
as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PlaylistsQuery].
|
||||
extension PlaylistsQueryPatterns on PlaylistsQuery {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PlaylistsQuery value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaylistsQuery() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PlaylistsQuery value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaylistsQuery():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PlaylistsQuery value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaylistsQuery() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int sourceId, IList<PlaylistsFilter> filter, IList<PlaylistsSortingTerm> sort, int? limit, int? offset)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaylistsQuery() when $default != null:
|
||||
return $default(_that.sourceId,_that.filter,_that.sort,_that.limit,_that.offset);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int sourceId, IList<PlaylistsFilter> filter, IList<PlaylistsSortingTerm> sort, int? limit, int? offset) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaylistsQuery():
|
||||
return $default(_that.sourceId,_that.filter,_that.sort,_that.limit,_that.offset);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int sourceId, IList<PlaylistsFilter> filter, IList<PlaylistsSortingTerm> sort, int? limit, int? offset)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaylistsQuery() when $default != null:
|
||||
return $default(_that.sourceId,_that.filter,_that.sort,_that.limit,_that.offset);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _PlaylistsQuery implements PlaylistsQuery {
|
||||
const _PlaylistsQuery({required this.sourceId, this.filter = const IListConst([]), required this.sort, this.limit, this.offset});
|
||||
factory _PlaylistsQuery.fromJson(Map<String, dynamic> json) => _$PlaylistsQueryFromJson(json);
|
||||
|
||||
@override final int sourceId;
|
||||
@override@JsonKey() final IList<PlaylistsFilter> filter;
|
||||
@override final IList<PlaylistsSortingTerm> sort;
|
||||
@override final int? limit;
|
||||
@override final int? offset;
|
||||
|
||||
/// Create a copy of PlaylistsQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PlaylistsQueryCopyWith<_PlaylistsQuery> get copyWith => __$PlaylistsQueryCopyWithImpl<_PlaylistsQuery>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PlaylistsQueryToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PlaylistsQuery&&(identical(other.sourceId, sourceId) || other.sourceId == sourceId)&&const DeepCollectionEquality().equals(other.filter, filter)&&const DeepCollectionEquality().equals(other.sort, sort)&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,sourceId,const DeepCollectionEquality().hash(filter),const DeepCollectionEquality().hash(sort),limit,offset);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaylistsQuery(sourceId: $sourceId, filter: $filter, sort: $sort, limit: $limit, offset: $offset)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PlaylistsQueryCopyWith<$Res> implements $PlaylistsQueryCopyWith<$Res> {
|
||||
factory _$PlaylistsQueryCopyWith(_PlaylistsQuery value, $Res Function(_PlaylistsQuery) _then) = __$PlaylistsQueryCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int sourceId, IList<PlaylistsFilter> filter, IList<PlaylistsSortingTerm> sort, int? limit, int? offset
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PlaylistsQueryCopyWithImpl<$Res>
|
||||
implements _$PlaylistsQueryCopyWith<$Res> {
|
||||
__$PlaylistsQueryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PlaylistsQuery _self;
|
||||
final $Res Function(_PlaylistsQuery) _then;
|
||||
|
||||
/// Create a copy of PlaylistsQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? sourceId = null,Object? filter = null,Object? sort = null,Object? limit = freezed,Object? offset = freezed,}) {
|
||||
return _then(_PlaylistsQuery(
|
||||
sourceId: null == sourceId ? _self.sourceId : sourceId // ignore: cast_nullable_to_non_nullable
|
||||
as int,filter: null == filter ? _self.filter : filter // ignore: cast_nullable_to_non_nullable
|
||||
as IList<PlaylistsFilter>,sort: null == sort ? _self.sort : sort // ignore: cast_nullable_to_non_nullable
|
||||
as IList<PlaylistsSortingTerm>,limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable
|
||||
as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
PlaylistsFilter _$PlaylistsFilterFromJson(
|
||||
Map<String, dynamic> json
|
||||
) {
|
||||
switch (json['runtimeType']) {
|
||||
case 'nameSearch':
|
||||
return PlaylistsFilterNameSearch.fromJson(
|
||||
json
|
||||
);
|
||||
case 'public':
|
||||
return PlaylistsFilterPublic.fromJson(
|
||||
json
|
||||
);
|
||||
|
||||
default:
|
||||
throw CheckedFromJsonException(
|
||||
json,
|
||||
'runtimeType',
|
||||
'PlaylistsFilter',
|
||||
'Invalid union type "${json['runtimeType']}"!'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PlaylistsFilter {
|
||||
|
||||
|
||||
|
||||
/// Serializes this PlaylistsFilter to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PlaylistsFilter);
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaylistsFilter()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class $PlaylistsFilterCopyWith<$Res> {
|
||||
$PlaylistsFilterCopyWith(PlaylistsFilter _, $Res Function(PlaylistsFilter) __);
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PlaylistsFilter].
|
||||
extension PlaylistsFilterPatterns on PlaylistsFilter {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( PlaylistsFilterNameSearch value)? nameSearch,TResult Function( PlaylistsFilterPublic value)? public,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case PlaylistsFilterNameSearch() when nameSearch != null:
|
||||
return nameSearch(_that);case PlaylistsFilterPublic() when public != null:
|
||||
return public(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( PlaylistsFilterNameSearch value) nameSearch,required TResult Function( PlaylistsFilterPublic value) public,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case PlaylistsFilterNameSearch():
|
||||
return nameSearch(_that);case PlaylistsFilterPublic():
|
||||
return public(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( PlaylistsFilterNameSearch value)? nameSearch,TResult? Function( PlaylistsFilterPublic value)? public,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case PlaylistsFilterNameSearch() when nameSearch != null:
|
||||
return nameSearch(_that);case PlaylistsFilterPublic() when public != null:
|
||||
return public(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String name)? nameSearch,TResult Function( bool public)? public,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case PlaylistsFilterNameSearch() when nameSearch != null:
|
||||
return nameSearch(_that.name);case PlaylistsFilterPublic() when public != null:
|
||||
return public(_that.public);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String name) nameSearch,required TResult Function( bool public) public,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case PlaylistsFilterNameSearch():
|
||||
return nameSearch(_that.name);case PlaylistsFilterPublic():
|
||||
return public(_that.public);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String name)? nameSearch,TResult? Function( bool public)? public,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case PlaylistsFilterNameSearch() when nameSearch != null:
|
||||
return nameSearch(_that.name);case PlaylistsFilterPublic() when public != null:
|
||||
return public(_that.public);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class PlaylistsFilterNameSearch implements PlaylistsFilter {
|
||||
const PlaylistsFilterNameSearch(this.name, {final String? $type}): $type = $type ?? 'nameSearch';
|
||||
factory PlaylistsFilterNameSearch.fromJson(Map<String, dynamic> json) => _$PlaylistsFilterNameSearchFromJson(json);
|
||||
|
||||
final String name;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of PlaylistsFilter
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PlaylistsFilterNameSearchCopyWith<PlaylistsFilterNameSearch> get copyWith => _$PlaylistsFilterNameSearchCopyWithImpl<PlaylistsFilterNameSearch>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PlaylistsFilterNameSearchToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PlaylistsFilterNameSearch&&(identical(other.name, name) || other.name == name));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaylistsFilter.nameSearch(name: $name)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PlaylistsFilterNameSearchCopyWith<$Res> implements $PlaylistsFilterCopyWith<$Res> {
|
||||
factory $PlaylistsFilterNameSearchCopyWith(PlaylistsFilterNameSearch value, $Res Function(PlaylistsFilterNameSearch) _then) = _$PlaylistsFilterNameSearchCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String name
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PlaylistsFilterNameSearchCopyWithImpl<$Res>
|
||||
implements $PlaylistsFilterNameSearchCopyWith<$Res> {
|
||||
_$PlaylistsFilterNameSearchCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PlaylistsFilterNameSearch _self;
|
||||
final $Res Function(PlaylistsFilterNameSearch) _then;
|
||||
|
||||
/// Create a copy of PlaylistsFilter
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? name = null,}) {
|
||||
return _then(PlaylistsFilterNameSearch(
|
||||
null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class PlaylistsFilterPublic implements PlaylistsFilter {
|
||||
const PlaylistsFilterPublic(this.public, {final String? $type}): $type = $type ?? 'public';
|
||||
factory PlaylistsFilterPublic.fromJson(Map<String, dynamic> json) => _$PlaylistsFilterPublicFromJson(json);
|
||||
|
||||
final bool public;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of PlaylistsFilter
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PlaylistsFilterPublicCopyWith<PlaylistsFilterPublic> get copyWith => _$PlaylistsFilterPublicCopyWithImpl<PlaylistsFilterPublic>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PlaylistsFilterPublicToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PlaylistsFilterPublic&&(identical(other.public, public) || other.public == public));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,public);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaylistsFilter.public(public: $public)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PlaylistsFilterPublicCopyWith<$Res> implements $PlaylistsFilterCopyWith<$Res> {
|
||||
factory $PlaylistsFilterPublicCopyWith(PlaylistsFilterPublic value, $Res Function(PlaylistsFilterPublic) _then) = _$PlaylistsFilterPublicCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool public
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PlaylistsFilterPublicCopyWithImpl<$Res>
|
||||
implements $PlaylistsFilterPublicCopyWith<$Res> {
|
||||
_$PlaylistsFilterPublicCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PlaylistsFilterPublic _self;
|
||||
final $Res Function(PlaylistsFilterPublic) _then;
|
||||
|
||||
/// Create a copy of PlaylistsFilter
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? public = null,}) {
|
||||
return _then(PlaylistsFilterPublic(
|
||||
null == public ? _self.public : public // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -71,6 +71,31 @@ const _$SongsColumnEnumMap = {
|
||||
SongsColumn.starred: 'starred',
|
||||
SongsColumn.disc: 'disc',
|
||||
SongsColumn.track: 'track',
|
||||
SongsColumn.album: 'album',
|
||||
SongsColumn.artist: 'artist',
|
||||
SongsColumn.albumArtist: 'albumArtist',
|
||||
SongsColumn.playlistPosition: 'playlistPosition',
|
||||
};
|
||||
|
||||
PlaylistsSortingTerm _$PlaylistsSortingTermFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PlaylistsSortingTerm(
|
||||
dir: $enumDecode(_$SortDirectionEnumMap, json['dir']),
|
||||
by: $enumDecode(_$PlaylistsColumnEnumMap, json['by']),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlaylistsSortingTermToJson(
|
||||
PlaylistsSortingTerm instance,
|
||||
) => <String, dynamic>{
|
||||
'dir': _$SortDirectionEnumMap[instance.dir]!,
|
||||
'by': _$PlaylistsColumnEnumMap[instance.by]!,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
const _$PlaylistsColumnEnumMap = {
|
||||
PlaylistsColumn.name: 'name',
|
||||
PlaylistsColumn.created: 'created',
|
||||
};
|
||||
|
||||
_AlbumsQuery _$AlbumsQueryFromJson(Map<String, dynamic> json) => _AlbumsQuery(
|
||||
@@ -224,3 +249,55 @@ Map<String, dynamic> _$SongsFilterPlaylistIdToJson(
|
||||
'playlistId': instance.playlistId,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_PlaylistsQuery _$PlaylistsQueryFromJson(Map<String, dynamic> json) =>
|
||||
_PlaylistsQuery(
|
||||
sourceId: (json['sourceId'] as num).toInt(),
|
||||
filter: json['filter'] == null
|
||||
? const IListConst([])
|
||||
: IList<PlaylistsFilter>.fromJson(
|
||||
json['filter'],
|
||||
(value) =>
|
||||
PlaylistsFilter.fromJson(value as Map<String, dynamic>),
|
||||
),
|
||||
sort: IList<PlaylistsSortingTerm>.fromJson(
|
||||
json['sort'],
|
||||
(value) => PlaylistsSortingTerm.fromJson(value as Map<String, dynamic>),
|
||||
),
|
||||
limit: (json['limit'] as num?)?.toInt(),
|
||||
offset: (json['offset'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlaylistsQueryToJson(_PlaylistsQuery instance) =>
|
||||
<String, dynamic>{
|
||||
'sourceId': instance.sourceId,
|
||||
'filter': instance.filter.toJson((value) => value),
|
||||
'sort': instance.sort.toJson((value) => value),
|
||||
'limit': instance.limit,
|
||||
'offset': instance.offset,
|
||||
};
|
||||
|
||||
PlaylistsFilterNameSearch _$PlaylistsFilterNameSearchFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PlaylistsFilterNameSearch(
|
||||
json['name'] as String,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlaylistsFilterNameSearchToJson(
|
||||
PlaylistsFilterNameSearch instance,
|
||||
) => <String, dynamic>{'name': instance.name, 'runtimeType': instance.$type};
|
||||
|
||||
PlaylistsFilterPublic _$PlaylistsFilterPublicFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PlaylistsFilterPublic(
|
||||
json['public'] as bool,
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlaylistsFilterPublicToJson(
|
||||
PlaylistsFilterPublic instance,
|
||||
) => <String, dynamic>{
|
||||
'public': instance.public,
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
@@ -136,6 +136,7 @@ CREATE TABLE playlists(
|
||||
cover_art TEXT,
|
||||
created DATETIME NOT NULL,
|
||||
changed DATETIME NOT NULL,
|
||||
public BOOLEAN,
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Playlist;
|
||||
|
||||
@@ -34,6 +34,7 @@ Playlist mapPlaylist(XmlElement e) => Playlist(
|
||||
coverArt: e.getAttribute('coverArt'),
|
||||
created: DateTime.parse(e.getAttribute('created')!),
|
||||
changed: DateTime.parse(e.getAttribute('changed')!),
|
||||
public: bool.tryParse(e.getAttribute('public') ?? ''),
|
||||
owner: e.getAttribute('owner'),
|
||||
);
|
||||
|
||||
|
||||
48
lib/util/logger.dart
Normal file
48
lib/util/logger.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
class LogLevelFilter extends LogFilter {
|
||||
@override
|
||||
bool shouldLog(LogEvent event) {
|
||||
return event.level >= level!;
|
||||
}
|
||||
}
|
||||
|
||||
class SubtracksLogger extends Logger {
|
||||
SubtracksLogger({
|
||||
super.filter,
|
||||
super.printer,
|
||||
super.output,
|
||||
required Level level,
|
||||
}) : _level = level,
|
||||
super(level: level);
|
||||
|
||||
final Level _level;
|
||||
Level get level => _level;
|
||||
}
|
||||
|
||||
SubtracksLogger createLogger() {
|
||||
var isDebug = false;
|
||||
assert(() {
|
||||
isDebug = true;
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (isDebug) {
|
||||
return SubtracksLogger(
|
||||
filter: DevelopmentFilter(),
|
||||
printer: PrettyPrinter(),
|
||||
output: ConsoleOutput(),
|
||||
level: Level.debug,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: production logger
|
||||
return SubtracksLogger(
|
||||
filter: DevelopmentFilter(),
|
||||
printer: PrettyPrinter(),
|
||||
output: ConsoleOutput(),
|
||||
level: Level.debug,
|
||||
);
|
||||
}
|
||||
|
||||
final logger = createLogger();
|
||||
@@ -565,6 +565,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -26,6 +26,7 @@ dependencies:
|
||||
infinite_scroll_pagination: ^5.1.1
|
||||
intl: any
|
||||
json_annotation: ^4.9.0
|
||||
logger: ^2.6.2
|
||||
material_color_utilities: ^0.11.1
|
||||
material_symbols_icons: ^4.2874.0
|
||||
octo_image: ^2.1.0
|
||||
|
||||
Reference in New Issue
Block a user