mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 15:02:42 +01:00
Compare commits
9 Commits
3fcb938f2b
...
flutter-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad6d534286 | ||
|
|
2837d4576e | ||
|
|
7f6ba4776a | ||
|
|
f7874bcead | ||
|
|
ba169092fd | ||
|
|
4183e2d3b9 | ||
|
|
c3bb14edbf | ||
|
|
805e6fff7a | ||
|
|
d245fc7fef |
@@ -8,6 +8,7 @@ import 'screens/playlist_screen.dart';
|
|||||||
import 'screens/preload_screen.dart';
|
import 'screens/preload_screen.dart';
|
||||||
import 'screens/root_shell_screen.dart';
|
import 'screens/root_shell_screen.dart';
|
||||||
import 'screens/settings_screen.dart';
|
import 'screens/settings_screen.dart';
|
||||||
|
import 'screens/settings_source_screen.dart';
|
||||||
|
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
initialLocation: '/preload',
|
initialLocation: '/preload',
|
||||||
@@ -29,8 +30,9 @@ final router = GoRouter(
|
|||||||
AlbumScreen(id: state.pathParameters['id']!),
|
AlbumScreen(id: state.pathParameters['id']!),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'artists',
|
path: 'artists/:id',
|
||||||
builder: (context, state) => ArtistScreen(),
|
builder: (context, state) =>
|
||||||
|
ArtistScreen(id: state.pathParameters['id']!),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'playlists/:id',
|
path: 'playlists/:id',
|
||||||
@@ -49,5 +51,12 @@ final router = GoRouter(
|
|||||||
path: '/settings',
|
path: '/settings',
|
||||||
builder: (context, state) => SettingsScreen(),
|
builder: (context, state) => SettingsScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/sources/:id',
|
||||||
|
builder: (context, state) {
|
||||||
|
final id = state.pathParameters['id'];
|
||||||
|
return SettingsSourceScreen(id: id == 'add' ? null : int.parse(id!));
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,124 @@
|
|||||||
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class ArtistScreen extends StatelessWidget {
|
import '../../database/query.dart';
|
||||||
const ArtistScreen({super.key});
|
import '../../sources/models.dart';
|
||||||
|
import '../state/database.dart';
|
||||||
|
import '../state/source.dart';
|
||||||
|
import '../ui/cover_art_theme.dart';
|
||||||
|
import '../ui/gradient.dart';
|
||||||
|
import '../ui/images.dart';
|
||||||
|
import '../ui/lists/albums_list.dart';
|
||||||
|
|
||||||
|
class ArtistScreen extends HookConsumerWidget {
|
||||||
|
const ArtistScreen({
|
||||||
|
super.key,
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
final db = ref.watch(databaseProvider);
|
||||||
body: Center(child: Text('Artist!')),
|
final sourceId = ref.watch(sourceIdProvider);
|
||||||
|
|
||||||
|
final getArtist = useMemoized(
|
||||||
|
() => db.libraryDao.getArtist(sourceId, id).getSingle(),
|
||||||
|
);
|
||||||
|
final artist = useFuture(getArtist).data;
|
||||||
|
|
||||||
|
if (artist == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = AlbumsQuery(
|
||||||
|
sourceId: sourceId,
|
||||||
|
filter: IList([AlbumsFilter.artistId(artist.id)]),
|
||||||
|
sort: IList([
|
||||||
|
SortingTerm.albumsDesc(AlbumsColumn.year),
|
||||||
|
SortingTerm.albumsAsc(AlbumsColumn.name),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return CoverArtTheme(
|
||||||
|
coverArt: artist.coverArt,
|
||||||
|
child: Scaffold(
|
||||||
|
body: GradientScrollView(
|
||||||
|
slivers: [
|
||||||
|
ArtistHeader(artist: artist),
|
||||||
|
AlbumsList(query: query),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistHeader extends StatelessWidget {
|
||||||
|
const ArtistHeader({
|
||||||
|
super.key,
|
||||||
|
required this.artist,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Artist artist;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = TextTheme.of(context);
|
||||||
|
final colorScheme = ColorScheme.of(context);
|
||||||
|
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.passthrough,
|
||||||
|
alignment: AlignmentGeometry.bottomCenter,
|
||||||
|
children: [
|
||||||
|
CoverArtImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
coverArt: artist.coverArt,
|
||||||
|
thumbnail: false,
|
||||||
|
height: 350,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: AlignmentGeometry.centerRight,
|
||||||
|
end: AlignmentGeometry.centerLeft,
|
||||||
|
colors: [
|
||||||
|
colorScheme.surface.withAlpha(220),
|
||||||
|
colorScheme.surface.withAlpha(150),
|
||||||
|
colorScheme.surface.withAlpha(20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsetsGeometry.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
artist.name,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: textTheme.headlineLarge?.copyWith(
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
blurRadius: 20,
|
||||||
|
color: colorScheme.surface,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
import '../../database/query.dart';
|
|
||||||
import '../../l10n/generated/app_localizations.dart';
|
import '../../l10n/generated/app_localizations.dart';
|
||||||
|
import '../state/lists.dart';
|
||||||
import '../state/services.dart';
|
import '../state/services.dart';
|
||||||
import '../state/source.dart';
|
|
||||||
import '../ui/lists/albums_grid.dart';
|
import '../ui/lists/albums_grid.dart';
|
||||||
import '../ui/lists/artists_list.dart';
|
import '../ui/lists/artists_list.dart';
|
||||||
import '../ui/lists/items.dart';
|
import '../ui/lists/items.dart';
|
||||||
import '../ui/lists/playlists_list.dart';
|
import '../ui/lists/playlists_list.dart';
|
||||||
import '../ui/lists/songs_list.dart';
|
import '../ui/lists/songs_list.dart';
|
||||||
|
import '../ui/menus.dart';
|
||||||
import '../util/custom_scroll_fix.dart';
|
import '../util/custom_scroll_fix.dart';
|
||||||
|
|
||||||
const kIconSize = 26.0;
|
const kIconSize = 26.0;
|
||||||
const kTabHeight = 36.0;
|
const kTabHeight = 36.0;
|
||||||
|
|
||||||
enum LibraryTab {
|
enum LibraryTab {
|
||||||
home(Icon(Symbols.home_rounded)),
|
// home(Icon(Symbols.home_rounded)),
|
||||||
albums(Icon(Symbols.album_rounded)),
|
albums(Icon(Symbols.album_rounded)),
|
||||||
artists(Icon(Symbols.person_rounded)),
|
artists(Icon(Symbols.person_rounded)),
|
||||||
songs(Icon(Symbols.music_note_rounded)),
|
songs(Icon(Symbols.music_note_rounded)),
|
||||||
@@ -41,7 +40,7 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final tabController = useTabController(
|
final tabController = useTabController(
|
||||||
initialLength: LibraryTab.values.length,
|
initialLength: LibraryTab.values.length,
|
||||||
initialIndex: 1,
|
initialIndex: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -75,18 +74,7 @@ class LibraryTabBarView extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final sourceId = ref.watch(sourceIdProvider);
|
final songsQuery = ref.watch(songsQueryProvider);
|
||||||
|
|
||||||
final songsQuery = SongsQuery(
|
|
||||||
sourceId: sourceId,
|
|
||||||
sort: IList([
|
|
||||||
SortingTerm.songsAsc(SongsColumn.albumArtist),
|
|
||||||
SortingTerm.songsAsc(SongsColumn.album),
|
|
||||||
SortingTerm.songsAsc(SongsColumn.disc),
|
|
||||||
SortingTerm.songsAsc(SongsColumn.track),
|
|
||||||
SortingTerm.songsAsc(SongsColumn.title),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return TabBarView(
|
return TabBarView(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
@@ -107,7 +95,14 @@ class LibraryTabBarView extends HookConsumerWidget {
|
|||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => ArtistsList(),
|
// _ => SliverToBoxAdapter(child: Container()),
|
||||||
|
},
|
||||||
|
menuBuilder: switch (tab) {
|
||||||
|
LibraryTab.albums => (_) => AlbumsGridFilters(),
|
||||||
|
// LibraryTab.artists => (_) => AlbumsGridFilters(),
|
||||||
|
// LibraryTab.playlists => (_) => AlbumsGridFilters(),
|
||||||
|
// LibraryTab.songs => (_) => AlbumsGridFilters(),
|
||||||
|
_ => null,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -209,7 +204,7 @@ class TabTitleText extends HookConsumerWidget {
|
|||||||
|
|
||||||
String tabLocalization(LibraryTab tab) => switch (tab) {
|
String tabLocalization(LibraryTab tab) => switch (tab) {
|
||||||
LibraryTab.albums => l.navigationTabsAlbums,
|
LibraryTab.albums => l.navigationTabsAlbums,
|
||||||
LibraryTab.home => l.navigationTabsHome,
|
// LibraryTab.home => l.navigationTabsHome,
|
||||||
LibraryTab.artists => l.navigationTabsArtists,
|
LibraryTab.artists => l.navigationTabsArtists,
|
||||||
LibraryTab.songs => l.navigationTabsSongs,
|
LibraryTab.songs => l.navigationTabsSongs,
|
||||||
LibraryTab.playlists => l.navigationTabsPlaylists,
|
LibraryTab.playlists => l.navigationTabsPlaylists,
|
||||||
@@ -233,10 +228,12 @@ class TabScrollView extends HookConsumerWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.sliver,
|
required this.sliver,
|
||||||
|
this.menuBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int index;
|
final int index;
|
||||||
final Widget sliver;
|
final Widget sliver;
|
||||||
|
final WidgetBuilder? menuBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -244,7 +241,16 @@ class TabScrollView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final scrollProvider = CustomScrollProviderData.of(context);
|
final scrollProvider = CustomScrollProviderData.of(context);
|
||||||
|
|
||||||
return CustomScrollView(
|
final listBuilder = menuBuilder;
|
||||||
|
final floatingActionButton = listBuilder != null
|
||||||
|
? FabFilter(
|
||||||
|
listBuilder: listBuilder,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
floatingActionButton: floatingActionButton,
|
||||||
|
body: CustomScrollView(
|
||||||
controller: scrollProvider.scrollControllers[index],
|
controller: scrollProvider.scrollControllers[index],
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SliverOverlapInjector(
|
SliverOverlapInjector(
|
||||||
@@ -252,6 +258,7 @@ class TabScrollView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
sliver,
|
sliver,
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../l10n/generated/app_localizations.dart';
|
import '../../l10n/generated/app_localizations.dart';
|
||||||
@@ -14,15 +15,14 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final text = TextTheme.of(context);
|
final textTheme = TextTheme.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l.navigationTabsSettings, style: text.headlineLarge),
|
title: Text(l.navigationTabsSettings, style: textTheme.headlineLarge),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
// const SizedBox(height: 96),
|
|
||||||
_SectionHeader(l.settingsServersName),
|
_SectionHeader(l.settingsServersName),
|
||||||
const _Sources(),
|
const _Sources(),
|
||||||
// _SectionHeader(l.settingsNetworkName),
|
// _SectionHeader(l.settingsNetworkName),
|
||||||
@@ -36,7 +36,9 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _Section extends StatelessWidget {
|
class _Section extends StatelessWidget {
|
||||||
const _Section({required this.children});
|
const _Section({
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
final List<Widget> children;
|
final List<Widget> children;
|
||||||
|
|
||||||
@@ -46,14 +48,15 @@ class _Section extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
...children,
|
...children,
|
||||||
const SizedBox(height: 32),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SectionHeader extends StatelessWidget {
|
class _SectionHeader extends StatelessWidget {
|
||||||
const _SectionHeader(this.title);
|
const _SectionHeader(
|
||||||
|
this.title,
|
||||||
|
);
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@@ -61,17 +64,14 @@ class _SectionHeader extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final text = TextTheme.of(context);
|
final text = TextTheme.of(context);
|
||||||
|
|
||||||
return Column(
|
return Padding(
|
||||||
children: [
|
padding: EdgeInsetsGeometry.directional(
|
||||||
const SizedBox(height: 16),
|
start: kHorizontalPadding,
|
||||||
SizedBox(
|
end: kHorizontalPadding,
|
||||||
width: double.infinity,
|
top: 32,
|
||||||
child: Padding(
|
bottom: 8,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding),
|
),
|
||||||
child: Text(title, style: text.headlineMedium),
|
child: Text(title, style: text.headlineMedium),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,12 +376,12 @@ class _Sources extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
for (final (source, settings) in sources)
|
for (final (:source, :subsonicSetting) in sources)
|
||||||
RadioListTile<int>(
|
RadioListTile<int>(
|
||||||
value: source.id,
|
value: source.id,
|
||||||
title: Text(source.name),
|
title: Text(source.name),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
settings.address.toString(),
|
subsonicSetting.address.toString(),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
@@ -389,7 +389,7 @@ class _Sources extends HookConsumerWidget {
|
|||||||
secondary: IconButton(
|
secondary: IconButton(
|
||||||
icon: const Icon(Icons.edit_rounded),
|
icon: const Icon(Icons.edit_rounded),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// context.pushRoute(SourceRoute(id: source.id));
|
context.push('/sources/${source.id}');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -404,7 +404,7 @@ class _Sources extends HookConsumerWidget {
|
|||||||
icon: const Icon(Icons.add_rounded),
|
icon: const Icon(Icons.add_rounded),
|
||||||
label: Text(l.settingsServersActionsAdd),
|
label: Text(l.settingsServersActionsAdd),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// context.pushRoute(SourceRoute());
|
context.push('/sources/add');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
309
lib/app/screens/settings_source_screen.dart
Normal file
309
lib/app/screens/settings_source_screen.dart
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../database/dao/sources_dao.dart';
|
||||||
|
import '../../database/database.dart';
|
||||||
|
import '../../l10n/generated/app_localizations.dart';
|
||||||
|
import '../../util/logger.dart';
|
||||||
|
import '../state/database.dart';
|
||||||
|
import '../ui/menus.dart';
|
||||||
|
|
||||||
|
class SettingsSourceScreen extends HookConsumerWidget {
|
||||||
|
const SettingsSourceScreen({
|
||||||
|
super.key,
|
||||||
|
this.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int? id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final db = ref.watch(databaseProvider);
|
||||||
|
|
||||||
|
final getSource = useMemoized(
|
||||||
|
() async => id != null
|
||||||
|
? (result: await db.sourcesDao.getSource(id!).getSingle())
|
||||||
|
: await Future.value((result: null)),
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
final sourceResult = useFuture(getSource).data;
|
||||||
|
|
||||||
|
if (sourceResult == null) {
|
||||||
|
return Scaffold();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _SettingsSourceScreen(source: sourceResult.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsSourceScreen extends HookConsumerWidget {
|
||||||
|
const _SettingsSourceScreen({
|
||||||
|
required this.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SourceSetting? source;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l = AppLocalizations.of(context);
|
||||||
|
final textTheme = TextTheme.of(context);
|
||||||
|
final colorScheme = ColorScheme.of(context);
|
||||||
|
|
||||||
|
final form = useState(GlobalKey<FormState>()).value;
|
||||||
|
final isSaving = useState(false);
|
||||||
|
final isDeleting = useState(false);
|
||||||
|
|
||||||
|
final nameController = useTextEditingController(text: source?.source.name);
|
||||||
|
final addressController = useTextEditingController(
|
||||||
|
text: source?.subsonicSetting.address.toString(),
|
||||||
|
);
|
||||||
|
final usernameController = useTextEditingController(
|
||||||
|
text: source?.subsonicSetting.username,
|
||||||
|
);
|
||||||
|
final passwordController = useTextEditingController(
|
||||||
|
text: source?.subsonicSetting.password,
|
||||||
|
);
|
||||||
|
final forcePlaintextPassword = useState(
|
||||||
|
!(source?.subsonicSetting.useTokenAuth ?? true),
|
||||||
|
);
|
||||||
|
|
||||||
|
final name = LabeledTextField(
|
||||||
|
label: l.settingsServersFieldsName,
|
||||||
|
controller: nameController,
|
||||||
|
required: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final address = LabeledTextField(
|
||||||
|
label: l.settingsServersFieldsAddress,
|
||||||
|
controller: addressController,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
autofillHints: const [AutofillHints.url],
|
||||||
|
required: true,
|
||||||
|
validator: (value, label) {
|
||||||
|
final uri = Uri.tryParse(value ?? '');
|
||||||
|
if (uri?.isAbsolute != true || uri?.host.isNotEmpty != true) {
|
||||||
|
return '$label must be a valid URL';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final username = LabeledTextField(
|
||||||
|
label: l.settingsServersFieldsUsername,
|
||||||
|
controller: usernameController,
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
required: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final password = LabeledTextField(
|
||||||
|
label: l.settingsServersFieldsPassword,
|
||||||
|
controller: passwordController,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
obscureText: true,
|
||||||
|
required: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final forcePlaintextSwitch = SwitchListTile(
|
||||||
|
value: forcePlaintextPassword.value,
|
||||||
|
title: Text(l.settingsServersOptionsForcePlaintextPasswordTitle),
|
||||||
|
subtitle: forcePlaintextPassword.value
|
||||||
|
? Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOn)
|
||||||
|
: Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOff),
|
||||||
|
onChanged: (value) => forcePlaintextPassword.value = value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PopScope(
|
||||||
|
canPop: !isSaving.value && !isDeleting.value,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
source == null
|
||||||
|
? l.settingsServersActionsAdd
|
||||||
|
: l.settingsServersActionsEdit,
|
||||||
|
style: textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (source != null && source?.source.isActive != true)
|
||||||
|
FloatingActionButton(
|
||||||
|
backgroundColor: colorScheme.tertiaryContainer,
|
||||||
|
foregroundColor: colorScheme.onTertiaryContainer,
|
||||||
|
onPressed: !isSaving.value && !isDeleting.value
|
||||||
|
? () async {
|
||||||
|
try {
|
||||||
|
isDeleting.value = true;
|
||||||
|
await ref
|
||||||
|
.read(databaseProvider)
|
||||||
|
.sourcesDao
|
||||||
|
.deleteSource(source!.source.id);
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: isDeleting.value
|
||||||
|
? SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: colorScheme.onTertiaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.delete_forever_rounded),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
heroTag: null,
|
||||||
|
icon: isSaving.value
|
||||||
|
? const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.save_rounded),
|
||||||
|
label: Text(l.settingsServersActionsSave),
|
||||||
|
onPressed: !isSaving.value && !isDeleting.value
|
||||||
|
? () async {
|
||||||
|
if (!form.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = false;
|
||||||
|
try {
|
||||||
|
isSaving.value = true;
|
||||||
|
if (source != null) {
|
||||||
|
await ref
|
||||||
|
.read(databaseProvider)
|
||||||
|
.sourcesDao
|
||||||
|
.updateSource(
|
||||||
|
source!.source.copyWith(
|
||||||
|
name: nameController.text,
|
||||||
|
),
|
||||||
|
source!.subsonicSetting.copyWith(
|
||||||
|
address: Uri.parse(addressController.text),
|
||||||
|
username: usernameController.text,
|
||||||
|
password: passwordController.text,
|
||||||
|
useTokenAuth: !forcePlaintextPassword.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await ref
|
||||||
|
.read(databaseProvider)
|
||||||
|
.sourcesDao
|
||||||
|
.createSource(
|
||||||
|
SourcesCompanion.insert(
|
||||||
|
name: nameController.text,
|
||||||
|
),
|
||||||
|
SubsonicSettingsCompanion.insert(
|
||||||
|
address: Uri.parse(addressController.text),
|
||||||
|
username: usernameController.text,
|
||||||
|
password: passwordController.text,
|
||||||
|
useTokenAuth: Value(
|
||||||
|
!forcePlaintextPassword.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, _) {
|
||||||
|
// showErrorSnackbar(context, e.toString());
|
||||||
|
// log.severe('Saving source', e, st);
|
||||||
|
logger.w('fuck');
|
||||||
|
error = true;
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error && context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Form(
|
||||||
|
key: form,
|
||||||
|
child: AutofillGroup(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
forcePlaintextSwitch,
|
||||||
|
const FabPadding(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LabeledTextField extends HookConsumerWidget {
|
||||||
|
const LabeledTextField({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.controller,
|
||||||
|
this.obscureText = false,
|
||||||
|
this.keyboardType,
|
||||||
|
this.validator,
|
||||||
|
this.autofillHints,
|
||||||
|
this.required = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final bool obscureText;
|
||||||
|
final bool required;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
final Iterable<String>? autofillHints;
|
||||||
|
final String? Function(String? value, String label)? validator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final textTheme = TextTheme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(label, style: textTheme.titleMedium),
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: obscureText,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
autofillHints: autofillHints,
|
||||||
|
validator: (value) {
|
||||||
|
if (required) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '$label is required';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validator != null) {
|
||||||
|
return validator!(value, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show InsertMode, Value;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../database/database.dart';
|
import '../../database/database.dart';
|
||||||
@@ -6,18 +6,24 @@ import '../../database/database.dart';
|
|||||||
final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
||||||
final db = SubtracksDatabase();
|
final db = SubtracksDatabase();
|
||||||
|
|
||||||
await db
|
await db.batch((batch) {
|
||||||
.into(db.sources)
|
batch.insertAll(
|
||||||
.insertOnConflictUpdate(
|
db.sources,
|
||||||
|
[
|
||||||
SourcesCompanion.insert(
|
SourcesCompanion.insert(
|
||||||
id: Value(1),
|
id: Value(1),
|
||||||
name: 'test subsonic',
|
name: 'test subsonic',
|
||||||
// isActive: Value(true),
|
isActive: Value(true),
|
||||||
),
|
),
|
||||||
|
SourcesCompanion.insert(
|
||||||
|
id: Value(2),
|
||||||
|
name: 'test navidrome',
|
||||||
|
isActive: Value(null),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
mode: InsertMode.insertOrIgnore,
|
||||||
);
|
);
|
||||||
await db
|
batch.insertAllOnConflictUpdate(db.subsonicSettings, [
|
||||||
.into(db.subsonicSettings)
|
|
||||||
.insertOnConflictUpdate(
|
|
||||||
SubsonicSettingsCompanion.insert(
|
SubsonicSettingsCompanion.insert(
|
||||||
sourceId: Value(1),
|
sourceId: Value(1),
|
||||||
address: Uri.parse('http://demo.subsonic.org'),
|
address: Uri.parse('http://demo.subsonic.org'),
|
||||||
@@ -25,19 +31,6 @@ final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
|||||||
password: 'guest',
|
password: 'guest',
|
||||||
useTokenAuth: Value(true),
|
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(
|
SubsonicSettingsCompanion.insert(
|
||||||
sourceId: Value(2),
|
sourceId: Value(2),
|
||||||
address: Uri.parse('http://10.0.2.2:4533'),
|
address: Uri.parse('http://10.0.2.2:4533'),
|
||||||
@@ -45,7 +38,8 @@ final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
|||||||
password: 'password',
|
password: 'password',
|
||||||
useTokenAuth: Value(true),
|
useTokenAuth: Value(true),
|
||||||
),
|
),
|
||||||
);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
});
|
});
|
||||||
|
|||||||
31
lib/app/state/lists.dart
Normal file
31
lib/app/state/lists.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../database/query.dart';
|
||||||
|
import 'source.dart';
|
||||||
|
|
||||||
|
final albumsQueryProvider = Provider<AlbumsQuery>((ref) {
|
||||||
|
final sourceId = ref.watch(sourceIdProvider);
|
||||||
|
|
||||||
|
return AlbumsQuery(
|
||||||
|
sourceId: sourceId,
|
||||||
|
sort: IList([
|
||||||
|
SortingTerm.albumsDesc(AlbumsColumn.created),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final songsQueryProvider = Provider<SongsQuery>((ref) {
|
||||||
|
final sourceId = ref.watch(sourceIdProvider);
|
||||||
|
|
||||||
|
return SongsQuery(
|
||||||
|
sourceId: sourceId,
|
||||||
|
sort: IList([
|
||||||
|
SortingTerm.songsAsc(SongsColumn.albumArtist),
|
||||||
|
SortingTerm.songsAsc(SongsColumn.album),
|
||||||
|
SortingTerm.songsAsc(SongsColumn.disc),
|
||||||
|
SortingTerm.songsAsc(SongsColumn.track),
|
||||||
|
SortingTerm.songsAsc(SongsColumn.title),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../util/logger.dart';
|
||||||
import '../state/source.dart';
|
import '../state/source.dart';
|
||||||
import '../util/color_scheme.dart';
|
import '../util/color_scheme.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
@@ -36,8 +37,12 @@ class CoverArtTheme extends HookConsumerWidget {
|
|||||||
: 'https://placehold.net/400x400.png',
|
: 'https://placehold.net/400x400.png',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error, stackTrace) {
|
||||||
print(err);
|
logger.w(
|
||||||
|
'Could not create color scheme from image provider',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ class CoverArtImage extends HookConsumerWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.coverArt,
|
this.coverArt,
|
||||||
this.thumbnail = true,
|
this.thumbnail = true,
|
||||||
this.fit,
|
this.fit = BoxFit.cover,
|
||||||
this.height,
|
this.height,
|
||||||
this.width,
|
this.width,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? coverArt;
|
final String? coverArt;
|
||||||
final bool thumbnail;
|
final bool thumbnail;
|
||||||
final BoxFit? fit;
|
final BoxFit fit;
|
||||||
final double? height;
|
final double? height;
|
||||||
final double? width;
|
final double? width;
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class CoverArtImage extends HookConsumerWidget {
|
|||||||
cacheKey: '$sourceId$coverArt$thumbnail',
|
cacheKey: '$sourceId$coverArt$thumbnail',
|
||||||
placeholder: (context, url) => Icon(Symbols.cached_rounded),
|
placeholder: (context, url) => Icon(Symbols.cached_rounded),
|
||||||
errorWidget: (context, url, error) => Icon(Icons.error),
|
errorWidget: (context, url, error) => Icon(Icons.error),
|
||||||
fit: BoxFit.cover,
|
fit: fit,
|
||||||
fadeOutDuration: Duration(milliseconds: 100),
|
fadeOutDuration: Duration(milliseconds: 100),
|
||||||
fadeInDuration: Duration(milliseconds: 200),
|
fadeInDuration: Duration(milliseconds: 200),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
import '../../../database/query.dart';
|
|
||||||
import '../../../sources/models.dart';
|
import '../../../sources/models.dart';
|
||||||
import '../../hooks/use_on_source.dart';
|
import '../../hooks/use_on_source.dart';
|
||||||
import '../../hooks/use_paging_controller.dart';
|
import '../../hooks/use_paging_controller.dart';
|
||||||
import '../../state/database.dart';
|
import '../../state/database.dart';
|
||||||
|
import '../../state/lists.dart';
|
||||||
import '../../state/source.dart';
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
import 'items.dart';
|
import 'items.dart';
|
||||||
|
|
||||||
const kPageSize = 60;
|
const kPageSize = 60;
|
||||||
@@ -20,23 +21,22 @@ class AlbumsGrid extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final db = ref.watch(databaseProvider);
|
final db = ref.watch(databaseProvider);
|
||||||
|
final query = ref.watch(albumsQueryProvider);
|
||||||
|
|
||||||
final controller = usePagingController<int, Album>(
|
final controller = usePagingController<int, Album>(
|
||||||
getNextPageKey: (state) =>
|
getNextPageKey: (state) =>
|
||||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||||
fetchPage: (pageKey) => db.libraryDao.listAlbums(
|
fetchPage: (pageKey) => db.libraryDao.listAlbums(
|
||||||
AlbumsQuery(
|
query.copyWith(
|
||||||
sourceId: ref.read(sourceIdProvider),
|
sourceId: ref.read(sourceIdProvider),
|
||||||
sort: IList([
|
|
||||||
SortingTerm.albumsDesc(AlbumsColumn.created),
|
|
||||||
]),
|
|
||||||
limit: kPageSize,
|
limit: kPageSize,
|
||||||
offset: (pageKey - 1) * kPageSize,
|
offset: (pageKey - 1) * kPageSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
useOnSourceChange(ref, (_) => controller.refresh());
|
|
||||||
useOnSourceSync(ref, controller.refresh);
|
useOnSourceSync(ref, controller.refresh);
|
||||||
|
useValueChanged(query, (_, _) => controller.refresh());
|
||||||
|
|
||||||
return PagingListener(
|
return PagingListener(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@@ -49,7 +49,9 @@ class AlbumsGrid extends HookConsumerWidget {
|
|||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 3,
|
||||||
),
|
),
|
||||||
|
showNoMoreItemsIndicatorAsGridChild: false,
|
||||||
builderDelegate: PagedChildBuilderDelegate<Album>(
|
builderDelegate: PagedChildBuilderDelegate<Album>(
|
||||||
|
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||||
itemBuilder: (context, item, index) => AlbumGridTile(
|
itemBuilder: (context, item, index) => AlbumGridTile(
|
||||||
album: item,
|
album: item,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -63,3 +65,14 @@ class AlbumsGrid extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AlbumsGridFilters extends HookConsumerWidget {
|
||||||
|
const AlbumsGridFilters({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return ListView(
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
92
lib/app/ui/lists/albums_list.dart
Normal file
92
lib/app/ui/lists/albums_list.dart
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
|
import '../../../database/query.dart';
|
||||||
|
import '../../../sources/models.dart';
|
||||||
|
import '../../hooks/use_on_source.dart';
|
||||||
|
import '../../hooks/use_paging_controller.dart';
|
||||||
|
import '../../state/database.dart';
|
||||||
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
|
import 'items.dart';
|
||||||
|
|
||||||
|
const kPageSize = 30;
|
||||||
|
|
||||||
|
class AlbumsList extends HookConsumerWidget {
|
||||||
|
const AlbumsList({
|
||||||
|
super.key,
|
||||||
|
required this.query,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AlbumsQuery query;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final db = ref.watch(databaseProvider);
|
||||||
|
final controller = usePagingController<int, Album>(
|
||||||
|
getNextPageKey: (state) =>
|
||||||
|
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||||
|
fetchPage: (pageKey) => db.libraryDao.listAlbums(
|
||||||
|
query.copyWith(
|
||||||
|
sourceId: ref.read(sourceIdProvider),
|
||||||
|
limit: kPageSize,
|
||||||
|
offset: (pageKey - 1) * kPageSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
useOnSourceChange(ref, (_) => controller.refresh());
|
||||||
|
useOnSourceSync(ref, controller.refresh);
|
||||||
|
|
||||||
|
return PagingListener(
|
||||||
|
controller: controller,
|
||||||
|
builder: (context, state, fetchNextPage) {
|
||||||
|
return PagedSliverList(
|
||||||
|
state: state,
|
||||||
|
fetchNextPage: fetchNextPage,
|
||||||
|
builderDelegate: PagedChildBuilderDelegate<Album>(
|
||||||
|
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||||
|
itemBuilder: (context, item, index) {
|
||||||
|
final tile = AlbumListTile(
|
||||||
|
album: item,
|
||||||
|
onTap: () {
|
||||||
|
context.push('/albums/${item.id}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final currentItemYear = item.year;
|
||||||
|
final previousItemYear = index == 0
|
||||||
|
? currentItemYear
|
||||||
|
: controller.items?.elementAtOrNull(index - 1)?.year;
|
||||||
|
|
||||||
|
if (index == 0 || currentItemYear != previousItemYear) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 24,
|
||||||
|
bottom: 8,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
item.year?.toString() ?? 'Unknown year',
|
||||||
|
style: TextTheme.of(context).headlineMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tile,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tile;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import '../../hooks/use_on_source.dart';
|
|||||||
import '../../hooks/use_paging_controller.dart';
|
import '../../hooks/use_paging_controller.dart';
|
||||||
import '../../state/database.dart';
|
import '../../state/database.dart';
|
||||||
import '../../state/source.dart';
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
import 'items.dart';
|
import 'items.dart';
|
||||||
|
|
||||||
const kPageSize = 30;
|
const kPageSize = 30;
|
||||||
@@ -45,6 +46,7 @@ class ArtistsList extends HookConsumerWidget {
|
|||||||
state: state,
|
state: state,
|
||||||
fetchNextPage: fetchNextPage,
|
fetchNextPage: fetchNextPage,
|
||||||
builderDelegate: PagedChildBuilderDelegate<AristListItem>(
|
builderDelegate: PagedChildBuilderDelegate<AristListItem>(
|
||||||
|
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
final (:artist, :albumCount) = item;
|
final (:artist, :albumCount) = item;
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class SongsListHeader extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Container(
|
Container(
|
||||||
|
height: 300,
|
||||||
|
width: 300,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -48,7 +50,6 @@ class SongsListHeader extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: CoverArtImage(
|
child: CoverArtImage(
|
||||||
height: 300,
|
|
||||||
thumbnail: false,
|
thumbnail: false,
|
||||||
coverArt: coverArt,
|
coverArt: coverArt,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
|||||||
@@ -62,6 +62,63 @@ class ArtistListTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AlbumListTile extends StatelessWidget {
|
||||||
|
const AlbumListTile({
|
||||||
|
super.key,
|
||||||
|
required this.album,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Album album;
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = TextTheme.of(context);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 18),
|
||||||
|
child: RoundedBoxClip(
|
||||||
|
child: CoverArtImage(
|
||||||
|
coverArt: album.coverArt,
|
||||||
|
thumbnail: true,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
album.name,
|
||||||
|
style: textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
album.albumArtist ?? 'Unknown album artist',
|
||||||
|
style: textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PlaylistListTile extends StatelessWidget {
|
class PlaylistListTile extends StatelessWidget {
|
||||||
const PlaylistListTile({
|
const PlaylistListTile({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -77,10 +134,12 @@ class PlaylistListTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CoverArtImage(
|
leading: RoundedBoxClip(
|
||||||
|
child: CoverArtImage(
|
||||||
coverArt: playlist.coverArt,
|
coverArt: playlist.coverArt,
|
||||||
thumbnail: true,
|
thumbnail: true,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
title: Text(playlist.name),
|
title: Text(playlist.name),
|
||||||
subtitle: Text(playlist.comment ?? ''),
|
subtitle: Text(playlist.comment ?? ''),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -106,9 +165,11 @@ class SongListTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: showLeading
|
leading: showLeading
|
||||||
? CoverArtImage(
|
? RoundedBoxClip(
|
||||||
|
child: CoverArtImage(
|
||||||
coverArt: coverArt,
|
coverArt: coverArt,
|
||||||
thumbnail: true,
|
thumbnail: true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
title: Text(song.title),
|
title: Text(song.title),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../hooks/use_on_source.dart';
|
|||||||
import '../../hooks/use_paging_controller.dart';
|
import '../../hooks/use_paging_controller.dart';
|
||||||
import '../../state/database.dart';
|
import '../../state/database.dart';
|
||||||
import '../../state/source.dart';
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
import 'items.dart';
|
import 'items.dart';
|
||||||
|
|
||||||
const kPageSize = 30;
|
const kPageSize = 30;
|
||||||
@@ -45,6 +46,7 @@ class PlaylistsList extends HookConsumerWidget {
|
|||||||
state: state,
|
state: state,
|
||||||
fetchNextPage: fetchNextPage,
|
fetchNextPage: fetchNextPage,
|
||||||
builderDelegate: PagedChildBuilderDelegate<Playlist>(
|
builderDelegate: PagedChildBuilderDelegate<Playlist>(
|
||||||
|
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
return PlaylistListTile(
|
return PlaylistListTile(
|
||||||
playlist: item,
|
playlist: item,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
@@ -7,6 +8,8 @@ import '../../../database/query.dart';
|
|||||||
import '../../hooks/use_on_source.dart';
|
import '../../hooks/use_on_source.dart';
|
||||||
import '../../hooks/use_paging_controller.dart';
|
import '../../hooks/use_paging_controller.dart';
|
||||||
import '../../state/database.dart';
|
import '../../state/database.dart';
|
||||||
|
import '../../state/source.dart';
|
||||||
|
import '../menus.dart';
|
||||||
|
|
||||||
const kPageSize = 30;
|
const kPageSize = 30;
|
||||||
|
|
||||||
@@ -29,14 +32,15 @@ class SongsList extends HookConsumerWidget {
|
|||||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||||
fetchPage: (pageKey) => db.libraryDao.listSongs(
|
fetchPage: (pageKey) => db.libraryDao.listSongs(
|
||||||
query.copyWith(
|
query.copyWith(
|
||||||
|
sourceId: ref.read(sourceIdProvider),
|
||||||
limit: kPageSize,
|
limit: kPageSize,
|
||||||
offset: (pageKey - 1) * kPageSize,
|
offset: (pageKey - 1) * kPageSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
useOnSourceChange(ref, (_) => controller.refresh());
|
|
||||||
useOnSourceSync(ref, controller.refresh);
|
useOnSourceSync(ref, controller.refresh);
|
||||||
|
useValueChanged(query, (_, _) => controller.refresh());
|
||||||
|
|
||||||
return PagingListener(
|
return PagingListener(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@@ -45,6 +49,7 @@ class SongsList extends HookConsumerWidget {
|
|||||||
state: state,
|
state: state,
|
||||||
fetchNextPage: fetchNextPage,
|
fetchNextPage: fetchNextPage,
|
||||||
builderDelegate: PagedChildBuilderDelegate<SongListItem>(
|
builderDelegate: PagedChildBuilderDelegate<SongListItem>(
|
||||||
|
noMoreItemsIndicatorBuilder: (context) => FabPadding(),
|
||||||
itemBuilder: itemBuilder,
|
itemBuilder: itemBuilder,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
76
lib/app/ui/menus.dart
Normal file
76
lib/app/ui/menus.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
Future<void> showContextMenu({
|
||||||
|
required BuildContext context,
|
||||||
|
required WidgetBuilder listBuilder,
|
||||||
|
}) => showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
snap: true,
|
||||||
|
initialChildSize: 0.3,
|
||||||
|
minChildSize: 0.3,
|
||||||
|
maxChildSize: 0.4,
|
||||||
|
builder: (context, scrollController) => PrimaryScrollController(
|
||||||
|
controller: scrollController,
|
||||||
|
child: listBuilder(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ContextMenuList extends StatelessWidget {
|
||||||
|
const ContextMenuList({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView(
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FabFilter extends StatelessWidget {
|
||||||
|
const FabFilter({
|
||||||
|
super.key,
|
||||||
|
required this.listBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final WidgetBuilder listBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
showContextMenu(
|
||||||
|
context: context,
|
||||||
|
listBuilder: listBuilder,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
Symbols.filter_list_rounded,
|
||||||
|
weight: 500,
|
||||||
|
opticalSize: 28,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FabPadding extends StatelessWidget {
|
||||||
|
const FabPadding({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(height: 86);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,18 +11,19 @@ ThemeData subtracksTheme([ColorScheme? colorScheme]) {
|
|||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final text = theme.textTheme;
|
final text = theme.textTheme.copyWith(
|
||||||
return theme.copyWith(
|
headlineLarge: theme.textTheme.headlineLarge?.copyWith(
|
||||||
textTheme: text.copyWith(
|
|
||||||
headlineLarge: text.headlineLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
),
|
),
|
||||||
headlineMedium: text.headlineMedium?.copyWith(
|
headlineMedium: theme.textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
headlineSmall: text.headlineSmall?.copyWith(
|
headlineSmall: theme.textTheme.headlineSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
|
||||||
|
return theme.copyWith(
|
||||||
|
textTheme: text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,3 +19,21 @@ class CircleClip extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RoundedBoxClip extends StatelessWidget {
|
||||||
|
const RoundedBoxClip({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRRect(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
borderRadius: BorderRadiusGeometry.circular(3),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -225,6 +225,12 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<models.Artist> getArtist(int sourceId, String id) {
|
||||||
|
return db.managers.artists.filter(
|
||||||
|
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Selectable<models.Playlist> getPlaylist(int sourceId, String id) {
|
Selectable<models.Playlist> getPlaylist(int sourceId, String id) {
|
||||||
return db.managers.playlists.filter(
|
return db.managers.playlists.filter(
|
||||||
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
|
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import '../database.dart';
|
|||||||
|
|
||||||
part 'sources_dao.g.dart';
|
part 'sources_dao.g.dart';
|
||||||
|
|
||||||
|
typedef SourceSetting = ({Source source, SubsonicSetting subsonicSetting});
|
||||||
|
|
||||||
@DriftAccessor(include: {'../tables.drift'})
|
@DriftAccessor(include: {'../tables.drift'})
|
||||||
class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
||||||
with _$SourcesDaoMixin {
|
with _$SourcesDaoMixin {
|
||||||
@@ -16,7 +18,7 @@ class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
|||||||
.map((row) => row.read(sources.id));
|
.map((row) => row.read(sources.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<(Source, SubsonicSetting)>> listSources() {
|
Stream<List<SourceSetting>> listSources() {
|
||||||
final query = select(sources).join([
|
final query = select(sources).join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
subsonicSettings,
|
subsonicSettings,
|
||||||
@@ -24,18 +26,56 @@ class SourcesDao extends DatabaseAccessor<SubtracksDatabase>
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return query.watch().map(
|
return query
|
||||||
(rows) => rows
|
|
||||||
.map(
|
.map(
|
||||||
(row) => (
|
(row) => (
|
||||||
row.readTable(sources),
|
source: row.readTable(sources),
|
||||||
row.readTable(subsonicSettings),
|
subsonicSetting: row.readTable(subsonicSettings),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<SourceSetting> getSource(int id) {
|
||||||
|
final query = select(sources).join([
|
||||||
|
innerJoin(
|
||||||
|
subsonicSettings,
|
||||||
|
sources.id.equalsExp(subsonicSettings.sourceId),
|
||||||
|
),
|
||||||
|
])..where(sources.id.equals(id));
|
||||||
|
|
||||||
|
return query.map(
|
||||||
|
(row) => (
|
||||||
|
source: row.readTable(sources),
|
||||||
|
subsonicSetting: row.readTable(subsonicSettings),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSource(int id) async {
|
||||||
|
await sources.deleteWhere((f) => f.id.equals(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateSource(Source source, SubsonicSetting subsonic) async {
|
||||||
|
await db.transaction(() async {
|
||||||
|
await sources.update().replace(source);
|
||||||
|
await subsonicSettings.update().replace(subsonic);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createSource(
|
||||||
|
SourcesCompanion source,
|
||||||
|
SubsonicSettingsCompanion subsonic,
|
||||||
|
) async {
|
||||||
|
await db.transaction(() async {
|
||||||
|
final sourceId = await sources.insertOnConflictUpdate(source);
|
||||||
|
|
||||||
|
await subsonicSettings.insertOnConflictUpdate(
|
||||||
|
subsonic.copyWith(sourceId: Value(sourceId)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setActiveSource(int id) async {
|
Future<void> setActiveSource(int id) async {
|
||||||
await transaction(() async {
|
await transaction(() async {
|
||||||
await db.managers.sources.update((o) => o(isActive: Value(null)));
|
await db.managers.sources.update((o) => o(isActive: Value(null)));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../sources/models.dart' as models;
|
|||||||
import 'converters.dart';
|
import 'converters.dart';
|
||||||
import 'dao/library_dao.dart';
|
import 'dao/library_dao.dart';
|
||||||
import 'dao/sources_dao.dart';
|
import 'dao/sources_dao.dart';
|
||||||
|
import 'log_interceptor.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
@@ -27,14 +28,14 @@ class SubtracksDatabase extends _$SubtracksDatabase {
|
|||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
name: 'my_database',
|
name: 'subtracks_database',
|
||||||
native: DriftNativeOptions(
|
native: DriftNativeOptions(
|
||||||
databasePath: () async {
|
databasePath: () async {
|
||||||
final directory = await getApplicationSupportDirectory();
|
final directory = await getApplicationSupportDirectory();
|
||||||
return path.join(directory.absolute.path, 'subtracks.sqlite');
|
return path.join(directory.absolute.path, 'subtracks.sqlite');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
).interceptWith(LogInterceptor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
120
lib/database/log_interceptor.dart
Normal file
120
lib/database/log_interceptor.dart
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
import '../util/logger.dart';
|
||||||
|
|
||||||
|
/// https://drift.simonbinder.eu/examples/tracing/
|
||||||
|
class LogInterceptor extends QueryInterceptor {
|
||||||
|
Future<T> _run<T>(
|
||||||
|
String description,
|
||||||
|
FutureOr<T> Function() operation,
|
||||||
|
) async {
|
||||||
|
final trace = logger.level >= Level.trace;
|
||||||
|
final stopwatch = trace ? (Stopwatch()..start()) : null;
|
||||||
|
|
||||||
|
logger.t('Running $description');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await operation();
|
||||||
|
if (trace) {
|
||||||
|
logger.t(' => succeeded after ${stopwatch!.elapsedMilliseconds}ms');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} on Object catch (e, st) {
|
||||||
|
if (trace) {
|
||||||
|
logger.t(' => failed after ${stopwatch!.elapsedMilliseconds}ms');
|
||||||
|
}
|
||||||
|
logger.e('Query failed', error: e, stackTrace: st);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TransactionExecutor beginTransaction(QueryExecutor parent) {
|
||||||
|
logger.t('begin');
|
||||||
|
return super.beginTransaction(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> commitTransaction(TransactionExecutor inner) {
|
||||||
|
return _run('commit', () => inner.send());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> rollbackTransaction(TransactionExecutor inner) {
|
||||||
|
return _run('rollback', () => inner.rollback());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBatched(
|
||||||
|
QueryExecutor executor,
|
||||||
|
BatchedStatements statements,
|
||||||
|
) {
|
||||||
|
return _run(
|
||||||
|
'batch with $statements',
|
||||||
|
() => executor.runBatched(statements),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runInsert(
|
||||||
|
QueryExecutor executor,
|
||||||
|
String statement,
|
||||||
|
List<Object?> args,
|
||||||
|
) {
|
||||||
|
return _run(
|
||||||
|
'$statement with $args',
|
||||||
|
() => executor.runInsert(statement, args),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runUpdate(
|
||||||
|
QueryExecutor executor,
|
||||||
|
String statement,
|
||||||
|
List<Object?> args,
|
||||||
|
) {
|
||||||
|
return _run(
|
||||||
|
'$statement with $args',
|
||||||
|
() => executor.runUpdate(statement, args),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runDelete(
|
||||||
|
QueryExecutor executor,
|
||||||
|
String statement,
|
||||||
|
List<Object?> args,
|
||||||
|
) {
|
||||||
|
return _run(
|
||||||
|
'$statement with $args',
|
||||||
|
() => executor.runDelete(statement, args),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runCustom(
|
||||||
|
QueryExecutor executor,
|
||||||
|
String statement,
|
||||||
|
List<Object?> args,
|
||||||
|
) {
|
||||||
|
return _run(
|
||||||
|
'$statement with $args',
|
||||||
|
() => executor.runCustom(statement, args),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Map<String, Object?>>> runSelect(
|
||||||
|
QueryExecutor executor,
|
||||||
|
String statement,
|
||||||
|
List<Object?> args,
|
||||||
|
) {
|
||||||
|
return _run(
|
||||||
|
'$statement with $args',
|
||||||
|
() => executor.runSelect(statement, args),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/util/logger.dart
Normal file
48
lib/util/logger.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
class LogLevelFilter extends LogFilter {
|
||||||
|
@override
|
||||||
|
bool shouldLog(LogEvent event) {
|
||||||
|
return event.level >= level!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubtracksLogger extends Logger {
|
||||||
|
SubtracksLogger({
|
||||||
|
super.filter,
|
||||||
|
super.printer,
|
||||||
|
super.output,
|
||||||
|
required Level level,
|
||||||
|
}) : _level = level,
|
||||||
|
super(level: level);
|
||||||
|
|
||||||
|
final Level _level;
|
||||||
|
Level get level => _level;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubtracksLogger createLogger() {
|
||||||
|
var isDebug = false;
|
||||||
|
assert(() {
|
||||||
|
isDebug = true;
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
return SubtracksLogger(
|
||||||
|
filter: DevelopmentFilter(),
|
||||||
|
printer: PrettyPrinter(),
|
||||||
|
output: ConsoleOutput(),
|
||||||
|
level: Level.debug,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: production logger
|
||||||
|
return SubtracksLogger(
|
||||||
|
filter: DevelopmentFilter(),
|
||||||
|
printer: PrettyPrinter(),
|
||||||
|
output: ConsoleOutput(),
|
||||||
|
level: Level.debug,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final logger = createLogger();
|
||||||
@@ -565,6 +565,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
logger:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: logger
|
||||||
|
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.2"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ dependencies:
|
|||||||
infinite_scroll_pagination: ^5.1.1
|
infinite_scroll_pagination: ^5.1.1
|
||||||
intl: any
|
intl: any
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
|
logger: ^2.6.2
|
||||||
material_color_utilities: ^0.11.1
|
material_color_utilities: ^0.11.1
|
||||||
material_symbols_icons: ^4.2874.0
|
material_symbols_icons: ^4.2874.0
|
||||||
octo_image: ^2.1.0
|
octo_image: ^2.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user