initial support for home customization

This commit is contained in:
Moritz Weber 2022-09-17 22:23:24 +02:00
parent 71dc7bc803
commit 787690c6ea
7 changed files with 288 additions and 69 deletions

View file

@ -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<HomePage> {
@override
Widget build(BuildContext context) {
final store = GetIt.I<HomePageStore>();
final NavigationStore navStore = GetIt.I<NavigationStore>();
print('HomePage.build');
return SafeArea(
@ -31,6 +34,15 @@ class _HomePageState extends State<HomePage> {
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) {

View file

@ -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<NavigationStore>();
final homeStore = GetIt.I<HomePageStore>();
final navStore = GetIt.I<NavigationStore>();
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 ?? <HomeWidget>[];
final List<Widget> 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<void> _onTapAdd(BuildContext context) async {
final homeStore = GetIt.I<HomePageStore>();
final homeWidgets = homeStore.homeWidgetsStream.value ?? <HomeWidget>[];
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<void> _onTapMore(BuildContext context, HomeWidget homeWidget) async {
final homeStore = GetIt.I<HomePageStore>();
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);
}
}
}

View file

@ -21,4 +21,13 @@ abstract class _HomePageStore with Store {
@observable
late ObservableStream<List<HomeWidget>> homeWidgetsStream =
_homeWidgetRepository.homeWidgetsStream.asObservable();
Future<void> moveHomeWidget(int oldPosition, int newPosition) =>
_homeWidgetRepository.moveHomeWidget(oldPosition, newPosition);
Future<void> addHomeWidget(HomeWidget homeWidget) =>
_homeWidgetRepository.insertHomeWidget(homeWidget);
Future<void> removeHomeWidget(HomeWidget homeWidget) =>
_homeWidgetRepository.removeHomeWidget(homeWidget);
}

View file

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

View file

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

View file

@ -83,7 +83,7 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
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<SongBottomSheet> {
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<SongBottomSheet> {
];
final List<Widget> 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(

View file

@ -30,15 +30,40 @@ class HomeWidgetDao extends DatabaseAccessor<MoorDatabase>
}
@override
Future<void> moveHomeWidget(int oldPosition, int newPosition) {
// TODO: implement moveHomeWidget
throw UnimplementedError();
Future<void> 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<void> removeHomeWidget(HomeWidgetModel homeWidget) {
// TODO: implement removeHomeWidget
throw UnimplementedError();
Future<void> 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