diff --git a/lib/app/lists/albums_grid.dart b/lib/app/lists/albums_grid.dart index 33b9d62..b7abe73 100644 --- a/lib/app/lists/albums_grid.dart +++ b/lib/app/lists/albums_grid.dart @@ -1,20 +1,35 @@ +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'; +import '../../sources/models.dart'; import '../hooks/use_paging_controller.dart'; +import '../state/database.dart'; +import '../state/settings.dart'; import 'list_items.dart'; -class AlbumsGrid extends HookWidget { +const kPageSize = 30; + +class AlbumsGrid extends HookConsumerWidget { const AlbumsGrid({super.key}); @override - Widget build(BuildContext context) { - final controller = usePagingController( + Widget build(BuildContext context, WidgetRef ref) { + final db = ref.watch(databaseProvider); + final sourceId = ref.watch(sourceIdProvider); + + final controller = usePagingController( getNextPageKey: (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey, - fetchPage: (pageKey) => List.generate(30, (_) => pageKey.toString()), + fetchPage: (pageKey) async { + final query = db.albums.select() + ..where((f) => f.sourceId.equals(sourceId)) + ..limit(kPageSize, offset: (pageKey - 1) * kPageSize); + + return await query.get(); + }, ); return PagingListener( @@ -26,10 +41,11 @@ class AlbumsGrid extends HookWidget { gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, ), - builderDelegate: PagedChildBuilderDelegate( + builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) => AlbumGridTile( - onTap: () { - context.push('/album'); + album: item, + onTap: () async { + context.push('/album/${item.id}'); }, ), ), diff --git a/lib/app/lists/list_items.dart b/lib/app/lists/list_items.dart index 4c7ac54..d2f90c5 100644 --- a/lib/app/lists/list_items.dart +++ b/lib/app/lists/list_items.dart @@ -1,18 +1,26 @@ 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 'package:material_symbols_icons/symbols.dart'; +import 'package:octo_image/octo_image.dart'; +import '../../sources/models.dart'; +import '../state/source.dart'; import '../util/clip.dart'; -class AlbumGridTile extends StatelessWidget { +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) { + Widget build(BuildContext context, WidgetRef ref) { return CardTheme( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( @@ -21,16 +29,50 @@ class AlbumGridTile extends StatelessWidget { margin: EdgeInsets.all(2), child: ImageCard( onTap: onTap, - child: CachedNetworkImage( - imageUrl: 'https://placehold.net/400x400.png', - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => Icon(Icons.error), - ), + child: CoverArtImage(coverArt: album.coverArt), ), ); } } +class CoverArtImage extends HookConsumerWidget { + const CoverArtImage({ + super.key, + this.coverArt, + this.thumbnail = false, + }); + + final String? coverArt; + final bool thumbnail; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final source = ref.watch(sourceProvider); + + CachedNetworkImageProvider buildImageProvider() => + CachedNetworkImageProvider( + coverArt != null + ? source.coverArtUri(coverArt!, thumbnail: thumbnail).toString() + : 'https://placehold.net/400x400.png', + ); + + final imageProvider = useState(buildImageProvider()); + useEffect(() { + imageProvider.value = buildImageProvider(); + return; + }, [source, coverArt, thumbnail]); + + return OctoImage( + image: imageProvider.value, + placeholderBuilder: (context) => Icon(Symbols.album_rounded), + errorBuilder: (context, error, trace) => Icon(Icons.error), + fit: BoxFit.cover, + fadeOutDuration: Duration(milliseconds: 100), + fadeInDuration: Duration(milliseconds: 200), + ); + } +} + class ArtistListTile extends StatelessWidget { const ArtistListTile({super.key}); @@ -71,7 +113,7 @@ class ImageCard extends StatelessWidget { child, Positioned.fill( child: Material( - color: Colors.transparent, + type: MaterialType.transparency, child: InkWell( onTap: onTap, onLongPress: onLongPress, diff --git a/lib/app/router.dart b/lib/app/router.dart index f202789..5b4af5b 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -17,8 +17,9 @@ final router = GoRouter( builder: (context, state) => LibraryScreen(), routes: [ GoRoute( - path: 'album', - builder: (context, state) => AlbumScreen(), + path: 'album/:id', + builder: (context, state) => + AlbumScreen(id: state.pathParameters['id']!), ), GoRoute( path: 'artist', diff --git a/lib/app/screens/album_screen.dart b/lib/app/screens/album_screen.dart index bfdcc55..cdb8a5b 100644 --- a/lib/app/screens/album_screen.dart +++ b/lib/app/screens/album_screen.dart @@ -3,7 +3,12 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class AlbumScreen extends StatelessWidget { - const AlbumScreen({super.key}); + const AlbumScreen({ + super.key, + required this.id, + }); + + final String id; @override Widget build(BuildContext context) { @@ -12,7 +17,7 @@ class AlbumScreen extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Album!'), + Text('Album $id!'), TextButton( onPressed: () { context.push('/artist'); diff --git a/lib/app/screens/library_screen.dart b/lib/app/screens/library_screen.dart index f8e6395..173c627 100644 --- a/lib/app/screens/library_screen.dart +++ b/lib/app/screens/library_screen.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../lists/albums_grid.dart'; +import '../state/services.dart'; import '../util/custom_scroll_fix.dart'; class LibraryScreen extends StatefulWidget { @@ -126,13 +128,18 @@ class _LibraryScreenState extends State ) .toList(), ), - IconButton( - onPressed: () { - context.push('/settings'); - }, - icon: Icon( - Symbols.settings_rounded, - ), + Row( + children: [ + SyncButton(), + IconButton( + onPressed: () { + context.push('/settings'); + }, + icon: Icon( + Symbols.settings_rounded, + ), + ), + ], ), ], ), @@ -209,3 +216,19 @@ class _NewWidgetState extends State ); } } + +class SyncButton extends HookConsumerWidget { + const SyncButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final syncService = ref.watch(syncServiceProvider); + + return IconButton( + icon: Icon(Symbols.sync_rounded), + onPressed: () { + syncService.sync(); + }, + ); + } +} diff --git a/lib/app/state/database.dart b/lib/app/state/database.dart new file mode 100644 index 0000000..818f0b0 --- /dev/null +++ b/lib/app/state/database.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../database/database.dart'; + +final databaseProvider = Provider((ref) { + return SubtracksDatabase(); +}); diff --git a/lib/app/state/services.dart b/lib/app/state/services.dart new file mode 100644 index 0000000..8009608 --- /dev/null +++ b/lib/app/state/services.dart @@ -0,0 +1,14 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../services/sync_services.dart'; +import 'database.dart'; +import 'settings.dart'; +import 'source.dart'; + +final syncServiceProvider = Provider((ref) { + final db = ref.watch(databaseProvider); + final source = ref.watch(sourceProvider); + final sourceId = ref.watch(sourceIdProvider); + + return SyncService(source: source, db: db, sourceId: sourceId); +}); diff --git a/lib/app/state/settings.dart b/lib/app/state/settings.dart new file mode 100644 index 0000000..18e73e3 --- /dev/null +++ b/lib/app/state/settings.dart @@ -0,0 +1,5 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final sourceIdProvider = Provider((ref) { + return 1; +}); diff --git a/lib/app/state/source.dart b/lib/app/state/source.dart new file mode 100644 index 0000000..aa26da6 --- /dev/null +++ b/lib/app/state/source.dart @@ -0,0 +1,39 @@ +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../sources/music_source.dart'; +import '../../sources/subsonic/client.dart'; +import '../../sources/subsonic/source.dart'; +import '../../util/http.dart'; +import 'database.dart'; +import 'settings.dart'; + +final _sourceProvider = FutureProvider((ref) async { + final db = ref.watch(databaseProvider); + final sourceId = ref.watch(sourceIdProvider); + + final query = db.sources.select().join([ + leftOuterJoin( + db.subsonicSettings, + db.subsonicSettings.sourceId.equalsExp(db.sources.id), + ), + ])..where(db.sources.id.equals(sourceId)); + + final result = await query.getSingle(); + final subsonicSettings = result.readTable(db.subsonicSettings); + + return SubsonicSource( + SubsonicClient( + http: SubtracksHttpClient(), + address: subsonicSettings.address, + username: subsonicSettings.username, + password: subsonicSettings.password, + useTokenAuth: subsonicSettings.useTokenAuth, + ), + ); +}); + +final sourceProvider = Provider((ref) { + final source = ref.watch(_sourceProvider); + return source.requireValue; +}); diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index 6eec989..0f18018 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -300,12 +300,12 @@ class SourcesCompanion extends UpdateCompanion { } } -class SubsonicSourceOptions extends Table - with TableInfo { +class SubsonicSettings extends Table + with TableInfo { @override final GeneratedDatabase attachedDatabase; final String? _alias; - SubsonicSourceOptions(this.attachedDatabase, [this._alias]); + SubsonicSettings(this.attachedDatabase, [this._alias]); static const VerificationMeta _sourceIdMeta = const VerificationMeta( 'sourceId', ); @@ -325,7 +325,7 @@ class SubsonicSourceOptions extends Table type: DriftSqlType.string, requiredDuringInsert: true, $customConstraints: 'NOT NULL', - ).withConverter(SubsonicSourceOptions.$converteraddress); + ).withConverter(SubsonicSettings.$converteraddress); static const VerificationMeta _usernameMeta = const VerificationMeta( 'username', ); @@ -372,10 +372,10 @@ class SubsonicSourceOptions extends Table String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'subsonic_source_options'; + static const String $name = 'subsonic_settings'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); @@ -417,14 +417,14 @@ class SubsonicSourceOptions extends Table @override Set get $primaryKey => {sourceId}; @override - SubsonicSourceOption map(Map data, {String? tablePrefix}) { + SubsonicSetting map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return SubsonicSourceOption( + return SubsonicSetting( sourceId: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}source_id'], )!, - address: SubsonicSourceOptions.$converteraddress.fromSql( + address: SubsonicSettings.$converteraddress.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}address'], @@ -446,8 +446,8 @@ class SubsonicSourceOptions extends Table } @override - SubsonicSourceOptions createAlias(String alias) { - return SubsonicSourceOptions(attachedDatabase, alias); + SubsonicSettings createAlias(String alias) { + return SubsonicSettings(attachedDatabase, alias); } static TypeConverter $converteraddress = const UriConverter(); @@ -459,14 +459,13 @@ class SubsonicSourceOptions extends Table bool get dontWriteConstraints => true; } -class SubsonicSourceOption extends DataClass - implements Insertable { +class SubsonicSetting extends DataClass implements Insertable { final int sourceId; final Uri address; final String username; final String password; final bool useTokenAuth; - const SubsonicSourceOption({ + const SubsonicSetting({ required this.sourceId, required this.address, required this.username, @@ -479,7 +478,7 @@ class SubsonicSourceOption extends DataClass map['source_id'] = Variable(sourceId); { map['address'] = Variable( - SubsonicSourceOptions.$converteraddress.toSql(address), + SubsonicSettings.$converteraddress.toSql(address), ); } map['username'] = Variable(username); @@ -488,8 +487,8 @@ class SubsonicSourceOption extends DataClass return map; } - SubsonicSourceOptionsCompanion toCompanion(bool nullToAbsent) { - return SubsonicSourceOptionsCompanion( + SubsonicSettingsCompanion toCompanion(bool nullToAbsent) { + return SubsonicSettingsCompanion( sourceId: Value(sourceId), address: Value(address), username: Value(username), @@ -498,12 +497,12 @@ class SubsonicSourceOption extends DataClass ); } - factory SubsonicSourceOption.fromJson( + factory SubsonicSetting.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return SubsonicSourceOption( + return SubsonicSetting( sourceId: serializer.fromJson(json['source_id']), address: serializer.fromJson(json['address']), username: serializer.fromJson(json['username']), @@ -523,21 +522,21 @@ class SubsonicSourceOption extends DataClass }; } - SubsonicSourceOption copyWith({ + SubsonicSetting copyWith({ int? sourceId, Uri? address, String? username, String? password, bool? useTokenAuth, - }) => SubsonicSourceOption( + }) => SubsonicSetting( sourceId: sourceId ?? this.sourceId, address: address ?? this.address, username: username ?? this.username, password: password ?? this.password, useTokenAuth: useTokenAuth ?? this.useTokenAuth, ); - SubsonicSourceOption copyWithCompanion(SubsonicSourceOptionsCompanion data) { - return SubsonicSourceOption( + SubsonicSetting copyWithCompanion(SubsonicSettingsCompanion data) { + return SubsonicSetting( sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, address: data.address.present ? data.address.value : this.address, username: data.username.present ? data.username.value : this.username, @@ -550,7 +549,7 @@ class SubsonicSourceOption extends DataClass @override String toString() { - return (StringBuffer('SubsonicSourceOption(') + return (StringBuffer('SubsonicSetting(') ..write('sourceId: $sourceId, ') ..write('address: $address, ') ..write('username: $username, ') @@ -566,7 +565,7 @@ class SubsonicSourceOption extends DataClass @override bool operator ==(Object other) => identical(this, other) || - (other is SubsonicSourceOption && + (other is SubsonicSetting && other.sourceId == this.sourceId && other.address == this.address && other.username == this.username && @@ -574,21 +573,20 @@ class SubsonicSourceOption extends DataClass other.useTokenAuth == this.useTokenAuth); } -class SubsonicSourceOptionsCompanion - extends UpdateCompanion { +class SubsonicSettingsCompanion extends UpdateCompanion { final Value sourceId; final Value address; final Value username; final Value password; final Value useTokenAuth; - const SubsonicSourceOptionsCompanion({ + const SubsonicSettingsCompanion({ this.sourceId = const Value.absent(), this.address = const Value.absent(), this.username = const Value.absent(), this.password = const Value.absent(), this.useTokenAuth = const Value.absent(), }); - SubsonicSourceOptionsCompanion.insert({ + SubsonicSettingsCompanion.insert({ this.sourceId = const Value.absent(), required Uri address, required String username, @@ -597,7 +595,7 @@ class SubsonicSourceOptionsCompanion }) : address = Value(address), username = Value(username), password = Value(password); - static Insertable custom({ + static Insertable custom({ Expression? sourceId, Expression? address, Expression? username, @@ -613,14 +611,14 @@ class SubsonicSourceOptionsCompanion }); } - SubsonicSourceOptionsCompanion copyWith({ + SubsonicSettingsCompanion copyWith({ Value? sourceId, Value? address, Value? username, Value? password, Value? useTokenAuth, }) { - return SubsonicSourceOptionsCompanion( + return SubsonicSettingsCompanion( sourceId: sourceId ?? this.sourceId, address: address ?? this.address, username: username ?? this.username, @@ -637,7 +635,7 @@ class SubsonicSourceOptionsCompanion } if (address.present) { map['address'] = Variable( - SubsonicSourceOptions.$converteraddress.toSql(address.value), + SubsonicSettings.$converteraddress.toSql(address.value), ); } if (username.present) { @@ -654,7 +652,7 @@ class SubsonicSourceOptionsCompanion @override String toString() { - return (StringBuffer('SubsonicSourceOptionsCompanion(') + return (StringBuffer('SubsonicSettingsCompanion(') ..write('sourceId: $sourceId, ') ..write('address: $address, ') ..write('username: $username, ') @@ -2386,8 +2384,7 @@ abstract class _$SubtracksDatabase extends GeneratedDatabase { _$SubtracksDatabase(QueryExecutor e) : super(e); $SubtracksDatabaseManager get managers => $SubtracksDatabaseManager(this); late final Sources sources = Sources(this); - late final SubsonicSourceOptions subsonicSourceOptions = - SubsonicSourceOptions(this); + late final SubsonicSettings subsonicSettings = SubsonicSettings(this); late final Artists artists = Artists(this); late final Index artistsSourceId = Index( 'artists_source_id', @@ -2431,7 +2428,7 @@ abstract class _$SubtracksDatabase extends GeneratedDatabase { @override List get allSchemaEntities => [ sources, - subsonicSourceOptions, + subsonicSettings, artists, artistsSourceId, albums, @@ -2453,7 +2450,7 @@ abstract class _$SubtracksDatabase extends GeneratedDatabase { 'sources', limitUpdateKind: UpdateKind.delete, ), - result: [TableUpdate('subsonic_source_options', kind: UpdateKind.delete)], + result: [TableUpdate('subsonic_settings', kind: UpdateKind.delete)], ), WritePropagation( on: TableUpdateQuery.onTableName( @@ -2660,16 +2657,16 @@ typedef $SourcesProcessedTableManager = Source, PrefetchHooks Function() >; -typedef $SubsonicSourceOptionsCreateCompanionBuilder = - SubsonicSourceOptionsCompanion Function({ +typedef $SubsonicSettingsCreateCompanionBuilder = + SubsonicSettingsCompanion Function({ Value sourceId, required Uri address, required String username, required String password, Value useTokenAuth, }); -typedef $SubsonicSourceOptionsUpdateCompanionBuilder = - SubsonicSourceOptionsCompanion Function({ +typedef $SubsonicSettingsUpdateCompanionBuilder = + SubsonicSettingsCompanion Function({ Value sourceId, Value address, Value username, @@ -2677,9 +2674,9 @@ typedef $SubsonicSourceOptionsUpdateCompanionBuilder = Value useTokenAuth, }); -class $SubsonicSourceOptionsFilterComposer - extends Composer<_$SubtracksDatabase, SubsonicSourceOptions> { - $SubsonicSourceOptionsFilterComposer({ +class $SubsonicSettingsFilterComposer + extends Composer<_$SubtracksDatabase, SubsonicSettings> { + $SubsonicSettingsFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -2713,9 +2710,9 @@ class $SubsonicSourceOptionsFilterComposer ); } -class $SubsonicSourceOptionsOrderingComposer - extends Composer<_$SubtracksDatabase, SubsonicSourceOptions> { - $SubsonicSourceOptionsOrderingComposer({ +class $SubsonicSettingsOrderingComposer + extends Composer<_$SubtracksDatabase, SubsonicSettings> { + $SubsonicSettingsOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -2748,9 +2745,9 @@ class $SubsonicSourceOptionsOrderingComposer ); } -class $SubsonicSourceOptionsAnnotationComposer - extends Composer<_$SubtracksDatabase, SubsonicSourceOptions> { - $SubsonicSourceOptionsAnnotationComposer({ +class $SubsonicSettingsAnnotationComposer + extends Composer<_$SubtracksDatabase, SubsonicSettings> { + $SubsonicSettingsAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -2775,41 +2772,39 @@ class $SubsonicSourceOptionsAnnotationComposer ); } -class $SubsonicSourceOptionsTableManager +class $SubsonicSettingsTableManager extends RootTableManager< _$SubtracksDatabase, - SubsonicSourceOptions, - SubsonicSourceOption, - $SubsonicSourceOptionsFilterComposer, - $SubsonicSourceOptionsOrderingComposer, - $SubsonicSourceOptionsAnnotationComposer, - $SubsonicSourceOptionsCreateCompanionBuilder, - $SubsonicSourceOptionsUpdateCompanionBuilder, + SubsonicSettings, + SubsonicSetting, + $SubsonicSettingsFilterComposer, + $SubsonicSettingsOrderingComposer, + $SubsonicSettingsAnnotationComposer, + $SubsonicSettingsCreateCompanionBuilder, + $SubsonicSettingsUpdateCompanionBuilder, ( - SubsonicSourceOption, + SubsonicSetting, BaseReferences< _$SubtracksDatabase, - SubsonicSourceOptions, - SubsonicSourceOption + SubsonicSettings, + SubsonicSetting >, ), - SubsonicSourceOption, + SubsonicSetting, PrefetchHooks Function() > { - $SubsonicSourceOptionsTableManager( - _$SubtracksDatabase db, - SubsonicSourceOptions table, - ) : super( + $SubsonicSettingsTableManager(_$SubtracksDatabase db, SubsonicSettings table) + : super( TableManagerState( db: db, table: table, createFilteringComposer: () => - $SubsonicSourceOptionsFilterComposer($db: db, $table: table), + $SubsonicSettingsFilterComposer($db: db, $table: table), createOrderingComposer: () => - $SubsonicSourceOptionsOrderingComposer($db: db, $table: table), + $SubsonicSettingsOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $SubsonicSourceOptionsAnnotationComposer($db: db, $table: table), + $SubsonicSettingsAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value sourceId = const Value.absent(), @@ -2817,7 +2812,7 @@ class $SubsonicSourceOptionsTableManager Value username = const Value.absent(), Value password = const Value.absent(), Value useTokenAuth = const Value.absent(), - }) => SubsonicSourceOptionsCompanion( + }) => SubsonicSettingsCompanion( sourceId: sourceId, address: address, username: username, @@ -2831,7 +2826,7 @@ class $SubsonicSourceOptionsTableManager required String username, required String password, Value useTokenAuth = const Value.absent(), - }) => SubsonicSourceOptionsCompanion.insert( + }) => SubsonicSettingsCompanion.insert( sourceId: sourceId, address: address, username: username, @@ -2846,25 +2841,21 @@ class $SubsonicSourceOptionsTableManager ); } -typedef $SubsonicSourceOptionsProcessedTableManager = +typedef $SubsonicSettingsProcessedTableManager = ProcessedTableManager< _$SubtracksDatabase, - SubsonicSourceOptions, - SubsonicSourceOption, - $SubsonicSourceOptionsFilterComposer, - $SubsonicSourceOptionsOrderingComposer, - $SubsonicSourceOptionsAnnotationComposer, - $SubsonicSourceOptionsCreateCompanionBuilder, - $SubsonicSourceOptionsUpdateCompanionBuilder, + SubsonicSettings, + SubsonicSetting, + $SubsonicSettingsFilterComposer, + $SubsonicSettingsOrderingComposer, + $SubsonicSettingsAnnotationComposer, + $SubsonicSettingsCreateCompanionBuilder, + $SubsonicSettingsUpdateCompanionBuilder, ( - SubsonicSourceOption, - BaseReferences< - _$SubtracksDatabase, - SubsonicSourceOptions, - SubsonicSourceOption - >, + SubsonicSetting, + BaseReferences<_$SubtracksDatabase, SubsonicSettings, SubsonicSetting>, ), - SubsonicSourceOption, + SubsonicSetting, PrefetchHooks Function() >; typedef $ArtistsCreateCompanionBuilder = @@ -4137,8 +4128,8 @@ class $SubtracksDatabaseManager { final _$SubtracksDatabase _db; $SubtracksDatabaseManager(this._db); $SourcesTableManager get sources => $SourcesTableManager(_db, _db.sources); - $SubsonicSourceOptionsTableManager get subsonicSourceOptions => - $SubsonicSourceOptionsTableManager(_db, _db.subsonicSourceOptions); + $SubsonicSettingsTableManager get subsonicSettings => + $SubsonicSettingsTableManager(_db, _db.subsonicSettings); $ArtistsTableManager get artists => $ArtistsTableManager(_db, _db.artists); $AlbumsTableManager get albums => $AlbumsTableManager(_db, _db.albums); $PlaylistsTableManager get playlists => diff --git a/lib/database/tables.drift b/lib/database/tables.drift index 6e28b85..a191a65 100644 --- a/lib/database/tables.drift +++ b/lib/database/tables.drift @@ -51,7 +51,7 @@ CREATE TABLE sources( created_at DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)) ); -CREATE TABLE subsonic_source_options( +CREATE TABLE subsonic_settings( source_id INT NOT NULL PRIMARY KEY, address TEXT NOT NULL MAPPED BY `const UriConverter()`, username TEXT NOT NULL, diff --git a/lib/main.dart b/lib/main.dart index 8795b72..66bbb05 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,34 @@ +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'app/router.dart'; +import 'database/database.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + final db = SubtracksDatabase(); + await db + .into(db.sources) + .insertOnConflictUpdate( + SourcesCompanion.insert( + id: Value(1), + name: 'test navidrome', + ), + ); + await db + .into(db.subsonicSettings) + .insertOnConflictUpdate( + SubsonicSettingsCompanion.insert( + sourceId: Value(1), + address: Uri.parse('http://10.0.2.2:4533'), + username: 'admin', + password: 'password', + useTokenAuth: Value(true), + ), + ); -void main() { runApp(const MainApp()); } @@ -11,11 +37,13 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp.router( - themeMode: ThemeMode.dark, - darkTheme: ThemeData.dark(useMaterial3: true), - debugShowCheckedModeBanner: false, - routerConfig: router, + return ProviderScope( + child: MaterialApp.router( + themeMode: ThemeMode.dark, + darkTheme: ThemeData.dark(useMaterial3: true), + debugShowCheckedModeBanner: false, + routerConfig: router, + ), ); } } diff --git a/lib/sources/music_source.dart b/lib/sources/music_source.dart index 3c4e63c..9a84bd6 100644 --- a/lib/sources/music_source.dart +++ b/lib/sources/music_source.dart @@ -12,5 +12,5 @@ abstract class MusicSource { Uri streamUri(String songId); Uri downloadUri(String songId); - Uri coverArtUri(String coverArtId, {bool thumbnail = true}); + Uri coverArtUri(String coverArt, {bool thumbnail = false}); } diff --git a/lib/sources/subsonic/source.dart b/lib/sources/subsonic/source.dart index 0d57246..8a8ef50 100644 --- a/lib/sources/subsonic/source.dart +++ b/lib/sources/subsonic/source.dart @@ -151,8 +151,8 @@ class SubsonicSource implements MusicSource { } @override - Uri coverArtUri(String id, {bool thumbnail = true}) { - final opts = {'id': id}; + Uri coverArtUri(String coverArt, {bool thumbnail = false}) { + final opts = {'id': coverArt}; if (thumbnail) { opts['size'] = 256.toString(); } diff --git a/lib/util/http.dart b/lib/util/http.dart new file mode 100644 index 0000000..c469fd0 --- /dev/null +++ b/lib/util/http.dart @@ -0,0 +1,6 @@ +import 'package:http/http.dart'; + +class SubtracksHttpClient extends BaseClient { + @override + Future send(BaseRequest request) => request.send(); +} diff --git a/pubspec.lock b/pubspec.lock index d9d16a6..2969d5e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -625,7 +625,7 @@ packages: source: hosted version: "2.0.2" octo_image: - dependency: transitive + dependency: "direct main" description: name: octo_image sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" diff --git a/pubspec.yaml b/pubspec.yaml index fc64b1d..078c8f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: infinite_scroll_pagination: ^5.1.1 json_annotation: ^4.9.0 material_symbols_icons: ^4.2874.0 + octo_image: ^2.1.0 path: ^1.9.1 path_provider: ^2.1.5 pool: ^1.5.2