mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
Compare commits
19 Commits
064440b0ee
...
flutter-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad6d534286 | ||
|
|
2837d4576e | ||
|
|
7f6ba4776a | ||
|
|
f7874bcead | ||
|
|
ba169092fd | ||
|
|
4183e2d3b9 | ||
|
|
c3bb14edbf | ||
|
|
805e6fff7a | ||
|
|
d245fc7fef | ||
|
|
3fcb938f2b | ||
|
|
97ea3c3230 | ||
|
|
71132a1f0e | ||
|
|
f3969dc6af | ||
|
|
a4e4c6fa57 | ||
|
|
16a79c81cb | ||
|
|
6609671ae2 | ||
|
|
b9a094c1c4 | ||
|
|
fd800b0e12 | ||
|
|
b6153ce3b6 |
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:
|
||||||
22
lib/app/hooks/use_on_source.dart
Normal file
22
lib/app/hooks/use_on_source.dart
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../state/services.dart';
|
||||||
|
import '../state/source.dart';
|
||||||
|
|
||||||
|
void useOnSourceChange(WidgetRef ref, void Function(int sourceId) callback) {
|
||||||
|
final sourceId = ref.watch(sourceIdProvider);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
callback(sourceId);
|
||||||
|
return;
|
||||||
|
}, [sourceId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void useOnSourceSync(WidgetRef ref, void Function() callback) {
|
||||||
|
final syncService = ref.watch(syncServiceProvider);
|
||||||
|
|
||||||
|
useOnListenableChange(syncService, () {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../images/images.dart';
|
|
||||||
import '../../sources/models.dart';
|
|
||||||
import '../util/clip.dart';
|
|
||||||
|
|
||||||
class AlbumGridTile extends HookConsumerWidget {
|
|
||||||
const AlbumGridTile({
|
|
||||||
super.key,
|
|
||||||
required this.album,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Album album;
|
|
||||||
final void Function()? onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return CardTheme(
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadiusGeometry.circular(3),
|
|
||||||
),
|
|
||||||
margin: EdgeInsets.all(2),
|
|
||||||
child: ImageCard(
|
|
||||||
onTap: onTap,
|
|
||||||
child: CoverArtImage(coverArt: album.coverArt),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistListTile extends StatelessWidget {
|
|
||||||
const ArtistListTile({
|
|
||||||
super.key,
|
|
||||||
required this.artist,
|
|
||||||
this.albumCount,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Artist artist;
|
|
||||||
final int? albumCount;
|
|
||||||
final void Function()? onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
leading: CircleClip(
|
|
||||||
child: CoverArtImage(coverArt: artist.coverArt),
|
|
||||||
),
|
|
||||||
title: Text(artist.name),
|
|
||||||
subtitle: albumCount != null ? Text('$albumCount albums') : null,
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageCard extends StatelessWidget {
|
|
||||||
const ImageCard({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
this.onTap,
|
|
||||||
this.onLongPress,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final void Function()? onTap;
|
|
||||||
final void Function()? onLongPress;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.passthrough,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
child,
|
|
||||||
Positioned.fill(
|
|
||||||
child: Material(
|
|
||||||
type: MaterialType.transparency,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
onLongPress: onLongPress,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,9 +4,11 @@ import 'screens/album_screen.dart';
|
|||||||
import 'screens/artist_screen.dart';
|
import 'screens/artist_screen.dart';
|
||||||
import 'screens/library_screen.dart';
|
import 'screens/library_screen.dart';
|
||||||
import 'screens/now_playing_screen.dart';
|
import 'screens/now_playing_screen.dart';
|
||||||
|
import 'screens/playlist_screen.dart';
|
||||||
import 'screens/preload_screen.dart';
|
import 'screens/preload_screen.dart';
|
||||||
import 'screens/root_shell_screen.dart';
|
import 'screens/root_shell_screen.dart';
|
||||||
import 'screens/settings_screen.dart';
|
import 'screens/settings_screen.dart';
|
||||||
|
import 'screens/settings_source_screen.dart';
|
||||||
|
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
initialLocation: '/preload',
|
initialLocation: '/preload',
|
||||||
@@ -23,13 +25,19 @@ final router = GoRouter(
|
|||||||
builder: (context, state) => LibraryScreen(),
|
builder: (context, state) => LibraryScreen(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'album/:id',
|
path: 'albums/:id',
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
AlbumScreen(id: state.pathParameters['id']!),
|
AlbumScreen(id: state.pathParameters['id']!),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'artist',
|
path: 'artists/:id',
|
||||||
builder: (context, state) => ArtistScreen(),
|
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',
|
path: '/settings',
|
||||||
builder: (context, state) => SettingsScreen(),
|
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!));
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class AlbumScreen extends StatelessWidget {
|
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 AlbumScreen extends HookConsumerWidget {
|
||||||
const AlbumScreen({
|
const AlbumScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -11,23 +22,52 @@ class AlbumScreen extends StatelessWidget {
|
|||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
final l = AppLocalizations.of(context);
|
||||||
body: Center(
|
|
||||||
child: Column(
|
final db = ref.watch(databaseProvider);
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
final sourceId = ref.watch(sourceIdProvider);
|
||||||
children: [
|
|
||||||
Text('Album $id!'),
|
final getAlbum = useMemoized(
|
||||||
TextButton(
|
() => db.libraryDao.getAlbum(sourceId, id).getSingle(),
|
||||||
onPressed: () {
|
);
|
||||||
context.push('/artist');
|
final album = useFuture(getAlbum).data;
|
||||||
},
|
|
||||||
child: Text('Artist...'),
|
if (album == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = SongsQuery(
|
||||||
|
sourceId: sourceId,
|
||||||
|
filter: IList([SongsFilter.albumId(album.id)]),
|
||||||
|
sort: IList([
|
||||||
|
SortingTerm.songsAsc(SongsColumn.disc),
|
||||||
|
SortingTerm.songsAsc(SongsColumn.track),
|
||||||
|
SortingTerm.songsAsc(SongsColumn.title),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return CoverArtTheme(
|
||||||
|
coverArt: album.coverArt,
|
||||||
|
child: Scaffold(
|
||||||
|
body: GradientScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SongsListHeader(
|
||||||
|
title: album.name,
|
||||||
|
subtitle: album.albumArtist,
|
||||||
|
coverArt: album.coverArt,
|
||||||
|
playText: l.resourcesAlbumActionsPlay,
|
||||||
|
onPlay: () {},
|
||||||
|
onMore: () {},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
CachedNetworkImage(
|
SongsList(
|
||||||
imageUrl: 'https://placehold.net/400x400.png',
|
query: query,
|
||||||
placeholder: (context, url) => CircularProgressIndicator(),
|
itemBuilder: (context, item, index) => SongListTile(
|
||||||
errorWidget: (context, url, error) => Icon(Icons.error),
|
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/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class ArtistScreen extends StatelessWidget {
|
import '../../database/query.dart';
|
||||||
const ArtistScreen({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
final db = ref.watch(databaseProvider);
|
||||||
body: Center(child: Text('Artist!')),
|
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,17 +5,21 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
import '../../l10n/generated/app_localizations.dart';
|
import '../../l10n/generated/app_localizations.dart';
|
||||||
import '../lists/albums_grid.dart';
|
import '../state/lists.dart';
|
||||||
import '../lists/artists_list.dart';
|
|
||||||
import '../state/services.dart';
|
import '../state/services.dart';
|
||||||
import '../ui/text.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';
|
import '../util/custom_scroll_fix.dart';
|
||||||
|
|
||||||
const kIconSize = 26.0;
|
const kIconSize = 26.0;
|
||||||
const kTabHeight = 36.0;
|
const kTabHeight = 36.0;
|
||||||
|
|
||||||
enum LibraryTab {
|
enum LibraryTab {
|
||||||
home(Icon(Symbols.home_rounded)),
|
// home(Icon(Symbols.home_rounded)),
|
||||||
albums(Icon(Symbols.album_rounded)),
|
albums(Icon(Symbols.album_rounded)),
|
||||||
artists(Icon(Symbols.person_rounded)),
|
artists(Icon(Symbols.person_rounded)),
|
||||||
songs(Icon(Symbols.music_note_rounded)),
|
songs(Icon(Symbols.music_note_rounded)),
|
||||||
@@ -36,7 +40,7 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final tabController = useTabController(
|
final tabController = useTabController(
|
||||||
initialLength: LibraryTab.values.length,
|
initialLength: LibraryTab.values.length,
|
||||||
initialIndex: 1,
|
initialIndex: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -52,20 +56,7 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
builder: (context) => CustomScrollProvider(
|
builder: (context) => CustomScrollProvider(
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
parent: PrimaryScrollController.of(context),
|
parent: PrimaryScrollController.of(context),
|
||||||
child: TabBarView(
|
child: LibraryTabBarView(tabController: tabController),
|
||||||
controller: tabController,
|
|
||||||
children: LibraryTab.values
|
|
||||||
.map(
|
|
||||||
(tab) => TabScrollView(
|
|
||||||
index: LibraryTab.values.indexOf(tab),
|
|
||||||
sliver: switch (tab) {
|
|
||||||
LibraryTab.albums => AlbumsGrid(),
|
|
||||||
_ => ArtistsList(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -73,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 {
|
class LibraryTabsHeader extends HookConsumerWidget {
|
||||||
const LibraryTabsHeader({
|
const LibraryTabsHeader({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -162,10 +200,11 @@ class TabTitleText extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
|
final text = TextTheme.of(context);
|
||||||
|
|
||||||
String tabLocalization(LibraryTab tab) => switch (tab) {
|
String tabLocalization(LibraryTab tab) => switch (tab) {
|
||||||
LibraryTab.albums => l.navigationTabsAlbums,
|
LibraryTab.albums => l.navigationTabsAlbums,
|
||||||
LibraryTab.home => l.navigationTabsHome,
|
// LibraryTab.home => l.navigationTabsHome,
|
||||||
LibraryTab.artists => l.navigationTabsArtists,
|
LibraryTab.artists => l.navigationTabsArtists,
|
||||||
LibraryTab.songs => l.navigationTabsSongs,
|
LibraryTab.songs => l.navigationTabsSongs,
|
||||||
LibraryTab.playlists => l.navigationTabsPlaylists,
|
LibraryTab.playlists => l.navigationTabsPlaylists,
|
||||||
@@ -180,7 +219,7 @@ class TabTitleText extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}, [tabName]);
|
}, [tabName]);
|
||||||
|
|
||||||
return TextH1(tabText.value);
|
return Text(tabText.value, style: text.headlineLarge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +228,12 @@ class TabScrollView extends HookConsumerWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.sliver,
|
required this.sliver,
|
||||||
|
this.menuBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int index;
|
final int index;
|
||||||
final Widget sliver;
|
final Widget sliver;
|
||||||
|
final WidgetBuilder? menuBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -200,14 +241,24 @@ class TabScrollView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final scrollProvider = CustomScrollProviderData.of(context);
|
final scrollProvider = CustomScrollProviderData.of(context);
|
||||||
|
|
||||||
return CustomScrollView(
|
final listBuilder = menuBuilder;
|
||||||
controller: scrollProvider.scrollControllers[index],
|
final floatingActionButton = listBuilder != null
|
||||||
slivers: <Widget>[
|
? FabFilter(
|
||||||
SliverOverlapInjector(
|
listBuilder: listBuilder,
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
)
|
||||||
),
|
: null;
|
||||||
sliver,
|
|
||||||
],
|
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,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../l10n/generated/app_localizations.dart';
|
import '../../l10n/generated/app_localizations.dart';
|
||||||
import '../state/database.dart';
|
import '../state/database.dart';
|
||||||
import '../state/source.dart';
|
import '../state/source.dart';
|
||||||
import '../ui/text.dart';
|
|
||||||
|
|
||||||
const kHorizontalPadding = 18.0;
|
const kHorizontalPadding = 18.0;
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
|
final textTheme = TextTheme.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: TextH1(l.navigationTabsSettings),
|
title: Text(l.navigationTabsSettings, style: textTheme.headlineLarge),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
// const SizedBox(height: 96),
|
|
||||||
_SectionHeader(l.settingsServersName),
|
_SectionHeader(l.settingsServersName),
|
||||||
const _Sources(),
|
const _Sources(),
|
||||||
// _SectionHeader(l.settingsNetworkName),
|
// _SectionHeader(l.settingsNetworkName),
|
||||||
@@ -36,7 +36,9 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _Section extends StatelessWidget {
|
class _Section extends StatelessWidget {
|
||||||
const _Section({required this.children});
|
const _Section({
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
final List<Widget> children;
|
final List<Widget> children;
|
||||||
|
|
||||||
@@ -46,30 +48,30 @@ class _Section extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
...children,
|
...children,
|
||||||
const SizedBox(height: 32),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SectionHeader extends StatelessWidget {
|
class _SectionHeader extends StatelessWidget {
|
||||||
const _SectionHeader(this.title);
|
const _SectionHeader(
|
||||||
|
this.title,
|
||||||
|
);
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
final text = TextTheme.of(context);
|
||||||
children: [
|
|
||||||
const SizedBox(height: 16),
|
return Padding(
|
||||||
SizedBox(
|
padding: EdgeInsetsGeometry.directional(
|
||||||
width: double.infinity,
|
start: kHorizontalPadding,
|
||||||
child: Padding(
|
end: kHorizontalPadding,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding),
|
top: 32,
|
||||||
child: TextH2(title),
|
bottom: 8,
|
||||||
),
|
),
|
||||||
),
|
child: Text(title, style: text.headlineMedium),
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,12 +376,12 @@ class _Sources extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
for (final (source, settings) in sources)
|
for (final (:source, :subsonicSetting) in sources)
|
||||||
RadioListTile<int>(
|
RadioListTile<int>(
|
||||||
value: source.id,
|
value: source.id,
|
||||||
title: Text(source.name),
|
title: Text(source.name),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
settings.address.toString(),
|
subsonicSetting.address.toString(),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
@@ -387,7 +389,7 @@ class _Sources extends HookConsumerWidget {
|
|||||||
secondary: IconButton(
|
secondary: IconButton(
|
||||||
icon: const Icon(Icons.edit_rounded),
|
icon: const Icon(Icons.edit_rounded),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// context.pushRoute(SourceRoute(id: source.id));
|
context.push('/sources/${source.id}');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -402,7 +404,7 @@ class _Sources extends HookConsumerWidget {
|
|||||||
icon: const Icon(Icons.add_rounded),
|
icon: const Icon(Icons.add_rounded),
|
||||||
label: Text(l.settingsServersActionsAdd),
|
label: Text(l.settingsServersActionsAdd),
|
||||||
onPressed: () {
|
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 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../database/database.dart';
|
import '../../database/database.dart';
|
||||||
@@ -6,46 +6,40 @@ import '../../database/database.dart';
|
|||||||
final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
||||||
final db = SubtracksDatabase();
|
final db = SubtracksDatabase();
|
||||||
|
|
||||||
await db
|
await db.batch((batch) {
|
||||||
.into(db.sources)
|
batch.insertAll(
|
||||||
.insertOnConflictUpdate(
|
db.sources,
|
||||||
|
[
|
||||||
SourcesCompanion.insert(
|
SourcesCompanion.insert(
|
||||||
id: Value(1),
|
id: Value(1),
|
||||||
name: 'test subsonic',
|
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(
|
SourcesCompanion.insert(
|
||||||
id: Value(2),
|
id: Value(2),
|
||||||
name: 'test navidrome',
|
name: 'test navidrome',
|
||||||
// isActive: Value(null),
|
isActive: Value(null),
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
await db
|
mode: InsertMode.insertOrIgnore,
|
||||||
.into(db.subsonicSettings)
|
);
|
||||||
.insertOnConflictUpdate(
|
batch.insertAllOnConflictUpdate(db.subsonicSettings, [
|
||||||
SubsonicSettingsCompanion.insert(
|
SubsonicSettingsCompanion.insert(
|
||||||
sourceId: Value(2),
|
sourceId: Value(1),
|
||||||
address: Uri.parse('http://10.0.2.2:4533'),
|
address: Uri.parse('http://demo.subsonic.org'),
|
||||||
username: 'admin',
|
username: 'guest1',
|
||||||
password: 'password',
|
password: 'guest',
|
||||||
useTokenAuth: Value(true),
|
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;
|
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),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -12,15 +12,13 @@ final activeSourceInitializer = StreamProvider<(int, SubsonicSource)>((
|
|||||||
|
|
||||||
final activeSource = db.sourcesDao.activeSourceId().watchSingle();
|
final activeSource = db.sourcesDao.activeSourceId().watchSingle();
|
||||||
|
|
||||||
await for (final source in activeSource) {
|
await for (final sourceId in activeSource) {
|
||||||
final sourceId = source.read(db.sources.id)!;
|
|
||||||
|
|
||||||
final subsonicSettings = await db.managers.subsonicSettings
|
final subsonicSettings = await db.managers.subsonicSettings
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
.filter((f) => f.sourceId.equals(sourceId))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
|
|
||||||
yield (
|
yield (
|
||||||
sourceId,
|
sourceId!,
|
||||||
SubsonicSource(
|
SubsonicSource(
|
||||||
SubsonicClient(
|
SubsonicClient(
|
||||||
http: SubtracksHttpClient(),
|
http: SubtracksHttpClient(),
|
||||||
|
|||||||
61
lib/app/ui/cover_art_theme.dart
Normal file
61
lib/app/ui/cover_art_theme.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
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';
|
||||||
|
|
||||||
|
class CoverArtTheme extends HookConsumerWidget {
|
||||||
|
const CoverArtTheme({
|
||||||
|
super.key,
|
||||||
|
required this.coverArt,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? coverArt;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final source = ref.watch(sourceProvider);
|
||||||
|
final sourceId = ref.watch(sourceIdProvider);
|
||||||
|
|
||||||
|
final getColorScheme = useMemoized(
|
||||||
|
() async {
|
||||||
|
try {
|
||||||
|
return await colorSchemefromImageProvider(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
provider: CachedNetworkImageProvider(
|
||||||
|
coverArt != null
|
||||||
|
? source.coverArtUri(coverArt!, thumbnail: true).toString()
|
||||||
|
: 'https://placehold.net/400x400.png',
|
||||||
|
cacheKey: coverArt != null
|
||||||
|
? '$sourceId$coverArt${true}'
|
||||||
|
: 'https://placehold.net/400x400.png',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logger.w(
|
||||||
|
'Could not create color scheme from image provider',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[source, sourceId, coverArt],
|
||||||
|
);
|
||||||
|
|
||||||
|
final colorScheme = useFuture(getColorScheme).data;
|
||||||
|
|
||||||
|
return Theme(
|
||||||
|
data: colorScheme == null
|
||||||
|
? Theme.of(context)
|
||||||
|
: subtracksTheme(colorScheme),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/app/ui/gradient.dart
Normal file
63
lib/app/ui/gradient.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
|
||||||
|
class ThemedGradient extends LinearGradient {
|
||||||
|
const ThemedGradient({
|
||||||
|
required super.colors,
|
||||||
|
super.begin,
|
||||||
|
super.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ThemedGradient.of(
|
||||||
|
BuildContext context, {
|
||||||
|
AlignmentGeometry begin = Alignment.topCenter,
|
||||||
|
AlignmentGeometry end = Alignment.bottomCenter,
|
||||||
|
}) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return ThemedGradient(
|
||||||
|
begin: begin,
|
||||||
|
end: end,
|
||||||
|
colors: [
|
||||||
|
colorScheme.primaryContainer,
|
||||||
|
colorScheme.surface,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GradientScrollView extends HookConsumerWidget {
|
||||||
|
const GradientScrollView({
|
||||||
|
super.key,
|
||||||
|
required this.slivers,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> slivers;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverStack(
|
||||||
|
children: [
|
||||||
|
SliverPositioned.directional(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
top: 0,
|
||||||
|
child: Ink(
|
||||||
|
width: double.infinity,
|
||||||
|
height: MediaQuery.heightOf(context),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: ThemedGradient.of(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MultiSliver(children: slivers),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,23 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
import '../app/state/source.dart';
|
import '../state/source.dart';
|
||||||
|
|
||||||
class CoverArtImage extends HookConsumerWidget {
|
class CoverArtImage extends HookConsumerWidget {
|
||||||
const CoverArtImage({
|
const CoverArtImage({
|
||||||
super.key,
|
super.key,
|
||||||
this.coverArt,
|
this.coverArt,
|
||||||
this.thumbnail = false,
|
this.thumbnail = true,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.height,
|
||||||
|
this.width,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? coverArt;
|
final String? coverArt;
|
||||||
final bool thumbnail;
|
final bool thumbnail;
|
||||||
|
final BoxFit fit;
|
||||||
|
final double? height;
|
||||||
|
final double? width;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -24,34 +30,14 @@ class CoverArtImage extends HookConsumerWidget {
|
|||||||
? source.coverArtUri(coverArt!, thumbnail: thumbnail).toString()
|
? source.coverArtUri(coverArt!, thumbnail: thumbnail).toString()
|
||||||
: 'https://placehold.net/400x400.png';
|
: 'https://placehold.net/400x400.png';
|
||||||
|
|
||||||
return BaseImage(
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
// can't use the URL because of token auth, which is a cache-buster
|
|
||||||
cacheKey: '$sourceId$coverArt$thumbnail',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BaseImage extends HookConsumerWidget {
|
|
||||||
const BaseImage({
|
|
||||||
super.key,
|
|
||||||
required this.imageUrl,
|
|
||||||
this.cacheKey,
|
|
||||||
this.fit = BoxFit.cover,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String imageUrl;
|
|
||||||
final String? cacheKey;
|
|
||||||
final BoxFit fit;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
cacheKey: cacheKey,
|
cacheKey: '$sourceId$coverArt$thumbnail',
|
||||||
placeholder: (context, url) => Icon(Symbols.cached_rounded),
|
placeholder: (context, url) => Icon(Symbols.cached_rounded),
|
||||||
errorWidget: (context, url, error) => Icon(Icons.error),
|
errorWidget: (context, url, error) => Icon(Icons.error),
|
||||||
fit: BoxFit.cover,
|
fit: fit,
|
||||||
fadeOutDuration: Duration(milliseconds: 100),
|
fadeOutDuration: Duration(milliseconds: 100),
|
||||||
fadeInDuration: Duration(milliseconds: 200),
|
fadeInDuration: Duration(milliseconds: 200),
|
||||||
);
|
);
|
||||||
@@ -4,11 +4,14 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
import '../../sources/models.dart';
|
import '../../../sources/models.dart';
|
||||||
import '../hooks/use_paging_controller.dart';
|
import '../../hooks/use_on_source.dart';
|
||||||
import '../state/database.dart';
|
import '../../hooks/use_paging_controller.dart';
|
||||||
import '../state/source.dart';
|
import '../../state/database.dart';
|
||||||
import 'list_items.dart';
|
import '../../state/lists.dart';
|
||||||
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
|
import 'items.dart';
|
||||||
|
|
||||||
const kPageSize = 60;
|
const kPageSize = 60;
|
||||||
|
|
||||||
@@ -18,21 +21,22 @@ class AlbumsGrid extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final db = ref.watch(databaseProvider);
|
final db = ref.watch(databaseProvider);
|
||||||
final sourceId = ref.watch(sourceIdProvider);
|
final query = ref.watch(albumsQueryProvider);
|
||||||
|
|
||||||
final controller = usePagingController<int, Album>(
|
final controller = usePagingController<int, Album>(
|
||||||
getNextPageKey: (state) =>
|
getNextPageKey: (state) =>
|
||||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||||
fetchPage: (pageKey) => db.libraryDao.listAlbums(
|
fetchPage: (pageKey) => db.libraryDao.listAlbums(
|
||||||
limit: kPageSize,
|
query.copyWith(
|
||||||
offset: (pageKey - 1) * kPageSize,
|
sourceId: ref.read(sourceIdProvider),
|
||||||
|
limit: kPageSize,
|
||||||
|
offset: (pageKey - 1) * kPageSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useOnSourceSync(ref, controller.refresh);
|
||||||
controller.refresh();
|
useValueChanged(query, (_, _) => controller.refresh());
|
||||||
return;
|
|
||||||
}, [sourceId]);
|
|
||||||
|
|
||||||
return PagingListener(
|
return PagingListener(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@@ -45,11 +49,13 @@ class AlbumsGrid extends HookConsumerWidget {
|
|||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 3,
|
||||||
),
|
),
|
||||||
|
showNoMoreItemsIndicatorAsGridChild: false,
|
||||||
builderDelegate: PagedChildBuilderDelegate<Album>(
|
builderDelegate: PagedChildBuilderDelegate<Album>(
|
||||||
|
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||||
itemBuilder: (context, item, index) => AlbumGridTile(
|
itemBuilder: (context, item, index) => AlbumGridTile(
|
||||||
album: item,
|
album: item,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
context.push('/album/${item.id}');
|
context.push('/albums/${item.id}');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -59,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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,43 @@
|
|||||||
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
import '../../sources/models.dart';
|
import '../../../database/dao/library_dao.dart';
|
||||||
import '../hooks/use_paging_controller.dart';
|
import '../../../database/query.dart';
|
||||||
import '../state/database.dart';
|
import '../../hooks/use_on_source.dart';
|
||||||
import '../state/source.dart';
|
import '../../hooks/use_paging_controller.dart';
|
||||||
import 'list_items.dart';
|
import '../../state/database.dart';
|
||||||
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
|
import 'items.dart';
|
||||||
|
|
||||||
const kPageSize = 30;
|
const kPageSize = 30;
|
||||||
|
|
||||||
typedef _ArtistItem = ({Artist artist, int? albumCount});
|
|
||||||
|
|
||||||
class ArtistsList extends HookConsumerWidget {
|
class ArtistsList extends HookConsumerWidget {
|
||||||
const ArtistsList({super.key});
|
const ArtistsList({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final db = ref.watch(databaseProvider);
|
final db = ref.watch(databaseProvider);
|
||||||
final sourceId = ref.watch(sourceIdProvider);
|
final controller = usePagingController<int, AristListItem>(
|
||||||
|
|
||||||
final controller = usePagingController<int, _ArtistItem>(
|
|
||||||
getNextPageKey: (state) =>
|
getNextPageKey: (state) =>
|
||||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||||
fetchPage: (pageKey) => db.libraryDao.listArtists(
|
fetchPage: (pageKey) => db.libraryDao.listArtists(
|
||||||
limit: kPageSize,
|
ArtistsQuery(
|
||||||
offset: (pageKey - 1) * kPageSize,
|
sourceId: ref.read(sourceIdProvider),
|
||||||
|
sort: IList([
|
||||||
|
SortingTerm.artistsAsc(ArtistsColumn.name),
|
||||||
|
]),
|
||||||
|
limit: kPageSize,
|
||||||
|
offset: (pageKey - 1) * kPageSize,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useOnSourceChange(ref, (_) => controller.refresh());
|
||||||
controller.refresh();
|
useOnSourceSync(ref, controller.refresh);
|
||||||
return;
|
|
||||||
}, [sourceId]);
|
|
||||||
|
|
||||||
return PagingListener(
|
return PagingListener(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@@ -42,7 +45,8 @@ class ArtistsList extends HookConsumerWidget {
|
|||||||
return PagedSliverList(
|
return PagedSliverList(
|
||||||
state: state,
|
state: state,
|
||||||
fetchNextPage: fetchNextPage,
|
fetchNextPage: fetchNextPage,
|
||||||
builderDelegate: PagedChildBuilderDelegate<_ArtistItem>(
|
builderDelegate: PagedChildBuilderDelegate<AristListItem>(
|
||||||
|
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
final (:artist, :albumCount) = item;
|
final (:artist, :albumCount) = item;
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ class ArtistsList extends HookConsumerWidget {
|
|||||||
artist: artist,
|
artist: artist,
|
||||||
albumCount: albumCount,
|
albumCount: albumCount,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
context.push('/artist/${artist.id}');
|
context.push('/artists/${artist.id}');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
105
lib/app/ui/lists/header.dart
Normal file
105
lib/app/ui/lists/header.dart
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../images.dart';
|
||||||
|
|
||||||
|
class SongsListHeader extends HookConsumerWidget {
|
||||||
|
const SongsListHeader({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.coverArt,
|
||||||
|
this.playText,
|
||||||
|
this.onPlay,
|
||||||
|
this.onMore,
|
||||||
|
// required this.downloadActions,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String? subtitle;
|
||||||
|
final String? coverArt;
|
||||||
|
final String? playText;
|
||||||
|
final void Function()? onPlay;
|
||||||
|
final FutureOr<void> Function()? onMore;
|
||||||
|
// final List<DownloadAction> downloadActions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
minimum: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
height: 300,
|
||||||
|
width: 300,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
blurRadius: 20,
|
||||||
|
blurStyle: BlurStyle.normal,
|
||||||
|
color: Colors.black.withAlpha(100),
|
||||||
|
offset: Offset.zero,
|
||||||
|
spreadRadius: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: CoverArtImage(
|
||||||
|
thumbnail: false,
|
||||||
|
coverArt: coverArt,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.headlineMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (subtitle != null)
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.download_done_rounded),
|
||||||
|
),
|
||||||
|
if (onPlay != null)
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: onPlay,
|
||||||
|
icon: const Icon(Icons.play_arrow_rounded),
|
||||||
|
label: Text(
|
||||||
|
playText ?? '',
|
||||||
|
// style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
// color: theme.colorScheme.onPrimary,
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onMore != null)
|
||||||
|
IconButton(
|
||||||
|
onPressed: onMore,
|
||||||
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
215
lib/app/ui/lists/items.dart
Normal file
215
lib/app/ui/lists/items.dart
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../sources/models.dart';
|
||||||
|
import '../../util/clip.dart';
|
||||||
|
import '../images.dart';
|
||||||
|
|
||||||
|
class AlbumGridTile extends HookConsumerWidget {
|
||||||
|
const AlbumGridTile({
|
||||||
|
super.key,
|
||||||
|
required this.album,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Album album;
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return CardTheme(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadiusGeometry.circular(3),
|
||||||
|
),
|
||||||
|
margin: EdgeInsets.all(2),
|
||||||
|
child: ImageCard(
|
||||||
|
onTap: onTap,
|
||||||
|
child: CoverArtImage(
|
||||||
|
coverArt: album.coverArt,
|
||||||
|
thumbnail: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistListTile extends StatelessWidget {
|
||||||
|
const ArtistListTile({
|
||||||
|
super.key,
|
||||||
|
required this.artist,
|
||||||
|
required this.albumCount,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Artist artist;
|
||||||
|
final int albumCount;
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
leading: CircleClip(
|
||||||
|
child: CoverArtImage(
|
||||||
|
coverArt: artist.coverArt,
|
||||||
|
thumbnail: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(artist.name),
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: showLeading
|
||||||
|
? RoundedBoxClip(
|
||||||
|
child: CoverArtImage(
|
||||||
|
coverArt: coverArt,
|
||||||
|
thumbnail: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
title: Text(song.title),
|
||||||
|
subtitle: Text(song.artist ?? ''),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageCard extends StatelessWidget {
|
||||||
|
const ImageCard({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final void Function()? onTap;
|
||||||
|
final void Function()? onLongPress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.passthrough,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
Positioned.fill(
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/app/ui/lists/songs_list.dart
Normal file
59
lib/app/ui/lists/songs_list.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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 '../../hooks/use_on_source.dart';
|
||||||
|
import '../../hooks/use_paging_controller.dart';
|
||||||
|
import '../../state/database.dart';
|
||||||
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
|
|
||||||
|
const kPageSize = 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, 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
useOnSourceSync(ref, controller.refresh);
|
||||||
|
useValueChanged(query, (_, _) => controller.refresh());
|
||||||
|
|
||||||
|
return PagingListener(
|
||||||
|
controller: controller,
|
||||||
|
builder: (context, state, fetchNextPage) {
|
||||||
|
return PagedSliverList(
|
||||||
|
state: state,
|
||||||
|
fetchNextPage: fetchNextPage,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class TextH1 extends StatelessWidget {
|
|
||||||
const TextH1(
|
|
||||||
this.data, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String data;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
data,
|
|
||||||
style: theme.textTheme.headlineLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextH2 extends StatelessWidget {
|
|
||||||
const TextH2(
|
|
||||||
this.data, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String data;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
data,
|
|
||||||
style: theme.textTheme.headlineMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
lib/app/ui/theme.dart
Normal file
29
lib/app/ui/theme.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
ThemeData subtracksTheme([ColorScheme? colorScheme]) {
|
||||||
|
final theme = ThemeData.from(
|
||||||
|
colorScheme:
|
||||||
|
colorScheme ??
|
||||||
|
ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.purple.shade800,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
useMaterial3: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
431
lib/app/util/color_scheme.dart
Normal file
431
lib/app/util/color_scheme.dart
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
/// This file is a fork of the built-in [ColorScheme.fromImageProvider] function
|
||||||
|
/// with the following changes:
|
||||||
|
///
|
||||||
|
/// 1. [_extractColorsFromImageProvider] now runs the Quantizer to extract colors
|
||||||
|
/// in an isolate to prevent jank on the UI thread (especially noticable during
|
||||||
|
/// transitions).
|
||||||
|
///
|
||||||
|
/// 2. [_imageProviderToScaled] has its hard-coded image resize max dimensions
|
||||||
|
/// to 32x32 pixels.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||||
|
|
||||||
|
/// Generate a [ColorScheme] derived from the given `imageProvider`.
|
||||||
|
///
|
||||||
|
/// Material Color Utilities extracts the dominant color from the
|
||||||
|
/// supplied [ImageProvider]. Using this color, a [ColorScheme] is generated
|
||||||
|
/// with harmonious colors that meet contrast requirements for accessibility.
|
||||||
|
///
|
||||||
|
/// If any of the optional color parameters are non-null, they will be
|
||||||
|
/// used in place of the generated colors for that field in the resulting
|
||||||
|
/// [ColorScheme]. This allows apps to override specific colors for their
|
||||||
|
/// needs.
|
||||||
|
///
|
||||||
|
/// Given the nature of the algorithm, the most dominant color of the
|
||||||
|
/// `imageProvider` may not wind up as one of the [ColorScheme] colors.
|
||||||
|
///
|
||||||
|
/// The provided image will be scaled down to a maximum size of 112x112 pixels
|
||||||
|
/// during color extraction.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample shows how to use [ColorScheme.fromImageProvider] to create
|
||||||
|
/// content-based dynamic color schemes.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/color_scheme/dynamic_content_color.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [M3 Guidelines: Dynamic color from content](https://m3.material.io/styles/color/dynamic-color/user-generated-color#8af550b9-a19e-4e9f-bb0a-7f611fed5d0f)
|
||||||
|
/// * <https://pub.dev/packages/dynamic_color>, a package to create
|
||||||
|
/// [ColorScheme]s based on a platform's implementation of dynamic color.
|
||||||
|
/// * <https://m3.material.io/styles/color/the-color-system/color-roles>, the
|
||||||
|
/// Material 3 Color system specification.
|
||||||
|
/// * <https://pub.dev/packages/material_color_utilities>, the package
|
||||||
|
/// used to algorithmically determine the dominant color and to generate
|
||||||
|
/// the [ColorScheme].
|
||||||
|
Future<ColorScheme> colorSchemefromImageProvider({
|
||||||
|
required ImageProvider provider,
|
||||||
|
Brightness brightness = Brightness.light,
|
||||||
|
DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot,
|
||||||
|
double contrastLevel = 0.0,
|
||||||
|
Color? primary,
|
||||||
|
Color? onPrimary,
|
||||||
|
Color? primaryContainer,
|
||||||
|
Color? onPrimaryContainer,
|
||||||
|
Color? primaryFixed,
|
||||||
|
Color? primaryFixedDim,
|
||||||
|
Color? onPrimaryFixed,
|
||||||
|
Color? onPrimaryFixedVariant,
|
||||||
|
Color? secondary,
|
||||||
|
Color? onSecondary,
|
||||||
|
Color? secondaryContainer,
|
||||||
|
Color? onSecondaryContainer,
|
||||||
|
Color? secondaryFixed,
|
||||||
|
Color? secondaryFixedDim,
|
||||||
|
Color? onSecondaryFixed,
|
||||||
|
Color? onSecondaryFixedVariant,
|
||||||
|
Color? tertiary,
|
||||||
|
Color? onTertiary,
|
||||||
|
Color? tertiaryContainer,
|
||||||
|
Color? onTertiaryContainer,
|
||||||
|
Color? tertiaryFixed,
|
||||||
|
Color? tertiaryFixedDim,
|
||||||
|
Color? onTertiaryFixed,
|
||||||
|
Color? onTertiaryFixedVariant,
|
||||||
|
Color? error,
|
||||||
|
Color? onError,
|
||||||
|
Color? errorContainer,
|
||||||
|
Color? onErrorContainer,
|
||||||
|
Color? outline,
|
||||||
|
Color? outlineVariant,
|
||||||
|
Color? surface,
|
||||||
|
Color? onSurface,
|
||||||
|
Color? surfaceDim,
|
||||||
|
Color? surfaceBright,
|
||||||
|
Color? surfaceContainerLowest,
|
||||||
|
Color? surfaceContainerLow,
|
||||||
|
Color? surfaceContainer,
|
||||||
|
Color? surfaceContainerHigh,
|
||||||
|
Color? surfaceContainerHighest,
|
||||||
|
Color? onSurfaceVariant,
|
||||||
|
Color? inverseSurface,
|
||||||
|
Color? onInverseSurface,
|
||||||
|
Color? inversePrimary,
|
||||||
|
Color? shadow,
|
||||||
|
Color? scrim,
|
||||||
|
Color? surfaceTint,
|
||||||
|
@Deprecated(
|
||||||
|
'Use surface instead. '
|
||||||
|
'This feature was deprecated after v3.18.0-0.1.pre.',
|
||||||
|
)
|
||||||
|
Color? background,
|
||||||
|
@Deprecated(
|
||||||
|
'Use onSurface instead. '
|
||||||
|
'This feature was deprecated after v3.18.0-0.1.pre.',
|
||||||
|
)
|
||||||
|
Color? onBackground,
|
||||||
|
@Deprecated(
|
||||||
|
'Use surfaceContainerHighest instead. '
|
||||||
|
'This feature was deprecated after v3.18.0-0.1.pre.',
|
||||||
|
)
|
||||||
|
Color? surfaceVariant,
|
||||||
|
}) async {
|
||||||
|
// Extract dominant colors from image.
|
||||||
|
final QuantizerResult quantizerResult = await _extractColorsFromImageProvider(
|
||||||
|
provider,
|
||||||
|
);
|
||||||
|
final Map<int, int> colorToCount = quantizerResult.colorToCount.map(
|
||||||
|
(int key, int value) => MapEntry<int, int>(_getArgbFromAbgr(key), value),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Score colors for color scheme suitability.
|
||||||
|
final List<int> scoredResults = Score.score(colorToCount, desired: 1);
|
||||||
|
final ui.Color baseColor = Color(scoredResults.first);
|
||||||
|
|
||||||
|
final DynamicScheme scheme = _buildDynamicScheme(
|
||||||
|
brightness,
|
||||||
|
baseColor,
|
||||||
|
dynamicSchemeVariant,
|
||||||
|
contrastLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ColorScheme(
|
||||||
|
primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)),
|
||||||
|
onPrimary:
|
||||||
|
onPrimary ?? Color(MaterialDynamicColors.onPrimary.getArgb(scheme)),
|
||||||
|
primaryContainer:
|
||||||
|
primaryContainer ??
|
||||||
|
Color(MaterialDynamicColors.primaryContainer.getArgb(scheme)),
|
||||||
|
onPrimaryContainer:
|
||||||
|
onPrimaryContainer ??
|
||||||
|
Color(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)),
|
||||||
|
primaryFixed:
|
||||||
|
primaryFixed ??
|
||||||
|
Color(MaterialDynamicColors.primaryFixed.getArgb(scheme)),
|
||||||
|
primaryFixedDim:
|
||||||
|
primaryFixedDim ??
|
||||||
|
Color(MaterialDynamicColors.primaryFixedDim.getArgb(scheme)),
|
||||||
|
onPrimaryFixed:
|
||||||
|
onPrimaryFixed ??
|
||||||
|
Color(MaterialDynamicColors.onPrimaryFixed.getArgb(scheme)),
|
||||||
|
onPrimaryFixedVariant:
|
||||||
|
onPrimaryFixedVariant ??
|
||||||
|
Color(MaterialDynamicColors.onPrimaryFixedVariant.getArgb(scheme)),
|
||||||
|
secondary:
|
||||||
|
secondary ?? Color(MaterialDynamicColors.secondary.getArgb(scheme)),
|
||||||
|
onSecondary:
|
||||||
|
onSecondary ?? Color(MaterialDynamicColors.onSecondary.getArgb(scheme)),
|
||||||
|
secondaryContainer:
|
||||||
|
secondaryContainer ??
|
||||||
|
Color(MaterialDynamicColors.secondaryContainer.getArgb(scheme)),
|
||||||
|
onSecondaryContainer:
|
||||||
|
onSecondaryContainer ??
|
||||||
|
Color(MaterialDynamicColors.onSecondaryContainer.getArgb(scheme)),
|
||||||
|
secondaryFixed:
|
||||||
|
secondaryFixed ??
|
||||||
|
Color(MaterialDynamicColors.secondaryFixed.getArgb(scheme)),
|
||||||
|
secondaryFixedDim:
|
||||||
|
secondaryFixedDim ??
|
||||||
|
Color(MaterialDynamicColors.secondaryFixedDim.getArgb(scheme)),
|
||||||
|
onSecondaryFixed:
|
||||||
|
onSecondaryFixed ??
|
||||||
|
Color(MaterialDynamicColors.onSecondaryFixed.getArgb(scheme)),
|
||||||
|
onSecondaryFixedVariant:
|
||||||
|
onSecondaryFixedVariant ??
|
||||||
|
Color(MaterialDynamicColors.onSecondaryFixedVariant.getArgb(scheme)),
|
||||||
|
tertiary: tertiary ?? Color(MaterialDynamicColors.tertiary.getArgb(scheme)),
|
||||||
|
onTertiary:
|
||||||
|
onTertiary ?? Color(MaterialDynamicColors.onTertiary.getArgb(scheme)),
|
||||||
|
tertiaryContainer:
|
||||||
|
tertiaryContainer ??
|
||||||
|
Color(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)),
|
||||||
|
onTertiaryContainer:
|
||||||
|
onTertiaryContainer ??
|
||||||
|
Color(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)),
|
||||||
|
tertiaryFixed:
|
||||||
|
tertiaryFixed ??
|
||||||
|
Color(MaterialDynamicColors.tertiaryFixed.getArgb(scheme)),
|
||||||
|
tertiaryFixedDim:
|
||||||
|
tertiaryFixedDim ??
|
||||||
|
Color(MaterialDynamicColors.tertiaryFixedDim.getArgb(scheme)),
|
||||||
|
onTertiaryFixed:
|
||||||
|
onTertiaryFixed ??
|
||||||
|
Color(MaterialDynamicColors.onTertiaryFixed.getArgb(scheme)),
|
||||||
|
onTertiaryFixedVariant:
|
||||||
|
onTertiaryFixedVariant ??
|
||||||
|
Color(MaterialDynamicColors.onTertiaryFixedVariant.getArgb(scheme)),
|
||||||
|
error: error ?? Color(MaterialDynamicColors.error.getArgb(scheme)),
|
||||||
|
onError: onError ?? Color(MaterialDynamicColors.onError.getArgb(scheme)),
|
||||||
|
errorContainer:
|
||||||
|
errorContainer ??
|
||||||
|
Color(MaterialDynamicColors.errorContainer.getArgb(scheme)),
|
||||||
|
onErrorContainer:
|
||||||
|
onErrorContainer ??
|
||||||
|
Color(MaterialDynamicColors.onErrorContainer.getArgb(scheme)),
|
||||||
|
outline: outline ?? Color(MaterialDynamicColors.outline.getArgb(scheme)),
|
||||||
|
outlineVariant:
|
||||||
|
outlineVariant ??
|
||||||
|
Color(MaterialDynamicColors.outlineVariant.getArgb(scheme)),
|
||||||
|
surface: surface ?? Color(MaterialDynamicColors.surface.getArgb(scheme)),
|
||||||
|
surfaceDim:
|
||||||
|
surfaceDim ?? Color(MaterialDynamicColors.surfaceDim.getArgb(scheme)),
|
||||||
|
surfaceBright:
|
||||||
|
surfaceBright ??
|
||||||
|
Color(MaterialDynamicColors.surfaceBright.getArgb(scheme)),
|
||||||
|
surfaceContainerLowest:
|
||||||
|
surfaceContainerLowest ??
|
||||||
|
Color(MaterialDynamicColors.surfaceContainerLowest.getArgb(scheme)),
|
||||||
|
surfaceContainerLow:
|
||||||
|
surfaceContainerLow ??
|
||||||
|
Color(MaterialDynamicColors.surfaceContainerLow.getArgb(scheme)),
|
||||||
|
surfaceContainer:
|
||||||
|
surfaceContainer ??
|
||||||
|
Color(MaterialDynamicColors.surfaceContainer.getArgb(scheme)),
|
||||||
|
surfaceContainerHigh:
|
||||||
|
surfaceContainerHigh ??
|
||||||
|
Color(MaterialDynamicColors.surfaceContainerHigh.getArgb(scheme)),
|
||||||
|
surfaceContainerHighest:
|
||||||
|
surfaceContainerHighest ??
|
||||||
|
Color(MaterialDynamicColors.surfaceContainerHighest.getArgb(scheme)),
|
||||||
|
onSurface:
|
||||||
|
onSurface ?? Color(MaterialDynamicColors.onSurface.getArgb(scheme)),
|
||||||
|
onSurfaceVariant:
|
||||||
|
onSurfaceVariant ??
|
||||||
|
Color(MaterialDynamicColors.onSurfaceVariant.getArgb(scheme)),
|
||||||
|
inverseSurface:
|
||||||
|
inverseSurface ??
|
||||||
|
Color(MaterialDynamicColors.inverseSurface.getArgb(scheme)),
|
||||||
|
onInverseSurface:
|
||||||
|
onInverseSurface ??
|
||||||
|
Color(MaterialDynamicColors.inverseOnSurface.getArgb(scheme)),
|
||||||
|
inversePrimary:
|
||||||
|
inversePrimary ??
|
||||||
|
Color(MaterialDynamicColors.inversePrimary.getArgb(scheme)),
|
||||||
|
shadow: shadow ?? Color(MaterialDynamicColors.shadow.getArgb(scheme)),
|
||||||
|
scrim: scrim ?? Color(MaterialDynamicColors.scrim.getArgb(scheme)),
|
||||||
|
surfaceTint:
|
||||||
|
surfaceTint ?? Color(MaterialDynamicColors.primary.getArgb(scheme)),
|
||||||
|
brightness: brightness,
|
||||||
|
// DEPRECATED (newest deprecations at the bottom)
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
background:
|
||||||
|
background ?? Color(MaterialDynamicColors.background.getArgb(scheme)),
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
onBackground:
|
||||||
|
onBackground ??
|
||||||
|
Color(MaterialDynamicColors.onBackground.getArgb(scheme)),
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
surfaceVariant:
|
||||||
|
surfaceVariant ??
|
||||||
|
Color(MaterialDynamicColors.surfaceVariant.getArgb(scheme)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorScheme.fromImageProvider() utilities.
|
||||||
|
|
||||||
|
/// Extracts bytes from an [ImageProvider] and returns a [QuantizerResult]
|
||||||
|
/// containing the most dominant colors.
|
||||||
|
Future<QuantizerResult> _extractColorsFromImageProvider(
|
||||||
|
ImageProvider imageProvider,
|
||||||
|
) async {
|
||||||
|
final ui.Image scaledImage = await _imageProviderToScaled(imageProvider);
|
||||||
|
final ByteData? imageBytes = await scaledImage.toByteData();
|
||||||
|
|
||||||
|
return Isolate.run(
|
||||||
|
() => QuantizerCelebi().quantize(
|
||||||
|
imageBytes!.buffer.asUint32List(),
|
||||||
|
128,
|
||||||
|
returnInputPixelToClusterPixel: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale image size down to reduce computation time of color extraction.
|
||||||
|
Future<ui.Image> _imageProviderToScaled(ImageProvider imageProvider) async {
|
||||||
|
const double maxDimension = 32.0;
|
||||||
|
final ImageStream stream = imageProvider.resolve(
|
||||||
|
const ImageConfiguration(size: Size(maxDimension, maxDimension)),
|
||||||
|
);
|
||||||
|
final Completer<ui.Image> imageCompleter = Completer<ui.Image>();
|
||||||
|
late ImageStreamListener listener;
|
||||||
|
late ui.Image scaledImage;
|
||||||
|
Timer? loadFailureTimeout;
|
||||||
|
|
||||||
|
listener = ImageStreamListener(
|
||||||
|
(ImageInfo info, bool sync) async {
|
||||||
|
loadFailureTimeout?.cancel();
|
||||||
|
stream.removeListener(listener);
|
||||||
|
final ui.Image image = info.image;
|
||||||
|
final int width = image.width;
|
||||||
|
final int height = image.height;
|
||||||
|
double paintWidth = width.toDouble();
|
||||||
|
double paintHeight = height.toDouble();
|
||||||
|
assert(width > 0 && height > 0);
|
||||||
|
|
||||||
|
final bool rescale = width > maxDimension || height > maxDimension;
|
||||||
|
if (rescale) {
|
||||||
|
paintWidth = (width > height)
|
||||||
|
? maxDimension
|
||||||
|
: (maxDimension / height) * width;
|
||||||
|
paintHeight = (height > width)
|
||||||
|
? maxDimension
|
||||||
|
: (maxDimension / width) * height;
|
||||||
|
}
|
||||||
|
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||||
|
final Canvas canvas = Canvas(pictureRecorder);
|
||||||
|
paintImage(
|
||||||
|
canvas: canvas,
|
||||||
|
rect: Rect.fromLTRB(0, 0, paintWidth, paintHeight),
|
||||||
|
image: image,
|
||||||
|
filterQuality: FilterQuality.none,
|
||||||
|
);
|
||||||
|
|
||||||
|
final ui.Picture picture = pictureRecorder.endRecording();
|
||||||
|
scaledImage = await picture.toImage(
|
||||||
|
paintWidth.toInt(),
|
||||||
|
paintHeight.toInt(),
|
||||||
|
);
|
||||||
|
imageCompleter.complete(info.image);
|
||||||
|
},
|
||||||
|
onError: (Object exception, StackTrace? stackTrace) {
|
||||||
|
loadFailureTimeout?.cancel();
|
||||||
|
stream.removeListener(listener);
|
||||||
|
imageCompleter.completeError(
|
||||||
|
Exception('Failed to render image: $exception'),
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
loadFailureTimeout = Timer(const Duration(seconds: 5), () {
|
||||||
|
stream.removeListener(listener);
|
||||||
|
imageCompleter.completeError(
|
||||||
|
TimeoutException('Timeout occurred trying to load image'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.addListener(listener);
|
||||||
|
await imageCompleter.future;
|
||||||
|
return scaledImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts AABBGGRR color int to AARRGGBB format.
|
||||||
|
int _getArgbFromAbgr(int abgr) {
|
||||||
|
const int exceptRMask = 0xFF00FFFF;
|
||||||
|
const int onlyRMask = ~exceptRMask;
|
||||||
|
const int exceptBMask = 0xFFFFFF00;
|
||||||
|
const int onlyBMask = ~exceptBMask;
|
||||||
|
final int r = (abgr & onlyRMask) >> 16;
|
||||||
|
final int b = abgr & onlyBMask;
|
||||||
|
return (abgr & exceptRMask & exceptBMask) | (b << 16) | r;
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicScheme _buildDynamicScheme(
|
||||||
|
Brightness brightness,
|
||||||
|
Color seedColor,
|
||||||
|
DynamicSchemeVariant schemeVariant,
|
||||||
|
double contrastLevel,
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
contrastLevel >= -1.0 && contrastLevel <= 1.0,
|
||||||
|
'contrastLevel must be between -1.0 and 1.0 inclusive.',
|
||||||
|
);
|
||||||
|
final bool isDark = brightness == Brightness.dark;
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
final Hct sourceColor = Hct.fromInt(seedColor.value);
|
||||||
|
return switch (schemeVariant) {
|
||||||
|
DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.fidelity => SchemeFidelity(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.content => SchemeContent(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.monochrome => SchemeMonochrome(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.neutral => SchemeNeutral(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.vibrant => SchemeVibrant(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.expressive => SchemeExpressive(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.rainbow => SchemeRainbow(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(
|
||||||
|
sourceColorHct: sourceColor,
|
||||||
|
isDark: isDark,
|
||||||
|
contrastLevel: contrastLevel,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,35 +2,86 @@ import 'package:drift/drift.dart';
|
|||||||
|
|
||||||
import '../../sources/models.dart' as models;
|
import '../../sources/models.dart' as models;
|
||||||
import '../database.dart';
|
import '../database.dart';
|
||||||
|
import '../query.dart';
|
||||||
|
|
||||||
part 'library_dao.g.dart';
|
part 'library_dao.g.dart';
|
||||||
|
|
||||||
|
typedef AristListItem = ({
|
||||||
|
models.Artist artist,
|
||||||
|
int albumCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
typedef SongListItem = ({
|
||||||
|
models.Song song,
|
||||||
|
String? albumCoverArt,
|
||||||
|
});
|
||||||
|
|
||||||
|
extension on SortDirection {
|
||||||
|
OrderingMode toMode() => switch (this) {
|
||||||
|
SortDirection.asc => OrderingMode.asc,
|
||||||
|
SortDirection.desc => OrderingMode.desc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@DriftAccessor(include: {'../tables.drift'})
|
@DriftAccessor(include: {'../tables.drift'})
|
||||||
class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
||||||
with _$LibraryDaoMixin {
|
with _$LibraryDaoMixin {
|
||||||
LibraryDao(super.db);
|
LibraryDao(super.db);
|
||||||
|
|
||||||
Future<List<models.Album>> listAlbums({
|
Future<List<models.Album>> listAlbums(AlbumsQuery q) {
|
||||||
required int limit,
|
|
||||||
required int offset,
|
|
||||||
}) {
|
|
||||||
final query = albums.select()
|
final query = albums.select()
|
||||||
..where(
|
..where((albums) {
|
||||||
(f) => f.sourceId.equalsExp(
|
var filter = albums.sourceId.equals(q.sourceId);
|
||||||
subqueryExpression(db.sourcesDao.activeSourceId()),
|
for (final queryFilter in q.filter) {
|
||||||
),
|
filter &= switch (queryFilter) {
|
||||||
)
|
AlbumsFilterArtistId(:final artistId) => albums.artistId.equals(
|
||||||
..limit(limit, offset: offset);
|
artistId,
|
||||||
|
),
|
||||||
|
AlbumsFilterYearEquals(:final year) => albums.year.equals(year),
|
||||||
|
_ => CustomExpression(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
})
|
||||||
|
..orderBy(
|
||||||
|
q.sort
|
||||||
|
.map(
|
||||||
|
(sort) =>
|
||||||
|
(albums) => OrderingTerm(
|
||||||
|
expression: switch (sort.by) {
|
||||||
|
AlbumsColumn.name => albums.name,
|
||||||
|
AlbumsColumn.created => albums.created,
|
||||||
|
AlbumsColumn.year => albums.year,
|
||||||
|
AlbumsColumn.starred => albums.starred,
|
||||||
|
},
|
||||||
|
mode: sort.dir.toMode(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_limitQuery(query: query, limit: q.limit, offset: q.offset);
|
||||||
|
|
||||||
return query.get();
|
return query.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<({models.Artist artist, int albumCount})>> listArtists({
|
Future<List<AristListItem>> listArtists(ArtistsQuery q) {
|
||||||
required int limit,
|
|
||||||
required int offset,
|
|
||||||
}) async {
|
|
||||||
final albumCount = albums.id.count();
|
final albumCount = albums.id.count();
|
||||||
|
|
||||||
|
var filter =
|
||||||
|
artists.sourceId.equals(q.sourceId) &
|
||||||
|
albums.sourceId.equals(q.sourceId);
|
||||||
|
|
||||||
|
for (final queryFilter in q.filter) {
|
||||||
|
filter &= switch (queryFilter) {
|
||||||
|
ArtistsFilterStarred(:final starred) =>
|
||||||
|
starred ? artists.starred.isNotNull() : artists.starred.isNull(),
|
||||||
|
ArtistsFilterNameSearch() => CustomExpression(''),
|
||||||
|
_ => CustomExpression(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
final query =
|
final query =
|
||||||
artists.select().join([
|
artists.select().join([
|
||||||
leftOuterJoin(
|
leftOuterJoin(
|
||||||
@@ -39,25 +90,160 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
..addColumns([albumCount])
|
..addColumns([albumCount])
|
||||||
..where(
|
..where(filter)
|
||||||
artists.sourceId.equalsExp(
|
|
||||||
subqueryExpression(db.sourcesDao.activeSourceId()),
|
|
||||||
) &
|
|
||||||
albums.sourceId.equalsExp(
|
|
||||||
subqueryExpression(db.sourcesDao.activeSourceId()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..groupBy([artists.sourceId, artists.id])
|
..groupBy([artists.sourceId, artists.id])
|
||||||
..orderBy([OrderingTerm.asc(artists.name)])
|
..orderBy(
|
||||||
..limit(limit, offset: offset);
|
q.sort
|
||||||
|
.map(
|
||||||
|
(sort) => OrderingTerm(
|
||||||
|
expression: switch (sort.by) {
|
||||||
|
ArtistsColumn.name => artists.name,
|
||||||
|
ArtistsColumn.starred => artists.starred,
|
||||||
|
ArtistsColumn.albumCount => albumCount,
|
||||||
|
},
|
||||||
|
mode: sort.dir.toMode(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
return (await query.get())
|
_limitQuery(query: query, limit: q.limit, offset: q.offset);
|
||||||
|
|
||||||
|
return query
|
||||||
.map(
|
.map(
|
||||||
(row) => (
|
(row) => (
|
||||||
artist: row.readTable(artists),
|
artist: row.readTable(artists),
|
||||||
albumCount: row.read(albumCount) ?? 0,
|
albumCount: row.read(albumCount) ?? 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SongListItem>> listSongs(SongsQuery q) {
|
||||||
|
var joinPlaylistSongs = false;
|
||||||
|
var filter = songs.sourceId.equals(q.sourceId);
|
||||||
|
|
||||||
|
for (final queryFilter in q.filter) {
|
||||||
|
switch (queryFilter) {
|
||||||
|
case SongsFilterAlbumId(:final albumId):
|
||||||
|
filter &= songs.albumId.equals(albumId);
|
||||||
|
case SongsFilterPlaylistId(:final playlistId):
|
||||||
|
joinPlaylistSongs = true;
|
||||||
|
filter &= playlistSongs.playlistId.equals(playlistId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final query =
|
||||||
|
songs.select().join([
|
||||||
|
leftOuterJoin(
|
||||||
|
albums,
|
||||||
|
albums.id.equalsExp(songs.albumId) &
|
||||||
|
albums.sourceId.equals(q.sourceId),
|
||||||
|
),
|
||||||
|
if (joinPlaylistSongs)
|
||||||
|
leftOuterJoin(
|
||||||
|
playlistSongs,
|
||||||
|
playlistSongs.sourceId.equals(q.sourceId) &
|
||||||
|
playlistSongs.songId.equalsExp(songs.id),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..addColumns([
|
||||||
|
albums.coverArt,
|
||||||
|
])
|
||||||
|
..where(filter)
|
||||||
|
..orderBy(
|
||||||
|
q.sort
|
||||||
|
.map(
|
||||||
|
(sort) => OrderingTerm(
|
||||||
|
expression: switch (sort.by) {
|
||||||
|
SongsColumn.title => songs.title,
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_limitQuery(query: query, limit: q.limit, offset: q.offset);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return db.managers.albums.filter(
|
||||||
|
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
required int? offset,
|
||||||
|
}) {
|
||||||
|
if (limit != null) {
|
||||||
|
query.limit(limit, offset: offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,21 @@ import '../database.dart';
|
|||||||
|
|
||||||
part 'sources_dao.g.dart';
|
part 'sources_dao.g.dart';
|
||||||
|
|
||||||
|
typedef SourceSetting = ({Source source, SubsonicSetting subsonicSetting});
|
||||||
|
|
||||||
@DriftAccessor(include: {'../tables.drift'})
|
@DriftAccessor(include: {'../tables.drift'})
|
||||||
class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
||||||
with _$SourcesDaoMixin {
|
with _$SourcesDaoMixin {
|
||||||
SourcesDao(super.db);
|
SourcesDao(super.db);
|
||||||
|
|
||||||
JoinedSelectStatement<Sources, Source> activeSourceId() {
|
Selectable<int?> activeSourceId() {
|
||||||
return selectOnly(sources)
|
return (selectOnly(sources)
|
||||||
..addColumns([sources.id])
|
..addColumns([sources.id])
|
||||||
..where(sources.isActive.equals(true));
|
..where(sources.isActive.equals(true)))
|
||||||
|
.map((row) => row.read(sources.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<(Source, SubsonicSetting)>> listSources() {
|
Stream<List<SourceSetting>> listSources() {
|
||||||
final query = select(sources).join([
|
final query = select(sources).join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
subsonicSettings,
|
subsonicSettings,
|
||||||
@@ -23,18 +26,56 @@ class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return query.watch().map(
|
return query
|
||||||
(rows) => rows
|
.map(
|
||||||
.map(
|
(row) => (
|
||||||
(row) => (
|
source: row.readTable(sources),
|
||||||
row.readTable(sources),
|
subsonicSetting: row.readTable(subsonicSettings),
|
||||||
row.readTable(subsonicSettings),
|
),
|
||||||
),
|
)
|
||||||
)
|
.watch();
|
||||||
.toList(),
|
}
|
||||||
|
|
||||||
|
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 {
|
Future<void> setActiveSource(int id) async {
|
||||||
await transaction(() async {
|
await transaction(() async {
|
||||||
await db.managers.sources.update((o) => o(isActive: Value(null)));
|
await db.managers.sources.update((o) => o(isActive: Value(null)));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../sources/models.dart' as models;
|
|||||||
import 'converters.dart';
|
import 'converters.dart';
|
||||||
import 'dao/library_dao.dart';
|
import 'dao/library_dao.dart';
|
||||||
import 'dao/sources_dao.dart';
|
import 'dao/sources_dao.dart';
|
||||||
|
import 'log_interceptor.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
@@ -27,14 +28,14 @@ class SubtracksDatabase extends _$SubtracksDatabase {
|
|||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
name: 'my_database',
|
name: 'subtracks_database',
|
||||||
native: DriftNativeOptions(
|
native: DriftNativeOptions(
|
||||||
databasePath: () async {
|
databasePath: () async {
|
||||||
final directory = await getApplicationSupportDirectory();
|
final directory = await getApplicationSupportDirectory();
|
||||||
return path.join(directory.absolute.path, 'subtracks.sqlite');
|
return path.join(directory.absolute.path, 'subtracks.sqlite');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
).interceptWith(LogInterceptor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1471,6 +1471,15 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
|||||||
requiredDuringInsert: true,
|
requiredDuringInsert: true,
|
||||||
$customConstraints: 'NOT NULL',
|
$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
|
@override
|
||||||
List<GeneratedColumn> get $columns => [
|
List<GeneratedColumn> get $columns => [
|
||||||
sourceId,
|
sourceId,
|
||||||
@@ -1480,6 +1489,7 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
|||||||
coverArt,
|
coverArt,
|
||||||
created,
|
created,
|
||||||
changed,
|
changed,
|
||||||
|
public,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -1542,6 +1552,12 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
|||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_changedMeta);
|
context.missing(_changedMeta);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('public')) {
|
||||||
|
context.handle(
|
||||||
|
_publicMeta,
|
||||||
|
public.isAcceptableOrUnknown(data['public']!, _publicMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1575,6 +1591,10 @@ class Playlists extends Table with TableInfo<Playlists, models.Playlist> {
|
|||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
data['${effectivePrefix}cover_art'],
|
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<String?> coverArt;
|
||||||
final Value<DateTime> created;
|
final Value<DateTime> created;
|
||||||
final Value<DateTime> changed;
|
final Value<DateTime> changed;
|
||||||
|
final Value<bool?> public;
|
||||||
final Value<int> rowid;
|
final Value<int> rowid;
|
||||||
const PlaylistsCompanion({
|
const PlaylistsCompanion({
|
||||||
this.sourceId = const Value.absent(),
|
this.sourceId = const Value.absent(),
|
||||||
@@ -1609,6 +1630,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
this.coverArt = const Value.absent(),
|
this.coverArt = const Value.absent(),
|
||||||
this.created = const Value.absent(),
|
this.created = const Value.absent(),
|
||||||
this.changed = const Value.absent(),
|
this.changed = const Value.absent(),
|
||||||
|
this.public = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
});
|
});
|
||||||
PlaylistsCompanion.insert({
|
PlaylistsCompanion.insert({
|
||||||
@@ -1619,6 +1641,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
this.coverArt = const Value.absent(),
|
this.coverArt = const Value.absent(),
|
||||||
required DateTime created,
|
required DateTime created,
|
||||||
required DateTime changed,
|
required DateTime changed,
|
||||||
|
this.public = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
}) : sourceId = Value(sourceId),
|
}) : sourceId = Value(sourceId),
|
||||||
id = Value(id),
|
id = Value(id),
|
||||||
@@ -1633,6 +1656,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
Expression<String>? coverArt,
|
Expression<String>? coverArt,
|
||||||
Expression<DateTime>? created,
|
Expression<DateTime>? created,
|
||||||
Expression<DateTime>? changed,
|
Expression<DateTime>? changed,
|
||||||
|
Expression<bool>? public,
|
||||||
Expression<int>? rowid,
|
Expression<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
@@ -1643,6 +1667,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
if (coverArt != null) 'cover_art': coverArt,
|
if (coverArt != null) 'cover_art': coverArt,
|
||||||
if (created != null) 'created': created,
|
if (created != null) 'created': created,
|
||||||
if (changed != null) 'changed': changed,
|
if (changed != null) 'changed': changed,
|
||||||
|
if (public != null) 'public': public,
|
||||||
if (rowid != null) 'rowid': rowid,
|
if (rowid != null) 'rowid': rowid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1655,6 +1680,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
Value<String?>? coverArt,
|
Value<String?>? coverArt,
|
||||||
Value<DateTime>? created,
|
Value<DateTime>? created,
|
||||||
Value<DateTime>? changed,
|
Value<DateTime>? changed,
|
||||||
|
Value<bool?>? public,
|
||||||
Value<int>? rowid,
|
Value<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
return PlaylistsCompanion(
|
return PlaylistsCompanion(
|
||||||
@@ -1665,6 +1691,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
coverArt: coverArt ?? this.coverArt,
|
coverArt: coverArt ?? this.coverArt,
|
||||||
created: created ?? this.created,
|
created: created ?? this.created,
|
||||||
changed: changed ?? this.changed,
|
changed: changed ?? this.changed,
|
||||||
|
public: public ?? this.public,
|
||||||
rowid: rowid ?? this.rowid,
|
rowid: rowid ?? this.rowid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1693,6 +1720,9 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
if (changed.present) {
|
if (changed.present) {
|
||||||
map['changed'] = Variable<DateTime>(changed.value);
|
map['changed'] = Variable<DateTime>(changed.value);
|
||||||
}
|
}
|
||||||
|
if (public.present) {
|
||||||
|
map['public'] = Variable<bool>(public.value);
|
||||||
|
}
|
||||||
if (rowid.present) {
|
if (rowid.present) {
|
||||||
map['rowid'] = Variable<int>(rowid.value);
|
map['rowid'] = Variable<int>(rowid.value);
|
||||||
}
|
}
|
||||||
@@ -1709,6 +1739,7 @@ class PlaylistsCompanion extends UpdateCompanion<models.Playlist> {
|
|||||||
..write('coverArt: $coverArt, ')
|
..write('coverArt: $coverArt, ')
|
||||||
..write('created: $created, ')
|
..write('created: $created, ')
|
||||||
..write('changed: $changed, ')
|
..write('changed: $changed, ')
|
||||||
|
..write('public: $public, ')
|
||||||
..write('rowid: $rowid')
|
..write('rowid: $rowid')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
@@ -3432,6 +3463,7 @@ typedef $PlaylistsCreateCompanionBuilder =
|
|||||||
Value<String?> coverArt,
|
Value<String?> coverArt,
|
||||||
required DateTime created,
|
required DateTime created,
|
||||||
required DateTime changed,
|
required DateTime changed,
|
||||||
|
Value<bool?> public,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
});
|
});
|
||||||
typedef $PlaylistsUpdateCompanionBuilder =
|
typedef $PlaylistsUpdateCompanionBuilder =
|
||||||
@@ -3443,6 +3475,7 @@ typedef $PlaylistsUpdateCompanionBuilder =
|
|||||||
Value<String?> coverArt,
|
Value<String?> coverArt,
|
||||||
Value<DateTime> created,
|
Value<DateTime> created,
|
||||||
Value<DateTime> changed,
|
Value<DateTime> changed,
|
||||||
|
Value<bool?> public,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3489,6 +3522,11 @@ class $PlaylistsFilterComposer
|
|||||||
column: $table.changed,
|
column: $table.changed,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<bool> get public => $composableBuilder(
|
||||||
|
column: $table.public,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $PlaylistsOrderingComposer
|
class $PlaylistsOrderingComposer
|
||||||
@@ -3534,6 +3572,11 @@ class $PlaylistsOrderingComposer
|
|||||||
column: $table.changed,
|
column: $table.changed,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get public => $composableBuilder(
|
||||||
|
column: $table.public,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $PlaylistsAnnotationComposer
|
class $PlaylistsAnnotationComposer
|
||||||
@@ -3565,6 +3608,9 @@ class $PlaylistsAnnotationComposer
|
|||||||
|
|
||||||
GeneratedColumn<DateTime> get changed =>
|
GeneratedColumn<DateTime> get changed =>
|
||||||
$composableBuilder(column: $table.changed, builder: (column) => column);
|
$composableBuilder(column: $table.changed, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get public =>
|
||||||
|
$composableBuilder(column: $table.public, builder: (column) => column);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $PlaylistsTableManager
|
class $PlaylistsTableManager
|
||||||
@@ -3605,6 +3651,7 @@ class $PlaylistsTableManager
|
|||||||
Value<String?> coverArt = const Value.absent(),
|
Value<String?> coverArt = const Value.absent(),
|
||||||
Value<DateTime> created = const Value.absent(),
|
Value<DateTime> created = const Value.absent(),
|
||||||
Value<DateTime> changed = const Value.absent(),
|
Value<DateTime> changed = const Value.absent(),
|
||||||
|
Value<bool?> public = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) => PlaylistsCompanion(
|
}) => PlaylistsCompanion(
|
||||||
sourceId: sourceId,
|
sourceId: sourceId,
|
||||||
@@ -3614,6 +3661,7 @@ class $PlaylistsTableManager
|
|||||||
coverArt: coverArt,
|
coverArt: coverArt,
|
||||||
created: created,
|
created: created,
|
||||||
changed: changed,
|
changed: changed,
|
||||||
|
public: public,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
@@ -3625,6 +3673,7 @@ class $PlaylistsTableManager
|
|||||||
Value<String?> coverArt = const Value.absent(),
|
Value<String?> coverArt = const Value.absent(),
|
||||||
required DateTime created,
|
required DateTime created,
|
||||||
required DateTime changed,
|
required DateTime changed,
|
||||||
|
Value<bool?> public = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) => PlaylistsCompanion.insert(
|
}) => PlaylistsCompanion.insert(
|
||||||
sourceId: sourceId,
|
sourceId: sourceId,
|
||||||
@@ -3634,6 +3683,7 @@ class $PlaylistsTableManager
|
|||||||
coverArt: coverArt,
|
coverArt: coverArt,
|
||||||
created: created,
|
created: created,
|
||||||
changed: changed,
|
changed: changed,
|
||||||
|
public: public,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
lib/database/query.dart
Normal file
191
lib/database/query.dart
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'query.freezed.dart';
|
||||||
|
part 'query.g.dart';
|
||||||
|
|
||||||
|
enum SortDirection {
|
||||||
|
asc,
|
||||||
|
desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AlbumsColumn {
|
||||||
|
name,
|
||||||
|
created,
|
||||||
|
year,
|
||||||
|
starred,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ArtistsColumn {
|
||||||
|
name,
|
||||||
|
starred,
|
||||||
|
albumCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SongsColumn {
|
||||||
|
title,
|
||||||
|
starred,
|
||||||
|
disc,
|
||||||
|
track,
|
||||||
|
album,
|
||||||
|
artist,
|
||||||
|
albumArtist,
|
||||||
|
playlistPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlaylistsColumn {
|
||||||
|
name,
|
||||||
|
created,
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class SortingTerm with _$SortingTerm {
|
||||||
|
const factory SortingTerm.albums({
|
||||||
|
required SortDirection dir,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class AlbumsQuery with _$AlbumsQuery {
|
||||||
|
const factory AlbumsQuery({
|
||||||
|
required int sourceId,
|
||||||
|
@Default(IListConst([])) IList<AlbumsFilter> filter,
|
||||||
|
required IList<AlbumsSortingTerm> sort,
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
}) = _AlbumsQuery;
|
||||||
|
|
||||||
|
factory AlbumsQuery.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$AlbumsQueryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class AlbumsFilter with _$AlbumsFilter {
|
||||||
|
const factory AlbumsFilter.artistId(String artistId) = AlbumsFilterArtistId;
|
||||||
|
const factory AlbumsFilter.yearEquals(int year) = AlbumsFilterYearEquals;
|
||||||
|
|
||||||
|
factory AlbumsFilter.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$AlbumsFilterFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class ArtistsQuery with _$ArtistsQuery {
|
||||||
|
const factory ArtistsQuery({
|
||||||
|
required int sourceId,
|
||||||
|
@Default(IListConst([])) IList<ArtistsFilter> filter,
|
||||||
|
required IList<ArtistsSortingTerm> sort,
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
}) = _ArtistsQuery;
|
||||||
|
|
||||||
|
factory ArtistsQuery.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ArtistsQueryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class ArtistsFilter with _$ArtistsFilter {
|
||||||
|
const factory ArtistsFilter.nameSearch(String name) = ArtistsFilterNameSearch;
|
||||||
|
const factory ArtistsFilter.starred(bool starred) = ArtistsFilterStarred;
|
||||||
|
|
||||||
|
factory ArtistsFilter.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$ArtistsFilterFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class SongsQuery with _$SongsQuery {
|
||||||
|
const factory SongsQuery({
|
||||||
|
required int sourceId,
|
||||||
|
@Default(IListConst([])) IList<SongsFilter> filter,
|
||||||
|
required IList<SongsSortingTerm> sort,
|
||||||
|
int? limit,
|
||||||
|
int? offset,
|
||||||
|
}) = _SongsQuery;
|
||||||
|
|
||||||
|
factory SongsQuery.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SongsQueryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class SongsFilter with _$SongsFilter {
|
||||||
|
const factory SongsFilter.albumId(String albumId) = SongsFilterAlbumId;
|
||||||
|
const factory SongsFilter.playlistId(String playlistId) =
|
||||||
|
SongsFilterPlaylistId;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
3009
lib/database/query.freezed.dart
Normal file
3009
lib/database/query.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
303
lib/database/query.g.dart
Normal file
303
lib/database/query.g.dart
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'query.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
AlbumsSortingTerm _$AlbumsSortingTermFromJson(Map<String, dynamic> json) =>
|
||||||
|
AlbumsSortingTerm(
|
||||||
|
dir: $enumDecode(_$SortDirectionEnumMap, json['dir']),
|
||||||
|
by: $enumDecode(_$AlbumsColumnEnumMap, json['by']),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AlbumsSortingTermToJson(AlbumsSortingTerm instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'dir': _$SortDirectionEnumMap[instance.dir]!,
|
||||||
|
'by': _$AlbumsColumnEnumMap[instance.by]!,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$SortDirectionEnumMap = {
|
||||||
|
SortDirection.asc: 'asc',
|
||||||
|
SortDirection.desc: 'desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$AlbumsColumnEnumMap = {
|
||||||
|
AlbumsColumn.name: 'name',
|
||||||
|
AlbumsColumn.created: 'created',
|
||||||
|
AlbumsColumn.year: 'year',
|
||||||
|
AlbumsColumn.starred: 'starred',
|
||||||
|
};
|
||||||
|
|
||||||
|
ArtistsSortingTerm _$ArtistsSortingTermFromJson(Map<String, dynamic> json) =>
|
||||||
|
ArtistsSortingTerm(
|
||||||
|
dir: $enumDecode(_$SortDirectionEnumMap, json['dir']),
|
||||||
|
by: $enumDecode(_$ArtistsColumnEnumMap, json['by']),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ArtistsSortingTermToJson(ArtistsSortingTerm instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'dir': _$SortDirectionEnumMap[instance.dir]!,
|
||||||
|
'by': _$ArtistsColumnEnumMap[instance.by]!,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$ArtistsColumnEnumMap = {
|
||||||
|
ArtistsColumn.name: 'name',
|
||||||
|
ArtistsColumn.starred: 'starred',
|
||||||
|
ArtistsColumn.albumCount: 'albumCount',
|
||||||
|
};
|
||||||
|
|
||||||
|
SongsSortingTerm _$SongsSortingTermFromJson(Map<String, dynamic> json) =>
|
||||||
|
SongsSortingTerm(
|
||||||
|
dir: $enumDecode(_$SortDirectionEnumMap, json['dir']),
|
||||||
|
by: $enumDecode(_$SongsColumnEnumMap, json['by']),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SongsSortingTermToJson(SongsSortingTerm instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'dir': _$SortDirectionEnumMap[instance.dir]!,
|
||||||
|
'by': _$SongsColumnEnumMap[instance.by]!,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$SongsColumnEnumMap = {
|
||||||
|
SongsColumn.title: 'title',
|
||||||
|
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(
|
||||||
|
sourceId: (json['sourceId'] as num).toInt(),
|
||||||
|
filter: json['filter'] == null
|
||||||
|
? const IListConst([])
|
||||||
|
: IList<AlbumsFilter>.fromJson(
|
||||||
|
json['filter'],
|
||||||
|
(value) => AlbumsFilter.fromJson(value as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
sort: IList<AlbumsSortingTerm>.fromJson(
|
||||||
|
json['sort'],
|
||||||
|
(value) => AlbumsSortingTerm.fromJson(value as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
limit: (json['limit'] as num?)?.toInt(),
|
||||||
|
offset: (json['offset'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AlbumsQueryToJson(_AlbumsQuery instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'sourceId': instance.sourceId,
|
||||||
|
'filter': instance.filter.toJson((value) => value),
|
||||||
|
'sort': instance.sort.toJson((value) => value),
|
||||||
|
'limit': instance.limit,
|
||||||
|
'offset': instance.offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
AlbumsFilterArtistId _$AlbumsFilterArtistIdFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => AlbumsFilterArtistId(
|
||||||
|
json['artistId'] as String,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AlbumsFilterArtistIdToJson(
|
||||||
|
AlbumsFilterArtistId instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'artistId': instance.artistId,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
AlbumsFilterYearEquals _$AlbumsFilterYearEqualsFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => AlbumsFilterYearEquals(
|
||||||
|
(json['year'] as num).toInt(),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$AlbumsFilterYearEqualsToJson(
|
||||||
|
AlbumsFilterYearEquals instance,
|
||||||
|
) => <String, dynamic>{'year': instance.year, 'runtimeType': instance.$type};
|
||||||
|
|
||||||
|
_ArtistsQuery _$ArtistsQueryFromJson(Map<String, dynamic> json) =>
|
||||||
|
_ArtistsQuery(
|
||||||
|
sourceId: (json['sourceId'] as num).toInt(),
|
||||||
|
filter: json['filter'] == null
|
||||||
|
? const IListConst([])
|
||||||
|
: IList<ArtistsFilter>.fromJson(
|
||||||
|
json['filter'],
|
||||||
|
(value) => ArtistsFilter.fromJson(value as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
sort: IList<ArtistsSortingTerm>.fromJson(
|
||||||
|
json['sort'],
|
||||||
|
(value) => ArtistsSortingTerm.fromJson(value as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
limit: (json['limit'] as num?)?.toInt(),
|
||||||
|
offset: (json['offset'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ArtistsQueryToJson(_ArtistsQuery instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'sourceId': instance.sourceId,
|
||||||
|
'filter': instance.filter.toJson((value) => value),
|
||||||
|
'sort': instance.sort.toJson((value) => value),
|
||||||
|
'limit': instance.limit,
|
||||||
|
'offset': instance.offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
ArtistsFilterNameSearch _$ArtistsFilterNameSearchFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => ArtistsFilterNameSearch(
|
||||||
|
json['name'] as String,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ArtistsFilterNameSearchToJson(
|
||||||
|
ArtistsFilterNameSearch instance,
|
||||||
|
) => <String, dynamic>{'name': instance.name, 'runtimeType': instance.$type};
|
||||||
|
|
||||||
|
ArtistsFilterStarred _$ArtistsFilterStarredFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => ArtistsFilterStarred(
|
||||||
|
json['starred'] as bool,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ArtistsFilterStarredToJson(
|
||||||
|
ArtistsFilterStarred instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'starred': instance.starred,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
_SongsQuery _$SongsQueryFromJson(Map<String, dynamic> json) => _SongsQuery(
|
||||||
|
sourceId: (json['sourceId'] as num).toInt(),
|
||||||
|
filter: json['filter'] == null
|
||||||
|
? const IListConst([])
|
||||||
|
: IList<SongsFilter>.fromJson(
|
||||||
|
json['filter'],
|
||||||
|
(value) => SongsFilter.fromJson(value as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
sort: IList<SongsSortingTerm>.fromJson(
|
||||||
|
json['sort'],
|
||||||
|
(value) => SongsSortingTerm.fromJson(value as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
limit: (json['limit'] as num?)?.toInt(),
|
||||||
|
offset: (json['offset'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SongsQueryToJson(_SongsQuery instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'sourceId': instance.sourceId,
|
||||||
|
'filter': instance.filter.toJson((value) => value),
|
||||||
|
'sort': instance.sort.toJson((value) => value),
|
||||||
|
'limit': instance.limit,
|
||||||
|
'offset': instance.offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
SongsFilterAlbumId _$SongsFilterAlbumIdFromJson(Map<String, dynamic> json) =>
|
||||||
|
SongsFilterAlbumId(
|
||||||
|
json['albumId'] as String,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SongsFilterAlbumIdToJson(SongsFilterAlbumId instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'albumId': instance.albumId,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
SongsFilterPlaylistId _$SongsFilterPlaylistIdFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => SongsFilterPlaylistId(
|
||||||
|
json['playlistId'] as String,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SongsFilterPlaylistIdToJson(
|
||||||
|
SongsFilterPlaylistId instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'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,
|
cover_art TEXT,
|
||||||
created DATETIME NOT NULL,
|
created DATETIME NOT NULL,
|
||||||
changed DATETIME NOT NULL,
|
changed DATETIME NOT NULL,
|
||||||
|
public BOOLEAN,
|
||||||
PRIMARY KEY (source_id, id),
|
PRIMARY KEY (source_id, id),
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||||
) WITH Playlist;
|
) WITH Playlist;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'app/router.dart';
|
import 'app/router.dart';
|
||||||
|
import 'app/ui/theme.dart';
|
||||||
import 'l10n/generated/app_localizations.dart';
|
import 'l10n/generated/app_localizations.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -17,7 +18,7 @@ class MainApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
themeMode: ThemeMode.dark,
|
themeMode: ThemeMode.dark,
|
||||||
darkTheme: ThemeData.dark(useMaterial3: true),
|
darkTheme: subtracksTheme(),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../database/database.dart';
|
import '../database/database.dart';
|
||||||
import '../sources/music_source.dart';
|
import '../sources/music_source.dart';
|
||||||
|
|
||||||
const kSliceSize = 200;
|
const kSliceSize = 200;
|
||||||
|
|
||||||
class SyncService {
|
class SyncService with ChangeNotifier {
|
||||||
SyncService({
|
SyncService({
|
||||||
required this.source,
|
required this.source,
|
||||||
required this.db,
|
required this.db,
|
||||||
@@ -28,6 +29,7 @@ class SyncService {
|
|||||||
syncPlaylistSongs(),
|
syncPlaylistSongs(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> syncArtists() async {
|
Future<void> syncArtists() async {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Playlist mapPlaylist(XmlElement e) => Playlist(
|
|||||||
coverArt: e.getAttribute('coverArt'),
|
coverArt: e.getAttribute('coverArt'),
|
||||||
created: DateTime.parse(e.getAttribute('created')!),
|
created: DateTime.parse(e.getAttribute('created')!),
|
||||||
changed: DateTime.parse(e.getAttribute('changed')!),
|
changed: DateTime.parse(e.getAttribute('changed')!),
|
||||||
|
public: bool.tryParse(e.getAttribute('public') ?? ''),
|
||||||
owner: e.getAttribute('owner'),
|
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();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[tools]
|
[tools]
|
||||||
android-sdk = "latest"
|
android-sdk = "latest"
|
||||||
deno = "2.5.3"
|
deno = "2.5.3"
|
||||||
flutter = "3.35"
|
flutter = "3.38"
|
||||||
java = "17"
|
java = "17"
|
||||||
yq = "latest"
|
yq = "latest"
|
||||||
|
|||||||
172
pubspec.lock
172
pubspec.lock
@@ -5,34 +5,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "85.0.0"
|
version: "91.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
|
sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.6.0"
|
version: "8.4.0"
|
||||||
analyzer_buffer:
|
analyzer_buffer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer_buffer
|
name: analyzer_buffer
|
||||||
sha256: f7833bee67c03c37241c67f8741b17cc501b69d9758df7a5a4a13ed6c947be43
|
sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.10"
|
version: "0.1.11"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_plugin
|
||||||
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
|
sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.4"
|
version: "0.13.10"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -61,10 +61,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d
|
sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "4.0.3"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -77,34 +77,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_daemon
|
name: build_daemon
|
||||||
sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d"
|
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.1"
|
||||||
build_resolvers:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_resolvers
|
|
||||||
sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.3"
|
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
|
sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.1"
|
version: "2.10.4"
|
||||||
build_runner_core:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_runner_core
|
|
||||||
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "9.3.1"
|
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -117,10 +101,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.0"
|
version: "8.12.1"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -245,50 +229,50 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.7"
|
||||||
custom_lint:
|
custom_lint:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: custom_lint
|
name: custom_lint
|
||||||
sha256: "78085fbe842de7c5bef92de811ca81536968dbcbbcdac5c316711add2d15e796"
|
sha256: "751ee9440920f808266c3ec2553420dea56d3c7837dd2d62af76b11be3fcece5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.8.1"
|
||||||
custom_lint_builder:
|
custom_lint_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_builder
|
name: custom_lint_builder
|
||||||
sha256: cc5532d5733d4eccfccaaec6070a1926e9f21e613d93ad0927fad020b95c9e52
|
sha256: "1128db6f58e71d43842f3b9be7465c83f0c47f4dd8918f878dd6ad3b72a32072"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.8.1"
|
||||||
custom_lint_core:
|
custom_lint_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_core
|
name: custom_lint_core
|
||||||
sha256: cc4684d22ca05bf0a4a51127e19a8aea576b42079ed2bc9e956f11aaebe35dd1
|
sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.8.1"
|
||||||
custom_lint_visitor:
|
custom_lint_visitor:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: custom_lint_visitor
|
name: custom_lint_visitor
|
||||||
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
|
sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0+7.7.0"
|
version: "1.0.0+8.4.0"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
|
sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.3"
|
||||||
drift:
|
drift:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -449,10 +433,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: e1d7ffb0db475e6e845eb58b44768f50b830e23960e3df6908924acd8f7f70ea
|
sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.2.5"
|
version: "17.0.0"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -481,10 +465,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.6.0"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -545,10 +529,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: json_serializable
|
name: json_serializable
|
||||||
sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe"
|
sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.11.1"
|
version: "6.11.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -581,6 +565,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
logger:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: logger
|
||||||
|
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.2"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -598,7 +590,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
@@ -617,10 +609,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -637,6 +629,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.0"
|
||||||
octo_image:
|
octo_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -689,18 +689,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
|
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.20"
|
version: "2.2.22"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
|
sha256: "6192e477f34018ef1ea790c56fffc7302e3bc3efede9e798b934c252c8c105ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.3"
|
version: "2.5.0"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -851,7 +851,7 @@ packages:
|
|||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
sliver_tools:
|
sliver_tools:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sliver_tools
|
name: sliver_tools
|
||||||
sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6
|
sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6
|
||||||
@@ -862,10 +862,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
|
sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "4.1.1"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -898,14 +898,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
sprintf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sprintf
|
|
||||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "7.0.0"
|
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1030,34 +1022,26 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
|
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.26.2"
|
version: "1.26.3"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.7"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
|
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.11"
|
version: "0.6.12"
|
||||||
timing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: timing
|
|
||||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1078,34 +1062,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
|
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.24"
|
version: "6.3.28"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
|
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.5"
|
version: "6.3.6"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_linux
|
name: url_launcher_linux
|
||||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.2"
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.4"
|
version: "3.2.5"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1126,18 +1110,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_windows
|
name: url_launcher_windows
|
||||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.1"
|
version: "4.5.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -20,18 +20,21 @@ dependencies:
|
|||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
freezed_annotation: ^3.1.0
|
freezed_annotation: ^3.1.0
|
||||||
go_router: ^16.2.5
|
go_router: ^17.0.0
|
||||||
hooks_riverpod: ^3.0.3
|
hooks_riverpod: ^3.0.3
|
||||||
http: ^1.5.0
|
http: ^1.5.0
|
||||||
infinite_scroll_pagination: ^5.1.1
|
infinite_scroll_pagination: ^5.1.1
|
||||||
intl: any
|
intl: any
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
|
logger: ^2.6.2
|
||||||
|
material_color_utilities: ^0.11.1
|
||||||
material_symbols_icons: ^4.2874.0
|
material_symbols_icons: ^4.2874.0
|
||||||
octo_image: ^2.1.0
|
octo_image: ^2.1.0
|
||||||
package_info_plus: ^9.0.0
|
package_info_plus: ^9.0.0
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
pool: ^1.5.2
|
pool: ^1.5.2
|
||||||
|
sliver_tools: ^0.2.12
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
xml: ^6.6.1
|
xml: ^6.6.1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user