active source switching and reactivity

This commit is contained in:
austinried
2025-11-22 11:33:40 +09:00
parent de9bc98044
commit 914ec77ce0
11 changed files with 271 additions and 773 deletions

View File

@@ -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<int, Album>(
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) {

View File

@@ -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<int, _ArtistItem>(
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) {

View File

@@ -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<Widget> children;
const _Section({required this.children});
final List<Widget> 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<int>(
// 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<int>(
groupValue: activeSourceId,
onChanged: (value) {
if (value != null) {
db.sourcesDao.setActiveSource(value);
}
},
child: Column(
children: [
for (final (source, settings) in sources)
RadioListTile<int>(
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());
},
),
],
),
],
);
}
}

View File

@@ -11,8 +11,8 @@ final databaseInitializer = FutureProvider<SubtracksDatabase>((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<SubtracksDatabase>((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),
),
);

View File

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