display albums from db

This commit is contained in:
austinried 2025-11-08 18:27:37 +09:00
parent 0c80dbdba5
commit 933775b92f
17 changed files with 305 additions and 127 deletions

View File

@ -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<int, String>(
Widget build(BuildContext context, WidgetRef ref) {
final db = ref.watch(databaseProvider);
final sourceId = ref.watch(sourceIdProvider);
final controller = usePagingController<int, Album>(
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<String>(
builderDelegate: PagedChildBuilderDelegate<Album>(
itemBuilder: (context, item, index) => AlbumGridTile(
onTap: () {
context.push('/album');
album: item,
onTap: () async {
context.push('/album/${item.id}');
},
),
),

View File

@ -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,

View File

@ -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',

View File

@ -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');

View File

@ -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,6 +128,9 @@ class _LibraryScreenState extends State<LibraryScreen>
)
.toList(),
),
Row(
children: [
SyncButton(),
IconButton(
onPressed: () {
context.push('/settings');
@ -136,6 +141,8 @@ class _LibraryScreenState extends State<LibraryScreen>
),
],
),
],
),
),
),
),
@ -209,3 +216,19 @@ class _NewWidgetState extends State<NewWidget>
);
}
}
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();
},
);
}
}

View File

@ -0,0 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../database/database.dart';
final databaseProvider = Provider<SubtracksDatabase>((ref) {
return SubtracksDatabase();
});

View File

@ -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<SyncService>((ref) {
final db = ref.watch(databaseProvider);
final source = ref.watch(sourceProvider);
final sourceId = ref.watch(sourceIdProvider);
return SyncService(source: source, db: db, sourceId: sourceId);
});

View File

@ -0,0 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final sourceIdProvider = Provider<int>((ref) {
return 1;
});

39
lib/app/state/source.dart Normal file
View File

@ -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<MusicSource>((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<MusicSource>((ref) {
final source = ref.watch(_sourceProvider);
return source.requireValue;
});

View File

@ -300,12 +300,12 @@ class SourcesCompanion extends UpdateCompanion<Source> {
}
}
class SubsonicSourceOptions extends Table
with TableInfo<SubsonicSourceOptions, SubsonicSourceOption> {
class SubsonicSettings extends Table
with TableInfo<SubsonicSettings, SubsonicSetting> {
@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<Uri>(SubsonicSourceOptions.$converteraddress);
).withConverter<Uri>(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<SubsonicSourceOption> instance, {
Insertable<SubsonicSetting> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
@ -417,14 +417,14 @@ class SubsonicSourceOptions extends Table
@override
Set<GeneratedColumn> get $primaryKey => {sourceId};
@override
SubsonicSourceOption map(Map<String, dynamic> data, {String? tablePrefix}) {
SubsonicSetting map(Map<String, dynamic> 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<Uri, String> $converteraddress = const UriConverter();
@ -459,14 +459,13 @@ class SubsonicSourceOptions extends Table
bool get dontWriteConstraints => true;
}
class SubsonicSourceOption extends DataClass
implements Insertable<SubsonicSourceOption> {
class SubsonicSetting extends DataClass implements Insertable<SubsonicSetting> {
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<int>(sourceId);
{
map['address'] = Variable<String>(
SubsonicSourceOptions.$converteraddress.toSql(address),
SubsonicSettings.$converteraddress.toSql(address),
);
}
map['username'] = Variable<String>(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<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SubsonicSourceOption(
return SubsonicSetting(
sourceId: serializer.fromJson<int>(json['source_id']),
address: serializer.fromJson<Uri>(json['address']),
username: serializer.fromJson<String>(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<SubsonicSourceOption> {
class SubsonicSettingsCompanion extends UpdateCompanion<SubsonicSetting> {
final Value<int> sourceId;
final Value<Uri> address;
final Value<String> username;
final Value<String> password;
final Value<bool> 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<SubsonicSourceOption> custom({
static Insertable<SubsonicSetting> custom({
Expression<int>? sourceId,
Expression<String>? address,
Expression<String>? username,
@ -613,14 +611,14 @@ class SubsonicSourceOptionsCompanion
});
}
SubsonicSourceOptionsCompanion copyWith({
SubsonicSettingsCompanion copyWith({
Value<int>? sourceId,
Value<Uri>? address,
Value<String>? username,
Value<String>? password,
Value<bool>? 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<String>(
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<DatabaseSchemaEntity> 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<int> sourceId,
required Uri address,
required String username,
required String password,
Value<bool> useTokenAuth,
});
typedef $SubsonicSourceOptionsUpdateCompanionBuilder =
SubsonicSourceOptionsCompanion Function({
typedef $SubsonicSettingsUpdateCompanionBuilder =
SubsonicSettingsCompanion Function({
Value<int> sourceId,
Value<Uri> address,
Value<String> username,
@ -2677,9 +2674,9 @@ typedef $SubsonicSourceOptionsUpdateCompanionBuilder =
Value<bool> 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<int> sourceId = const Value.absent(),
@ -2817,7 +2812,7 @@ class $SubsonicSourceOptionsTableManager
Value<String> username = const Value.absent(),
Value<String> password = const Value.absent(),
Value<bool> 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<bool> 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 =>

View File

@ -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,

View File

@ -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(
return ProviderScope(
child: MaterialApp.router(
themeMode: ThemeMode.dark,
darkTheme: ThemeData.dark(useMaterial3: true),
debugShowCheckedModeBanner: false,
routerConfig: router,
),
);
}
}

View File

@ -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});
}

View File

@ -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();
}

6
lib/util/http.dart Normal file
View File

@ -0,0 +1,6 @@
import 'package:http/http.dart';
class SubtracksHttpClient extends BaseClient {
@override
Future<StreamedResponse> send(BaseRequest request) => request.send();
}

View File

@ -625,7 +625,7 @@ packages:
source: hosted
version: "2.0.2"
octo_image:
dependency: transitive
dependency: "direct main"
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"

View File

@ -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