From 798a907cca0c675553bcc8754a5207b87e041342 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 22 Nov 2025 11:33:40 +0900 Subject: [PATCH] active source switching and reactivity --- lib/app/lists/albums_grid.dart | 18 +- lib/app/lists/artists_list.dart | 39 +- lib/app/screens/settings_screen.dart | 163 ++++--- lib/app/state/database.dart | 27 +- lib/app/state/source.dart | 10 +- lib/database/dao/library_dao.dart | 63 +++ lib/database/dao/library_dao.g.dart | 14 + lib/database/dao/sources_dao.dart | 46 ++ lib/database/dao/sources_dao.g.dart | 14 + lib/database/database.dart | 648 +-------------------------- lib/database/database.g.dart | 2 + lib/images/images.dart | 1 - 12 files changed, 271 insertions(+), 774 deletions(-) create mode 100644 lib/database/dao/library_dao.dart create mode 100644 lib/database/dao/library_dao.g.dart create mode 100644 lib/database/dao/sources_dao.dart create mode 100644 lib/database/dao/sources_dao.g.dart diff --git a/lib/app/lists/albums_grid.dart b/lib/app/lists/albums_grid.dart index 49781b6..d2afc86 100644 --- a/lib/app/lists/albums_grid.dart +++ b/lib/app/lists/albums_grid.dart @@ -1,5 +1,5 @@ -import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -23,15 +23,17 @@ class AlbumsGrid extends HookConsumerWidget { final controller = usePagingController( getNextPageKey: (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey, - fetchPage: (pageKey) async { - final query = db.albums.select() - ..where((f) => f.sourceId.equals(sourceId)) - ..limit(kPageSize, offset: (pageKey - 1) * kPageSize); - - return await query.get(); - }, + fetchPage: (pageKey) => db.libraryDao.listAlbums( + limit: kPageSize, + offset: (pageKey - 1) * kPageSize, + ), ); + useEffect(() { + controller.refresh(); + return; + }, [sourceId]); + return PagingListener( controller: controller, builder: (context, state, fetchNextPage) { diff --git a/lib/app/lists/artists_list.dart b/lib/app/lists/artists_list.dart index 7379997..3ff808a 100644 --- a/lib/app/lists/artists_list.dart +++ b/lib/app/lists/artists_list.dart @@ -1,5 +1,5 @@ -import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -25,36 +25,17 @@ class ArtistsList extends HookConsumerWidget { final controller = usePagingController( getNextPageKey: (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey, - fetchPage: (pageKey) async { - final albumCount = db.albums.id.count(); - - final query = - db.artists.select().join([ - leftOuterJoin( - db.albums, - db.albums.artistId.equalsExp(db.artists.id), - ), - ]) - ..addColumns([albumCount]) - ..where( - db.artists.sourceId.equals(sourceId) & - db.albums.sourceId.equals(sourceId), - ) - ..groupBy([db.artists.sourceId, db.artists.id]) - ..orderBy([OrderingTerm.asc(db.artists.name)]) - ..limit(kPageSize, offset: (pageKey - 1) * kPageSize); - - return (await query.get()) - .map( - (row) => ( - artist: row.readTable(db.artists), - albumCount: row.read(albumCount), - ), - ) - .toList(); - }, + fetchPage: (pageKey) => db.libraryDao.listArtists( + limit: kPageSize, + offset: (pageKey - 1) * kPageSize, + ), ); + useEffect(() { + controller.refresh(); + return; + }, [sourceId]); + return PagingListener( controller: controller, builder: (context, state, fetchNextPage) { diff --git a/lib/app/screens/settings_screen.dart b/lib/app/screens/settings_screen.dart index 50709ef..035451d 100644 --- a/lib/app/screens/settings_screen.dart +++ b/lib/app/screens/settings_screen.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../l10n/generated/app_localizations.dart'; +import '../state/database.dart'; +import '../state/source.dart'; +import '../ui/text.dart'; -const kHorizontalPadding = 16.0; +const kHorizontalPadding = 18.0; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @@ -13,11 +17,14 @@ class SettingsScreen extends HookConsumerWidget { final l = AppLocalizations.of(context); return Scaffold( + appBar: AppBar( + title: TextH1(l.navigationTabsSettings), + ), body: ListView( children: [ - const SizedBox(height: 96), + // const SizedBox(height: 96), _SectionHeader(l.settingsServersName), - // const _Sources(), + const _Sources(), // _SectionHeader(l.settingsNetworkName), // const _Network(), // _SectionHeader(l.settingsAboutName), @@ -29,10 +36,10 @@ class SettingsScreen extends HookConsumerWidget { } class _Section extends StatelessWidget { - final List children; - const _Section({required this.children}); + final List children; + @override Widget build(BuildContext context) { return Column( @@ -46,27 +53,22 @@ class _Section extends StatelessWidget { } class _SectionHeader extends StatelessWidget { - final String title; - const _SectionHeader(this.title); + final String title; + @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Column( children: [ + const SizedBox(height: 16), SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding), - child: Text( - title, - style: theme.textTheme.displaySmall, - ), + child: TextH2(title), ), ), - const SizedBox(height: 12), ], ); } @@ -346,79 +348,66 @@ class _SectionHeader extends StatelessWidget { // } // } -// class _Sources extends HookConsumerWidget { -// const _Sources(); +class _Sources extends HookConsumerWidget { + const _Sources(); -// @override -// Widget build(BuildContext context, WidgetRef ref) { -// final sources = ref.watch( -// settingsServiceProvider.select( -// (value) => value.sources, -// ), -// ); -// final activeSource = ref.watch( -// settingsServiceProvider.select( -// (value) => value.activeSource, -// ), -// ); + @override + Widget build(BuildContext context, WidgetRef ref) { + final db = ref.watch(databaseProvider); + final activeSourceId = ref.watch(sourceIdProvider); + final sources = useStream(db.sourcesDao.listSources()).data; -// final l = AppLocalizations.of(context); + final l = AppLocalizations.of(context); -// return _Section( -// children: [ -// for (var source in sources) -// RadioListTile( -// value: source.id, -// groupValue: activeSource?.id, -// onChanged: (value) { -// ref -// .read(settingsServiceProvider.notifier) -// .setActiveSource(source.id); -// }, -// title: Text(source.name), -// subtitle: Text( -// source.address.toString(), -// maxLines: 1, -// softWrap: false, -// overflow: TextOverflow.fade, -// ), -// secondary: IconButton( -// icon: const Icon(Icons.edit_rounded), -// onPressed: () { -// context.pushRoute(SourceRoute(id: source.id)); -// }, -// ), -// ), -// const SizedBox(height: 8), -// Row( -// mainAxisAlignment: MainAxisAlignment.center, -// children: [ -// OutlinedButton.icon( -// icon: const Icon(Icons.add_rounded), -// label: Text(l.settingsServersActionsAdd), -// onPressed: () { -// context.pushRoute(SourceRoute()); -// }, -// ), -// ], -// ), -// // TODO: remove -// if (kDebugMode) -// Row( -// mainAxisAlignment: MainAxisAlignment.center, -// children: [ -// OutlinedButton.icon( -// icon: const Icon(Icons.add_rounded), -// label: const Text('Add TEST'), -// onPressed: () { -// ref -// .read(settingsServiceProvider.notifier) -// .addTestSource('TEST'); -// }, -// ), -// ], -// ), -// ], -// ); -// } -// } + if (sources == null) { + return Container(); + } + + return _Section( + children: [ + RadioGroup( + groupValue: activeSourceId, + onChanged: (value) { + if (value != null) { + db.sourcesDao.setActiveSource(value); + } + }, + child: Column( + children: [ + for (final (source, settings) in sources) + RadioListTile( + value: source.id, + title: Text(source.name), + subtitle: Text( + settings.address.toString(), + maxLines: 1, + softWrap: false, + overflow: TextOverflow.fade, + ), + secondary: IconButton( + icon: const Icon(Icons.edit_rounded), + onPressed: () { + // context.pushRoute(SourceRoute(id: source.id)); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton.icon( + icon: const Icon(Icons.add_rounded), + label: Text(l.settingsServersActionsAdd), + onPressed: () { + // context.pushRoute(SourceRoute()); + }, + ), + ], + ), + ], + ); + } +} diff --git a/lib/app/state/database.dart b/lib/app/state/database.dart index e63a467..62b98e5 100644 --- a/lib/app/state/database.dart +++ b/lib/app/state/database.dart @@ -11,8 +11,8 @@ final databaseInitializer = FutureProvider((ref) async { .insertOnConflictUpdate( SourcesCompanion.insert( id: Value(1), - name: 'test navidrome', - isActive: Value(true), + name: 'test subsonic', + // isActive: Value(true), ), ); await db @@ -23,9 +23,26 @@ final databaseInitializer = FutureProvider((ref) async { address: Uri.parse('http://demo.subsonic.org'), username: 'guest1', password: 'guest', - // address: Uri.parse('http://10.0.2.2:4533'), - // username: 'admin', - // password: 'password', + useTokenAuth: Value(true), + ), + ); + await db + .into(db.sources) + .insertOnConflictUpdate( + SourcesCompanion.insert( + id: Value(2), + name: 'test navidrome', + // isActive: Value(null), + ), + ); + await db + .into(db.subsonicSettings) + .insertOnConflictUpdate( + SubsonicSettingsCompanion.insert( + sourceId: Value(2), + address: Uri.parse('http://10.0.2.2:4533'), + username: 'admin', + password: 'password', useTokenAuth: Value(true), ), ); diff --git a/lib/app/state/source.dart b/lib/app/state/source.dart index 5cf55f4..60b9532 100644 --- a/lib/app/state/source.dart +++ b/lib/app/state/source.dart @@ -10,17 +10,17 @@ final activeSourceInitializer = StreamProvider<(int, SubsonicSource)>(( ) async* { final db = ref.watch(databaseProvider); - final activeSource = db.managers.sources - .filter((f) => f.isActive.equals(true)) - .watchSingle(); + final activeSource = db.sourcesDao.activeSourceId().watchSingle(); await for (final source in activeSource) { + final sourceId = source.read(db.sources.id)!; + final subsonicSettings = await db.managers.subsonicSettings - .filter((f) => f.sourceId.equals(source.id)) + .filter((f) => f.sourceId.equals(sourceId)) .getSingle(); yield ( - source.id, + sourceId, SubsonicSource( SubsonicClient( http: SubtracksHttpClient(), diff --git a/lib/database/dao/library_dao.dart b/lib/database/dao/library_dao.dart new file mode 100644 index 0000000..888187a --- /dev/null +++ b/lib/database/dao/library_dao.dart @@ -0,0 +1,63 @@ +import 'package:drift/drift.dart'; + +import '../../sources/models.dart' as models; +import '../database.dart'; + +part 'library_dao.g.dart'; + +@DriftAccessor(include: {'../tables.drift'}) +class LibraryDao extends DatabaseAccessor + with _$LibraryDaoMixin { + LibraryDao(super.db); + + Future> listAlbums({ + required int limit, + required int offset, + }) { + final query = albums.select() + ..where( + (f) => f.sourceId.equalsExp( + subqueryExpression(db.sourcesDao.activeSourceId()), + ), + ) + ..limit(limit, offset: offset); + + return query.get(); + } + + Future> listArtists({ + required int limit, + required int offset, + }) async { + final albumCount = albums.id.count(); + + final query = + artists.select().join([ + leftOuterJoin( + albums, + albums.artistId.equalsExp(artists.id), + ), + ]) + ..addColumns([albumCount]) + ..where( + artists.sourceId.equalsExp( + subqueryExpression(db.sourcesDao.activeSourceId()), + ) & + albums.sourceId.equalsExp( + subqueryExpression(db.sourcesDao.activeSourceId()), + ), + ) + ..groupBy([artists.sourceId, artists.id]) + ..orderBy([OrderingTerm.asc(artists.name)]) + ..limit(limit, offset: offset); + + return (await query.get()) + .map( + (row) => ( + artist: row.readTable(artists), + albumCount: row.read(albumCount) ?? 0, + ), + ) + .toList(); + } +} diff --git a/lib/database/dao/library_dao.g.dart b/lib/database/dao/library_dao.g.dart new file mode 100644 index 0000000..ff9d0ed --- /dev/null +++ b/lib/database/dao/library_dao.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_dao.dart'; + +// ignore_for_file: type=lint +mixin _$LibraryDaoMixin on DatabaseAccessor { + Sources get sources => attachedDatabase.sources; + SubsonicSettings get subsonicSettings => attachedDatabase.subsonicSettings; + Artists get artists => attachedDatabase.artists; + Albums get albums => attachedDatabase.albums; + Playlists get playlists => attachedDatabase.playlists; + PlaylistSongs get playlistSongs => attachedDatabase.playlistSongs; + Songs get songs => attachedDatabase.songs; +} diff --git a/lib/database/dao/sources_dao.dart b/lib/database/dao/sources_dao.dart new file mode 100644 index 0000000..dd9fbd2 --- /dev/null +++ b/lib/database/dao/sources_dao.dart @@ -0,0 +1,46 @@ +import 'package:drift/drift.dart'; + +import '../database.dart'; + +part 'sources_dao.g.dart'; + +@DriftAccessor(include: {'../tables.drift'}) +class SourcesDao extends DatabaseAccessor + with _$SourcesDaoMixin { + SourcesDao(super.db); + + JoinedSelectStatement activeSourceId() { + return selectOnly(sources) + ..addColumns([sources.id]) + ..where(sources.isActive.equals(true)); + } + + Stream> listSources() { + final query = select(sources).join([ + innerJoin( + subsonicSettings, + sources.id.equalsExp(subsonicSettings.sourceId), + ), + ]); + + return query.watch().map( + (rows) => rows + .map( + (row) => ( + row.readTable(sources), + row.readTable(subsonicSettings), + ), + ) + .toList(), + ); + } + + Future setActiveSource(int id) async { + await transaction(() async { + await db.managers.sources.update((o) => o(isActive: Value(null))); + await db.managers.sources + .filter((f) => f.id.equals(id)) + .update((o) => o(isActive: Value(true))); + }); + } +} diff --git a/lib/database/dao/sources_dao.g.dart b/lib/database/dao/sources_dao.g.dart new file mode 100644 index 0000000..76b1a76 --- /dev/null +++ b/lib/database/dao/sources_dao.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sources_dao.dart'; + +// ignore_for_file: type=lint +mixin _$SourcesDaoMixin on DatabaseAccessor { + Sources get sources => attachedDatabase.sources; + SubsonicSettings get subsonicSettings => attachedDatabase.subsonicSettings; + Artists get artists => attachedDatabase.artists; + Albums get albums => attachedDatabase.albums; + Playlists get playlists => attachedDatabase.playlists; + PlaylistSongs get playlistSongs => attachedDatabase.playlistSongs; + Songs get songs => attachedDatabase.songs; +} diff --git a/lib/database/database.dart b/lib/database/database.dart index 79c714b..290fdee 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -5,6 +5,8 @@ import 'package:path_provider/path_provider.dart'; import '../sources/models.dart' as models; import 'converters.dart'; +import 'dao/library_dao.dart'; +import 'dao/sources_dao.dart'; part 'database.g.dart'; @@ -12,7 +14,13 @@ part 'database.g.dart'; // https://www.sqlite.org/limits.html const kSqliteMaxVariableNumber = 32766; -@DriftDatabase(include: {'tables.drift'}) +@DriftDatabase( + include: {'tables.drift'}, + daos: [ + SourcesDao, + LibraryDao, + ], +) class SubtracksDatabase extends _$SubtracksDatabase { SubtracksDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @@ -40,395 +48,6 @@ class SubtracksDatabase extends _$SubtracksDatabase { }, ); } - - // MultiSelectable albumsList(int sourceId, ListQuery opt) { - // return filterAlbums( - // (_) => _filterPredicate('albums', sourceId, opt), - // (_) => _filterOrderBy(opt), - // (_) => _filterLimit(opt), - // ); - // } - - // MultiSelectable albumsListDownloaded(int sourceId, ListQuery opt) { - // return filterAlbumsDownloaded( - // (_, __) => _filterPredicate('albums', sourceId, opt), - // (_, __) => _filterOrderBy(opt), - // (_, __) => _filterLimit(opt), - // ); - // } - - // MultiSelectable artistsList(int sourceId, ListQuery opt) { - // return filterArtists( - // (_) => _filterPredicate('artists', sourceId, opt), - // (_) => _filterOrderBy(opt), - // (_) => _filterLimit(opt), - // ); - // } - - // MultiSelectable artistsListDownloaded(int sourceId, ListQuery opt) { - // return filterArtistsDownloaded( - // (_, __, ___) => _filterPredicate('artists', sourceId, opt), - // (_, __, ___) => _filterOrderBy(opt), - // (_, __, ___) => _filterLimit(opt), - // ); - // } - - // MultiSelectable playlistsList(int sourceId, ListQuery opt) { - // return filterPlaylists( - // (_) => _filterPredicate('playlists', sourceId, opt), - // (_) => _filterOrderBy(opt), - // (_) => _filterLimit(opt), - // ); - // } - - // MultiSelectable playlistsListDownloaded( - // int sourceId, - // ListQuery opt, - // ) { - // return filterPlaylistsDownloaded( - // (_, __, ___) => _filterPredicate('playlists', sourceId, opt), - // (_, __, ___) => _filterOrderBy(opt), - // (_, __, ___) => _filterLimit(opt), - // ); - // } - - // MultiSelectable songsList(int sourceId, ListQuery opt) { - // return filterSongs( - // (_) => _filterPredicate('songs', sourceId, opt), - // (_) => _filterOrderBy(opt), - // (_) => _filterLimit(opt), - // ); - // } - - // MultiSelectable songsListDownloaded(int sourceId, ListQuery opt) { - // return filterSongsDownloaded( - // (_) => _filterPredicate('songs', sourceId, opt), - // (_) => _filterOrderBy(opt), - // (_) => _filterLimit(opt), - // ); - // } - - // Expression _filterPredicate(String table, int sourceId, ListQuery opt) { - // return opt.filters - // .map((filter) => buildFilter(filter)) - // .fold( - // CustomExpression('$table.source_id = $sourceId'), - // (previousValue, element) => previousValue & element, - // ); - // } - - // OrderBy _filterOrderBy(ListQuery opt) { - // return opt.sort != null - // ? OrderBy([_buildOrder(opt.sort!)]) - // : const OrderBy.nothing(); - // } - - // Limit _filterLimit(ListQuery opt) { - // return Limit(opt.page.limit, opt.page.offset); - // } - - // MultiSelectable albumSongsList(SourceId sid, ListQuery opt) { - // return listQuery( - // select(songs)..where( - // (tbl) => tbl.sourceId.equals(sid.sourceId) & tbl.albumId.equals(sid.id), - // ), - // opt, - // ); - // } - - // MultiSelectable songsByAlbumList(int sourceId, ListQuery opt) { - // return filterSongsByGenre( - // (_, __) => _filterPredicate('songs', sourceId, opt), - // (_, __) => _filterOrderBy(opt), - // (_, __) => _filterLimit(opt), - // ); - // } - - // MultiSelectable playlistSongsList(SourceId sid, ListQuery opt) { - // return listQueryJoined( - // select(songs).join([ - // innerJoin( - // playlistSongs, - // playlistSongs.sourceId.equalsExp(songs.sourceId) & - // playlistSongs.songId.equalsExp(songs.id), - // useColumns: false, - // ), - // ])..where( - // playlistSongs.sourceId.equals(sid.sourceId) & - // playlistSongs.playlistId.equals(sid.id), - // ), - // opt, - // ).map((row) => row.readTable(songs)); - // } - - // Future saveArtists(Iterable artists) async { - // await batch((batch) { - // batch.insertAllOnConflictUpdate(this.artists, artists); - // }); - // } - - // Future deleteArtistsNotIn(int sourceId, Set ids) { - // return transaction(() async { - // final allIds = - // (await (selectOnly(artists) - // ..addColumns([artists.id]) - // ..where(artists.sourceId.equals(sourceId))) - // .map((row) => row.read(artists.id)) - // .get()) - // .whereNotNull() - // .toSet(); - // final downloadIds = (await artistIdsWithDownloadStatus( - // sourceId, - // ).get()).whereNotNull().toSet(); - - // final diff = allIds.difference(downloadIds).difference(ids); - // for (var slice in diff.slices(kSqliteMaxVariableNumber)) { - // await (delete(artists)..where( - // (tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice), - // )) - // .go(); - // } - // }); - // } - - // Future saveAlbums(Iterable albums) async { - // await batch((batch) { - // batch.insertAllOnConflictUpdate(this.albums, albums); - // }); - // } - - // Future deleteAlbumsNotIn(int sourceId, Set ids) { - // return transaction(() async { - // final allIds = - // (await (selectOnly(albums) - // ..addColumns([albums.id]) - // ..where(albums.sourceId.equals(sourceId))) - // .map((row) => row.read(albums.id)) - // .get()) - // .whereNotNull() - // .toSet(); - // final downloadIds = (await albumIdsWithDownloadStatus( - // sourceId, - // ).get()).whereNotNull().toSet(); - - // final diff = allIds.difference(downloadIds).difference(ids); - // for (var slice in diff.slices(kSqliteMaxVariableNumber)) { - // await (delete(albums)..where( - // (tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice), - // )) - // .go(); - // } - // }); - // } - - // Future savePlaylists( - // Iterable playlistsWithSongs, - // ) async { - // final playlists = playlistsWithSongs.map((e) => e.playist); - // final playlistSongs = playlistsWithSongs.expand((e) => e.songs); - // final sourceId = playlists.first.sourceId.value; - - // await (delete(this.playlistSongs)..where( - // (tbl) => - // tbl.sourceId.equals(sourceId) & - // tbl.playlistId.isIn(playlists.map((e) => e.id.value)), - // )) - // .go(); - - // await batch((batch) { - // batch.insertAllOnConflictUpdate(this.playlists, playlists); - // batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs); - // }); - // } - - // Future deletePlaylistsNotIn(int sourceId, Set ids) { - // return transaction(() async { - // final allIds = - // (await (selectOnly(playlists) - // ..addColumns([playlists.id]) - // ..where(playlists.sourceId.equals(sourceId))) - // .map((row) => row.read(playlists.id)) - // .get()) - // .whereNotNull() - // .toSet(); - // final downloadIds = (await playlistIdsWithDownloadStatus( - // sourceId, - // ).get()).whereNotNull().toSet(); - - // final diff = allIds.difference(downloadIds).difference(ids); - // for (var slice in diff.slices(kSqliteMaxVariableNumber)) { - // await (delete(playlists)..where( - // (tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice), - // )) - // .go(); - // await (delete(playlistSongs)..where( - // (tbl) => - // tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(slice), - // )) - // .go(); - // } - // }); - // } - - // Future savePlaylistSongs( - // int sourceId, - // List ids, - // Iterable playlistSongs, - // ) async { - // await (delete(this.playlistSongs)..where( - // (tbl) => tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(ids), - // )) - // .go(); - // await batch((batch) { - // batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs); - // }); - // } - - // Future saveSongs(Iterable songs) async { - // await batch((batch) { - // batch.insertAllOnConflictUpdate(this.songs, songs); - // }); - // } - - // Future deleteSongsNotIn(int sourceId, Set ids) { - // return transaction(() async { - // final allIds = - // (await (selectOnly(songs) - // ..addColumns([songs.id]) - // ..where( - // songs.sourceId.equals(sourceId) & - // songs.downloadFilePath.isNull() & - // songs.downloadTaskId.isNull(), - // )) - // .map((row) => row.read(songs.id)) - // .get()) - // .whereNotNull() - // .toSet(); - - // final diff = allIds.difference(ids); - // for (var slice in diff.slices(kSqliteMaxVariableNumber)) { - // await (delete(songs)..where( - // (tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice), - // )) - // .go(); - // await (delete(playlistSongs)..where( - // (tbl) => tbl.sourceId.equals(sourceId) & tbl.songId.isIn(slice), - // )) - // .go(); - // } - // }); - // } - - // Selectable getLastBottomNavState() { - // return select(lastBottomNavState)..where((tbl) => tbl.id.equals(1)); - // } - - // Future saveLastBottomNavState(LastBottomNavStateData update) { - // return into(lastBottomNavState).insertOnConflictUpdate(update); - // } - - // Selectable getLastLibraryState() { - // return select(lastLibraryState)..where((tbl) => tbl.id.equals(1)); - // } - - // Future saveLastLibraryState(LastLibraryStateData update) { - // return into(lastLibraryState).insertOnConflictUpdate(update); - // } - - // Selectable getLastAudioState() { - // return select(lastAudioState)..where((tbl) => tbl.id.equals(1)); - // } - - // Future saveLastAudioState(LastAudioStateCompanion update) { - // return into(lastAudioState).insertOnConflictUpdate(update); - // } - - // Future insertQueue(Iterable songs) async { - // await batch((batch) { - // batch.insertAll(queue, songs); - // }); - // } - - // Future clearQueue() async { - // await delete(queue).go(); - // } - - // Future setCurrentTrack(int index) async { - // await transaction(() async { - // await (update(queue)..where((tbl) => tbl.index.equals(index).not())) - // .write(const QueueCompanion(currentTrack: Value(null))); - // await (update(queue)..where((tbl) => tbl.index.equals(index))).write( - // const QueueCompanion(currentTrack: Value(true)), - // ); - // }); - // } - - // Future createSource( - // SourcesCompanion source, - // SubsonicSourcesCompanion subsonic, - // ) async { - // await transaction(() async { - // final count = await sourcesCount().getSingle(); - // if (count == 0) { - // source = source.copyWith(isActive: const Value(true)); - // } - - // final id = await into(sources).insert(source); - // subsonic = subsonic.copyWith(sourceId: Value(id)); - // await into(subsonicSources).insert(subsonic); - // }); - // } - - // Future updateSource(SubsonicSettings source) async { - // await transaction(() async { - // await into(sources).insertOnConflictUpdate(source.toSourceInsertable()); - // await into( - // subsonicSources, - // ).insertOnConflictUpdate(source.toSubsonicInsertable()); - // }); - // } - - // Future deleteSource(int sourceId) async { - // await transaction(() async { - // await (delete( - // subsonicSources, - // )..where((tbl) => tbl.sourceId.equals(sourceId))).go(); - // await (delete(sources)..where((tbl) => tbl.id.equals(sourceId))).go(); - - // await (delete(songs)..where((tbl) => tbl.sourceId.equals(sourceId))).go(); - // await (delete( - // albums, - // )..where((tbl) => tbl.sourceId.equals(sourceId))).go(); - // await (delete( - // artists, - // )..where((tbl) => tbl.sourceId.equals(sourceId))).go(); - // await (delete( - // playlistSongs, - // )..where((tbl) => tbl.sourceId.equals(sourceId))).go(); - // await (delete( - // playlists, - // )..where((tbl) => tbl.sourceId.equals(sourceId))).go(); - // }); - // } - - // Future setActiveSource(int id) async { - // await batch((batch) { - // batch.update( - // sources, - // const SourcesCompanion(isActive: Value(null)), - // where: (t) => t.id.isNotValue(id), - // ); - // batch.update( - // sources, - // const SourcesCompanion(isActive: Value(true)), - // where: (t) => t.id.equals(id), - // ); - // }); - // } - - // Future updateSettings(AppSettingsCompanion settings) async { - // await into(appSettings).insertOnConflictUpdate(settings); - // } } extension ArtistToDb on models.Artist { @@ -495,252 +114,3 @@ extension PlaylistSongToDb on models.PlaylistSong { position: position, ); } - -// LazyDatabase _openConnection() { -// return LazyDatabase(() async { -// final dbFolder = await getApplicationDocumentsDirectory(); -// final file = File(p.join(dbFolder.path, 'subtracks.sqlite')); -// // return NativeDatabase.createInBackground(file, logStatements: true); - -// return ErrorLoggingDatabase( -// NativeDatabase.createInBackground(file), -// (e, s) => log.severe('SQL error', e, s), -// ); -// }); -// } - -// @Riverpod(keepAlive: true) -// SubtracksDatabase database(DatabaseRef ref) { -// return SubtracksDatabase(); -// } - -// OrderingTerm _buildOrder(SortBy sort) { -// OrderingMode? mode = sort.dir == SortDirection.asc -// ? OrderingMode.asc -// : OrderingMode.desc; -// return OrderingTerm( -// expression: CustomExpression(sort.column), -// mode: mode, -// ); -// } - -// SimpleSelectStatement listQuery( -// SimpleSelectStatement query, -// ListQuery opt, -// ) { -// if (opt.page.limit > 0) { -// query.limit(opt.page.limit, offset: opt.page.offset); -// } - -// if (opt.sort != null) { -// OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc -// ? OrderingMode.asc -// : OrderingMode.desc; -// query.orderBy([ -// (t) => OrderingTerm( -// expression: CustomExpression(opt.sort!.column), -// mode: mode, -// ), -// ]); -// } - -// for (var filter in opt.filters) { -// query.where((tbl) => buildFilter(filter)); -// } - -// return query; -// } - -// JoinedSelectStatement listQueryJoined( -// JoinedSelectStatement query, -// ListQuery opt, -// ) { -// if (opt.page.limit > 0) { -// query.limit(opt.page.limit, offset: opt.page.offset); -// } - -// if (opt.sort != null) { -// OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc -// ? OrderingMode.asc -// : OrderingMode.desc; -// query.orderBy([ -// OrderingTerm( -// expression: CustomExpression(opt.sort!.column), -// mode: mode, -// ), -// ]); -// } - -// for (var filter in opt.filters) { -// query.where(buildFilter(filter)); -// } - -// return query; -// } - -// CustomExpression buildFilter( -// FilterWith filter, -// ) { -// return filter.when( -// equals: (column, value, invert) => CustomExpression( -// '$column ${invert ? '<>' : '='} \'$value\'', -// ), -// greaterThan: (column, value, orEquals) => CustomExpression( -// '$column ${orEquals ? '>=' : '>'} $value', -// ), -// isNull: (column, invert) => CustomExpression( -// '$column ${invert ? 'IS NOT' : 'IS'} NULL', -// ), -// betweenInt: (column, from, to) => CustomExpression( -// '$column BETWEEN $from AND $to', -// ), -// isIn: (column, invert, values) => CustomExpression( -// '$column ${invert ? 'NOT IN' : 'IN'} (${values.join(',')})', -// ), -// ); -// } - -// class AlbumSongsCompanion { -// final AlbumsCompanion album; -// final Iterable songs; - -// AlbumSongsCompanion(this.album, this.songs); -// } - -// class ArtistAlbumsCompanion { -// final ArtistsCompanion artist; -// final Iterable albums; - -// ArtistAlbumsCompanion(this.artist, this.albums); -// } - -// class PlaylistWithSongsCompanion { -// final PlaylistsCompanion playist; -// final Iterable songs; - -// PlaylistWithSongsCompanion(this.playist, this.songs); -// } - -// Future saveArtist( -// SubtracksDatabase db, -// ArtistAlbumsCompanion artistAlbums, -// ) async { -// return db.background((db) async { -// final artist = artistAlbums.artist; -// final albums = artistAlbums.albums; - -// await db.batch((batch) { -// batch.insertAllOnConflictUpdate(db.artists, [artist]); -// batch.insertAllOnConflictUpdate(db.albums, albums); - -// // remove this artistId from albums not found in source -// // don't delete them since they coud have been moved to another artist -// // that we haven't synced yet -// final albumIds = {for (var a in albums) a.id.value}; -// batch.update( -// db.albums, -// const AlbumsCompanion(artistId: Value(null)), -// where: (tbl) => -// tbl.sourceId.equals(artist.sourceId.value) & -// tbl.artistId.equals(artist.id.value) & -// tbl.id.isNotIn(albumIds), -// ); -// }); -// }); -// } - -// Future saveAlbum( -// SubtracksDatabase db, -// AlbumSongsCompanion albumSongs, -// ) async { -// return db.background((db) async { -// final album = albumSongs.album.copyWith(synced: Value(DateTime.now())); -// final songs = albumSongs.songs; - -// final songIds = {for (var a in songs) a.id.value}; -// final hardDeletedSongIds = (await (db.selectOnly(db.songs) -// ..addColumns([db.songs.id]) -// ..where( -// db.songs.sourceId.equals(album.sourceId.value) & -// db.songs.albumId.equals(album.id.value) & -// db.songs.id.isNotIn(songIds) & -// db.songs.downloadFilePath.isNull() & -// db.songs.downloadTaskId.isNull(), -// )) -// .map((row) => row.read(db.songs.id)) -// .get()) -// .whereNotNull(); - -// await db.batch((batch) { -// batch.insertAllOnConflictUpdate(db.albums, [album]); -// batch.insertAllOnConflictUpdate(db.songs, songs); - -// // soft delete songs that have been downloaded so that the user -// // can decide to keep or remove them later -// // TODO: add a setting to skip soft delete and just remove download too -// batch.update( -// db.songs, -// const SongsCompanion(isDeleted: Value(true)), -// where: (tbl) => -// tbl.sourceId.equals(album.sourceId.value) & -// tbl.albumId.equals(album.id.value) & -// tbl.id.isNotIn(songIds) & -// (tbl.downloadFilePath.isNotNull() | tbl.downloadTaskId.isNotNull()), -// ); - -// // safe to hard delete songs that have not been downloaded -// batch.deleteWhere( -// db.songs, -// (tbl) => -// tbl.sourceId.equals(album.sourceId.value) & -// tbl.id.isIn(hardDeletedSongIds), -// ); - -// // also need to remove these songs from any playlists that contain them -// batch.deleteWhere( -// db.playlistSongs, -// (tbl) => -// tbl.sourceId.equals(album.sourceId.value) & -// tbl.songId.isIn(hardDeletedSongIds), -// ); -// }); -// }); -// } - -// Future savePlaylist( -// SubtracksDatabase db, -// PlaylistWithSongsCompanion playlistWithSongs, -// ) async { -// return db.background((db) async { -// final playlist = -// playlistWithSongs.playist.copyWith(synced: Value(DateTime.now())); -// final songs = playlistWithSongs.songs; - -// await db.batch((batch) { -// batch.insertAllOnConflictUpdate(db.playlists, [playlist]); -// batch.insertAllOnConflictUpdate(db.songs, songs); - -// batch.insertAllOnConflictUpdate( -// db.playlistSongs, -// songs.mapIndexed( -// (index, song) => PlaylistSongsCompanion.insert( -// sourceId: playlist.sourceId.value, -// playlistId: playlist.id.value, -// songId: song.id.value, -// position: index, -// ), -// ), -// ); - -// // the new playlist could be shorter than the old one, so we delete -// // playlist songs above our new playlist's length -// batch.deleteWhere( -// db.playlistSongs, -// (tbl) => -// tbl.sourceId.equals(playlist.sourceId.value) & -// tbl.playlistId.equals(playlist.id.value) & -// tbl.position.isBiggerOrEqualValue(songs.length), -// ); -// }); -// }); -// } diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index ab1a11c..f24724b 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -2454,6 +2454,8 @@ abstract class _$SubtracksDatabase extends GeneratedDatabase { 'songs_source_id_artist_id_idx', 'CREATE INDEX songs_source_id_artist_id_idx ON songs (source_id, artist_id)', ); + late final SourcesDao sourcesDao = SourcesDao(this as SubtracksDatabase); + late final LibraryDao libraryDao = LibraryDao(this as SubtracksDatabase); @override Iterable> get allTables => allSchemaEntities.whereType>(); diff --git a/lib/images/images.dart b/lib/images/images.dart index d050d11..b063273 100644 --- a/lib/images/images.dart +++ b/lib/images/images.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; -import '../app/state/settings.dart'; import '../app/state/source.dart'; class CoverArtImage extends HookConsumerWidget {