From 787690c6ea499d81ec2f03a5f8d4462660682b15 Mon Sep 17 00:00:00 2001 From: Moritz Weber Date: Sat, 17 Sep 2022 22:23:24 +0200 Subject: [PATCH] initial support for home customization --- lib/presentation/pages/home_page.dart | 12 ++ .../pages/home_settings_page.dart | 170 +++++++++++++++++- lib/presentation/state/home_page_store.dart | 9 + .../widgets/custom_modal_bottom_sheet.dart | 38 ++-- .../widgets/exclude_level_options.dart | 2 +- .../widgets/song_bottom_sheet.dart | 89 ++++----- .../datasources/moor/home_widget_dao.dart | 37 +++- 7 files changed, 288 insertions(+), 69 deletions(-) diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index c67709e..fc6cb5f 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -6,11 +6,13 @@ import '../../domain/entities/home_widgets/artist_of_day.dart'; import '../../domain/entities/home_widgets/home_widget.dart'; import '../../domain/entities/home_widgets/shuffle_all.dart'; import '../state/home_page_store.dart'; +import '../state/navigation_store.dart'; import '../theming.dart'; import '../widgets/highlight_album.dart'; import '../widgets/highlight_artist.dart'; import '../widgets/shuffle_all_button.dart'; import '../widgets/smart_lists.dart'; +import 'home_settings_page.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -23,6 +25,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { final store = GetIt.I(); + final NavigationStore navStore = GetIt.I(); print('HomePage.build'); return SafeArea( @@ -31,6 +34,15 @@ class _HomePageState extends State { title: const Text( 'Home', ), + actions: [ + IconButton( + icon: const Icon(Icons.edit_rounded), + onPressed: () => navStore.push( + context, + MaterialPageRoute(builder: (context) => const HomeSettingsPage()), + ), + ), + ], ), body: Observer( builder: (context) { diff --git a/lib/presentation/pages/home_settings_page.dart b/lib/presentation/pages/home_settings_page.dart index 967607c..0c40662 100644 --- a/lib/presentation/pages/home_settings_page.dart +++ b/lib/presentation/pages/home_settings_page.dart @@ -1,22 +1,46 @@ import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:get_it/get_it.dart'; +import 'package:reorderables/reorderables.dart'; +import '../../domain/entities/home_widgets/album_of_day.dart'; +import '../../domain/entities/home_widgets/artist_of_day.dart'; +import '../../domain/entities/home_widgets/home_widget.dart'; +import '../../domain/entities/home_widgets/playlists.dart'; +import '../../domain/entities/home_widgets/shuffle_all.dart'; +import '../../domain/entities/shuffle_mode.dart'; +import '../state/home_page_store.dart'; import '../state/navigation_store.dart'; import '../theming.dart'; +import '../widgets/custom_modal_bottom_sheet.dart'; class HomeSettingsPage extends StatelessWidget { const HomeSettingsPage({Key? key}) : super(key: key); + static const titles = { + HomeWidgetType.album_of_day: 'Album of the Day', + HomeWidgetType.artist_of_day: 'Artist of the Day', + HomeWidgetType.playlists: 'Playlists', + HomeWidgetType.shuffle_all: 'Shuffle All', + }; + + static const icons = { + HomeWidgetType.album_of_day: Icons.album_rounded, + HomeWidgetType.artist_of_day: Icons.person_rounded, + HomeWidgetType.playlists: Icons.playlist_play_rounded, + HomeWidgetType.shuffle_all: Icons.shuffle_rounded, + }; + @override Widget build(BuildContext context) { - final NavigationStore navStore = GetIt.I(); + final homeStore = GetIt.I(); + final navStore = GetIt.I(); - return SafeArea( child: Scaffold( appBar: AppBar( title: const Text( - 'Home Settings', + 'Home Customization', style: TEXT_HEADER, ), leading: IconButton( @@ -25,12 +49,146 @@ class HomeSettingsPage extends StatelessWidget { ), actions: [ IconButton( - icon: const Icon(Icons.add_rounded), - onPressed: () => navStore.pop(context), - ), + icon: const Icon(Icons.add_rounded), + onPressed: () => _onTapAdd(context), + ), ], ), + body: Observer( + builder: (context) { + final widgetEntities = homeStore.homeWidgetsStream.value ?? []; + final List widgets = [ + const SliverPadding( + padding: EdgeInsets.only(top: 8.0), + ), + ]; + + widgets.add( + const SliverPadding( + padding: EdgeInsets.only(bottom: 8.0), + ), + ); + + return Scrollbar( + child: CustomScrollView( + slivers: [ + ReorderableSliverList( + delegate: ReorderableSliverChildBuilderDelegate( + (context, int index) { + return ListTile( + title: Text(titles[widgetEntities[index].type]!), + leading: Icon(icons[widgetEntities[index].type]), + trailing: IconButton( + onPressed: () => _onTapMore(context, widgetEntities[index]), + icon: const Icon(Icons.more_vert_rounded), + ), + contentPadding: const EdgeInsets.fromLTRB( + HORIZONTAL_PADDING, + 8.0, + 0.0, + 8.0, + ), + ); + }, + childCount: widgetEntities.length, + ), + onReorder: (oldIndex, newIndex) { + homeStore.moveHomeWidget(oldIndex, newIndex); + }, + ), + ], + ), + ); + }, + ), ), ); } + + Future _onTapAdd(BuildContext context) async { + final homeStore = GetIt.I(); + final homeWidgets = homeStore.homeWidgetsStream.value ?? []; + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Observer(builder: (context) { + return MyBottomSheet( + widgets: [ + const ListTile( + title: Text( + 'Add a Widget to Your Home Page', + style: TEXT_HEADER_S, + ), + tileColor: DARK2, + ), + for (final type in HomeWidgetType.values) + ListTile( + title: Text(titles[type]!), + leading: Icon(icons[type]), + onTap: () { + homeStore.addHomeWidget( + _createHomeWidget(type, homeWidgets.length), + ); + Navigator.of(context).pop(); + }, + ), + ], + ); + }), + ); + } + + Future _onTapMore(BuildContext context, HomeWidget homeWidget) async { + final homeStore = GetIt.I(); + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Observer(builder: (context) { + return MyBottomSheet( + widgets: [ + ListTile( + title: Text(titles[homeWidget.type]!), + subtitle: Text( + 'Position: ${homeWidget.position + 1}', + style: TEXT_SMALL_SUBTITLE, + ), + leading: Icon(icons[homeWidget.type]), + tileColor: DARK2, + ), + ListTile( + title: const Text('Remove widget'), + leading: const Icon( + Icons.delete_forever_rounded, + color: RED, + ), + onTap: () { + homeStore.removeHomeWidget(homeWidget); + Navigator.of(context).pop(); + }, + ), + ], + ); + }), + ); + } + + // TODO: replace this with opening a custom bottom sheet for components with parameters + HomeWidget _createHomeWidget(HomeWidgetType type, int position) { + switch (type) { + case HomeWidgetType.shuffle_all: + return HomeShuffleAll(position, ShuffleMode.plus); + case HomeWidgetType.album_of_day: + return HomeAlbumOfDay(position); + case HomeWidgetType.artist_of_day: + return HomeArtistOfDay(position, ShuffleMode.plus); + case HomeWidgetType.playlists: + return HomePlaylists(position); + } + } } diff --git a/lib/presentation/state/home_page_store.dart b/lib/presentation/state/home_page_store.dart index 6a03430..b0ec27c 100644 --- a/lib/presentation/state/home_page_store.dart +++ b/lib/presentation/state/home_page_store.dart @@ -21,4 +21,13 @@ abstract class _HomePageStore with Store { @observable late ObservableStream> homeWidgetsStream = _homeWidgetRepository.homeWidgetsStream.asObservable(); + + Future moveHomeWidget(int oldPosition, int newPosition) => + _homeWidgetRepository.moveHomeWidget(oldPosition, newPosition); + + Future addHomeWidget(HomeWidget homeWidget) => + _homeWidgetRepository.insertHomeWidget(homeWidget); + + Future removeHomeWidget(HomeWidget homeWidget) => + _homeWidgetRepository.removeHomeWidget(homeWidget); } diff --git a/lib/presentation/widgets/custom_modal_bottom_sheet.dart b/lib/presentation/widgets/custom_modal_bottom_sheet.dart index 5e55f50..f199181 100644 --- a/lib/presentation/widgets/custom_modal_bottom_sheet.dart +++ b/lib/presentation/widgets/custom_modal_bottom_sheet.dart @@ -14,26 +14,34 @@ class MyBottomSheet extends StatelessWidget { return Padding( padding: const EdgeInsets.all(12.0), child: Container( + clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - color: DARK3, borderRadius: BorderRadius.circular(8.0), boxShadow: const [ - BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 1)), + BoxShadow( + color: Colors.black26, + blurRadius: 8, + offset: Offset(0, 1), + ), ], ), - child: Column( - // TODO: evaluate list view here - mainAxisSize: MainAxisSize.min, - children: List.generate(count, (index) { - if (index.isEven) { - return widgets[(index / 2).round()]; - } else { - return Container( - height: 1, - color: Colors.white10, - ); - } - }), + child: Material( + color: DARK25, + child: ListView( + // TODO: evaluate list view here + // mainAxisSize: MainAxisSize.min, + shrinkWrap: true, + children: List.generate(count, (index) { + if (index.isEven) { + return widgets[index ~/ 2]; + } else { + return Container( + height: 1, + color: DARK2, + ); + } + }), + ), ), ), ); diff --git a/lib/presentation/widgets/exclude_level_options.dart b/lib/presentation/widgets/exclude_level_options.dart index 7fdfbe5..b466e75 100644 --- a/lib/presentation/widgets/exclude_level_options.dart +++ b/lib/presentation/widgets/exclude_level_options.dart @@ -22,7 +22,7 @@ class ExcludeLevelOptions extends StatelessWidget { final lvl = _commonBlockLevel(songs); return Container( - color: Colors.white10, + // color: Colors.white10, child: Padding( padding: const EdgeInsets.only( left: HORIZONTAL_PADDING - 12, diff --git a/lib/presentation/widgets/song_bottom_sheet.dart b/lib/presentation/widgets/song_bottom_sheet.dart index 54e047e..b389c5a 100644 --- a/lib/presentation/widgets/song_bottom_sheet.dart +++ b/lib/presentation/widgets/song_bottom_sheet.dart @@ -83,7 +83,7 @@ class _SongBottomSheetState extends State { final options = [ const SizedBox.shrink(), Container( - color: Colors.white10, + // color: DARK3, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -95,7 +95,7 @@ class _SongBottomSheetState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), ), ), - Container(width: 1.0, height: 24.0, color: Colors.white38), + Container(width: 1.0, height: 24.0, color: DARK2), Expanded( child: SwitchListTile( title: const Text('Next'), @@ -111,47 +111,54 @@ class _SongBottomSheetState extends State { ]; final List widgets = [ - Padding( - padding: const EdgeInsets.all(HORIZONTAL_PADDING), - child: Row( - children: [ - Container( - width: 64.0, - height: 64.0, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.0), - boxShadow: const [ - BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 1)), - ], - image: DecorationImage( - image: utils.getAlbumImage(song.albumArtPath), - fit: BoxFit.fill, + Container( + color: DARK2, + child: Padding( + padding: const EdgeInsets.all(HORIZONTAL_PADDING), + child: Row( + children: [ + Container( + width: 64.0, + height: 64.0, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.0), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 8, + offset: Offset(0, 1), + ), + ], + image: DecorationImage( + image: utils.getAlbumImage(song.albumArtPath), + fit: BoxFit.fill, + ), ), ), - ), - const SizedBox(width: 12.0), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song.title, - style: TEXT_HEADER_S, - ), - const SizedBox(height: 4.0), - Text( - '#${song.trackNumber} • ${utils.msToTimeString(song.duration)} • ${song.year}', - style: TEXT_SMALL_SUBTITLE, - ), - Text( - 'played: ${song.playCount} • skipped: ${song.skipCount}', - style: TEXT_SMALL_SUBTITLE, - ), - ], - ), - ) - ], + const SizedBox(width: 12.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + song.title, + style: TEXT_HEADER_S, + ), + const SizedBox(height: 4.0), + Text( + '#${song.trackNumber} • ${utils.msToTimeString(song.duration)} • ${song.year}', + style: TEXT_SMALL_SUBTITLE, + ), + Text( + 'played: ${song.playCount} • skipped: ${song.skipCount}', + style: TEXT_SMALL_SUBTITLE, + ), + ], + ), + ) + ], + ), ), ), ListTile( diff --git a/lib/system/datasources/moor/home_widget_dao.dart b/lib/system/datasources/moor/home_widget_dao.dart index 4df8944..77885f6 100644 --- a/lib/system/datasources/moor/home_widget_dao.dart +++ b/lib/system/datasources/moor/home_widget_dao.dart @@ -30,15 +30,40 @@ class HomeWidgetDao extends DatabaseAccessor } @override - Future moveHomeWidget(int oldPosition, int newPosition) { - // TODO: implement moveHomeWidget - throw UnimplementedError(); + Future moveHomeWidget(int oldPosition, int newPosition) async { + if (oldPosition != newPosition) { + transaction(() async { + await (update(homeWidgets)..where((tbl) => tbl.position.equals(oldPosition))) + .write(const HomeWidgetsCompanion(position: Value(-1))); + if (oldPosition < newPosition) { + for (int i = oldPosition + 1; i <= newPosition; i++) { + await (update(homeWidgets)..where((tbl) => tbl.position.equals(i))) + .write(HomeWidgetsCompanion(position: Value(i - 1))); + } + } else { + for (int i = oldPosition - 1; i >= newPosition; i--) { + await (update(homeWidgets)..where((tbl) => tbl.position.equals(i))) + .write(HomeWidgetsCompanion(position: Value(i + 1))); + } + } + await (update(homeWidgets)..where((tbl) => tbl.position.equals(-1))) + .write(HomeWidgetsCompanion(position: Value(newPosition))); + }); + } } @override - Future removeHomeWidget(HomeWidgetModel homeWidget) { - // TODO: implement removeHomeWidget - throw UnimplementedError(); + Future removeHomeWidget(HomeWidgetModel homeWidget) async { + final entries = await select(homeWidgets).get(); + final count = entries.length; + + transaction(() async { + await (delete(homeWidgets)..where((tbl) => tbl.position.equals(homeWidget.position))).go(); + for (int i = homeWidget.position + 1; i < count; i++) { + await (update(homeWidgets)..where((tbl) => tbl.position.equals(i))) + .write(HomeWidgetsCompanion(position: Value(i - 1))); + } + }); } @override