migration to Material 3; fix #67 (#72)

This commit is contained in:
Moritz Weber 2023-04-17 22:27:58 +02:00 committed by GitHub
parent 5411d47924
commit a3a6c7cc0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 2779 additions and 2510 deletions

View file

@ -2,6 +2,8 @@
- Fixed bug in "Append to manually queued songs"
- Fixed bug in queue when moving a song directly before the currently playing song
- Migration to Material 3 widgets including extensive UI changes
- New Icons for linked songs
- Added German translation (#51)
## 1.2.0

View file

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
enable-background="new 0 0 24 24"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg12"
sodipodi:docname="link_both.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="true"
inkscape:zoom="11.313709"
inkscape:cx="6.8942908"
inkscape:cy="9.8111062"
inkscape:window-width="1920"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg12">
<sodipodi:guide
position="16.305799,11.996861"
orientation="0,-1"
id="guide176"
inkscape:locked="false" />
<sodipodi:guide
position="3,11.996094"
orientation="1,0"
id="guide274"
inkscape:locked="false" />
<sodipodi:guide
position="21,12.003906"
orientation="1,0"
id="guide337"
inkscape:locked="false" />
<sodipodi:guide
position="12.020815,13.017748"
orientation="1,0"
id="guide339"
inkscape:locked="false" />
<sodipodi:guide
position="12.679688,20.996094"
orientation="0,-1"
id="guide341"
inkscape:locked="false" />
<sodipodi:guide
position="12.275641,2.9947327"
orientation="0,-1"
id="guide343"
inkscape:locked="false" />
<sodipodi:guide
position="17,12.96875"
orientation="1,0"
id="guide3508"
inkscape:locked="false" />
<sodipodi:guide
position="16.1875,15.984375"
orientation="0,-1"
id="guide3510"
inkscape:locked="false" />
<sodipodi:guide
position="8.8940775,21.989165"
orientation="0,-1"
id="guide4248"
inkscape:locked="false" />
<sodipodi:guide
position="22.914679,1.9692044"
orientation="0,-1"
id="guide4277"
inkscape:locked="false" />
</sodipodi:namedview>
<path
id="path4246-6"
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 4.9921875,18.332031 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 H 16.617187 a 1,1 0 0 0 0,1.390625 1,1 0 0 0 1.414063,0 l 1.683594,-1.683594 A 1.0001,1.0001 0 0 0 19.007812,18.332031 Z M 6.6757812,2.0117188 A 1,1 0 0 0 5.96875,2.3046875 L 4.2851562,3.9882813 A 1.0001,1.0001 0 0 0 4.9921875,5.6953125 H 19.007812 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 H 7.3984375 A 1,1 0 0 0 7.3828125,2.3046875 1,1 0 0 0 6.6757812,2.0117188 Z M 17,7 h -3 c -0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1 h 3 c 1.65,0 3,1.35 3,3 0,1.65 -1.35,3 -3,3 h -3 c -0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1 h 3 c 2.76,0 5,-2.24 5,-5 0,-2.76 -2.24,-5 -5,-5 z m -9,5 c 0,0.55 0.45,1 1,1 h 6 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 H 9 c -0.55,0 -1,0.45 -1,1 z m 2,3 H 7 C 5.35,15 4,13.65 4,12 4,10.35 5.35,9 7,9 h 3 C 10.55,9 11,8.55 11,8 11,7.45 10.55,7 10,7 H 7 c -2.76,0 -5,2.24 -5,5 0,2.76 2.24,5 5,5 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
enable-background="new 0 0 24 24"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg12"
sodipodi:docname="link_next.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="true"
inkscape:zoom="22.627418"
inkscape:cx="11.578873"
inkscape:cy="16.793785"
inkscape:window-width="1920"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg12">
<sodipodi:guide
position="16.305799,11.996861"
orientation="0,-1"
id="guide176"
inkscape:locked="false" />
<sodipodi:guide
position="3,11.996094"
orientation="1,0"
id="guide274"
inkscape:locked="false" />
<sodipodi:guide
position="21,12.003906"
orientation="1,0"
id="guide337"
inkscape:locked="false" />
<sodipodi:guide
position="12.020815,13.017748"
orientation="1,0"
id="guide339"
inkscape:locked="false" />
<sodipodi:guide
position="12.679688,20.996094"
orientation="0,-1"
id="guide341"
inkscape:locked="false" />
<sodipodi:guide
position="12.275641,2.9947327"
orientation="0,-1"
id="guide343"
inkscape:locked="false" />
<sodipodi:guide
position="17,12.96875"
orientation="1,0"
id="guide3508"
inkscape:locked="false" />
<sodipodi:guide
position="16.1875,15.984375"
orientation="0,-1"
id="guide3510"
inkscape:locked="false" />
<sodipodi:guide
position="8.8940775,21.989165"
orientation="0,-1"
id="guide4248"
inkscape:locked="false" />
<sodipodi:guide
position="22.914679,1.9692044"
orientation="0,-1"
id="guide4277"
inkscape:locked="false" />
</sodipodi:namedview>
<path
id="path4246-6"
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 4.9921875,18.332031 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 H 16.617187 a 1,1 0 0 0 0,1.390625 1,1 0 0 0 1.414063,0 l 1.683594,-1.683594 A 1.0001,1.0001 0 0 0 19.007812,18.332031 Z M 17,7 h -3 c -0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1 h 3 c 1.65,0 3,1.35 3,3 0,1.65 -1.35,3 -3,3 h -3 c -0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1 h 3 c 2.76,0 5,-2.24 5,-5 0,-2.76 -2.24,-5 -5,-5 z m -9,5 c 0,0.55 0.45,1 1,1 h 6 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 H 9 c -0.55,0 -1,0.45 -1,1 z m 2,3 H 7 C 5.35,15 4,13.65 4,12 4,10.35 5.35,9 7,9 h 3 C 10.55,9 11,8.55 11,8 11,7.45 10.55,7 10,7 H 7 c -2.76,0 -5,2.24 -5,5 0,2.76 2.24,5 5,5 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
enable-background="new 0 0 24 24"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg12"
sodipodi:docname="link_prev.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
id="path4246"
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 6.6757812,2.0117188 A 1,1 0 0 0 5.96875,2.3046875 L 4.2851562,3.9882813 A 1.0001,1.0001 0 0 0 4.9921875,5.6953125 H 19.007812 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 H 7.3984375 A 1,1 0 0 0 7.3828125,2.3046875 1,1 0 0 0 6.6757812,2.0117188 Z M 17,7 h -3 c -0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1 h 3 c 1.65,0 3,1.35 3,3 0,1.65 -1.35,3 -3,3 h -3 c -0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1 h 3 c 2.76,0 5,-2.24 5,-5 0,-2.76 -2.24,-5 -5,-5 z m -9,5 c 0,0.55 0.45,1 1,1 h 6 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 H 9 c -0.55,0 -1,0.45 -1,1 z m 2,3 H 7 C 5.35,15 4,13.65 4,12 4,10.35 5.35,9 7,9 h 3 C 10.55,9 11,8.55 11,8 11,7.45 10.55,7 10,7 H 7 c -2.76,0 -5,2.24 -5,5 0,2.76 2.24,5 5,5 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z" />
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="true"
inkscape:zoom="22.627418"
inkscape:cx="11.578873"
inkscape:cy="16.793785"
inkscape:window-width="1920"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg12">
<sodipodi:guide
position="16.305799,11.996861"
orientation="0,-1"
id="guide176"
inkscape:locked="false" />
<sodipodi:guide
position="3,11.996094"
orientation="1,0"
id="guide274"
inkscape:locked="false" />
<sodipodi:guide
position="21,12.003906"
orientation="1,0"
id="guide337"
inkscape:locked="false" />
<sodipodi:guide
position="12.020815,13.017748"
orientation="1,0"
id="guide339"
inkscape:locked="false" />
<sodipodi:guide
position="12.679688,20.996094"
orientation="0,-1"
id="guide341"
inkscape:locked="false" />
<sodipodi:guide
position="12.275641,2.9947327"
orientation="0,-1"
id="guide343"
inkscape:locked="false" />
<sodipodi:guide
position="17,12.96875"
orientation="1,0"
id="guide3508"
inkscape:locked="false" />
<sodipodi:guide
position="16.1875,15.984375"
orientation="0,-1"
id="guide3510"
inkscape:locked="false" />
<sodipodi:guide
position="8.8940775,21.989165"
orientation="0,-1"
id="guide4248"
inkscape:locked="false" />
<sodipodi:guide
position="22.914679,1.9692044"
orientation="0,-1"
id="guide4277"
inkscape:locked="false" />
</sodipodi:namedview>
<g
id="g10">
<g
id="g8" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

View file

@ -24,6 +24,7 @@ abstract class MusicDataInfoRepository {
Stream<List<Song>> getSmartListSongStream(SmartList smartList);
Future<List<Song>> getPredecessors(Song song);
Future<List<Song>> getSuccessors(Song song);
Future<List<bool>> isSongFirstLast(Song song);
Stream<List<Playlist>> get playlistsStream;
Stream<Playlist> getPlaylistStream(int playlistId);

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
const CUSTOM_GRADIENTS = <String, Gradient>{
'sanguine': LinearGradient(
'sanguine': LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [

View file

@ -39,80 +39,78 @@ class _ArtistOfDayFormPageState extends State<ArtistOfDayFormPage> {
Widget build(BuildContext context) {
final NavigationStore navStore = GetIt.I<NavigationStore>();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.artistOfTheDay,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.artistOfTheDay,
style: TEXT_HEADER,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.playback, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2].map<RadioListTile<int>>(
(int value) {
return RadioListTile<int>(
title: Text(
ShuffleMode.values[value].toText(context),
style: const TextStyle(
fontSize: 14.0,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.playback, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2].map<RadioListTile<int>>(
(int value) {
return RadioListTile<int>(
title: Text(
ShuffleMode.values[value].toText(context),
style: const TextStyle(
fontSize: 14.0,
),
value: value,
groupValue: store.shuffleMode.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue];
});
},
);
},
).toList(),
);
},
),
),
value: value,
groupValue: store.shuffleMode.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue];
});
},
);
},
).toList(),
);
},
),
),
],
),
),
],
),
],
),
),
],
),
),
),

View file

@ -37,102 +37,100 @@ class _HistoryFormPageState extends State<HistoryFormPage> {
Widget build(BuildContext context) {
final NavigationStore navStore = GetIt.I<NavigationStore>();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.history,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.history,
style: TEXT_HEADER,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.displaySettings, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Container(
color: Colors.transparent,
height: 48.0,
child: Text(L10n.of(context)!.maxNumberEntries),
alignment: Alignment.centerLeft,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.displaySettings, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Container(
color: Colors.transparent,
height: 48.0,
child: Text(L10n.of(context)!.maxNumberEntries),
alignment: Alignment.centerLeft,
),
SizedBox(
width: 56.0,
child: TextFormField(
keyboardType: TextInputType.number,
initialValue: widget.history.maxEntries.toString(),
onChanged: (value) {
store.maxEntries = value;
},
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
filled: true,
fillColor: DARK35,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
errorStyle: TextStyle(height: 0, fontSize: 0),
contentPadding: EdgeInsets.only(
top: 0.0,
bottom: 0.0,
left: 4.0,
right: 2.0,
),
),
SizedBox(
width: 56.0,
child: TextFormField(
keyboardType: TextInputType.number,
initialValue: widget.history.maxEntries.toString(),
onChanged: (value) {
store.maxEntries = value;
},
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
filled: true,
fillColor: DARK35,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
errorStyle: TextStyle(height: 0, fontSize: 0),
contentPadding: EdgeInsets.only(
top: 0.0,
bottom: 0.0,
left: 4.0,
right: 2.0,
),
),
),
],
),
);
},
),
),
],
),
);
},
),
),
],
),
),
],
),
],
),
),
],
),
),
),

View file

@ -59,192 +59,190 @@ class _PlaylistsFormPageState extends State<PlaylistsFormPage> {
L10n.of(context)!.smartlistsOnly,
];
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.playlists,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.playlists,
style: TEXT_HEADER,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Observer(
builder: (_) => TextFormField(
initialValue: store.title,
onChanged: (value) => store.title = value,
style: TEXT_HEADER,
decoration: InputDecoration(
labelText: L10n.of(context)!.name,
labelStyle: const TextStyle(color: Colors.white),
floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white),
// errorText: store.error.name,
errorStyle: const TextStyle(color: RED),
filled: true,
fillColor: DARK35,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 12.0,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Observer(
builder: (_) => TextFormField(
initialValue: store.title,
onChanged: (value) => store.title = value,
style: TEXT_HEADER,
decoration: InputDecoration(
labelText: L10n.of(context)!.name,
labelStyle: const TextStyle(color: Colors.white),
floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white),
// errorText: store.error.name,
errorStyle: const TextStyle(color: RED),
filled: true,
fillColor: DARK35,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 12.0,
),
),
),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.sortingFilterSettings, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.maxNumberEntries,
switchValue: store.maxEntriesEnabled,
onSwitchChanged: (bool value) {
store.maxEntriesEnabled = value;
},
textValue: store.maxEntries,
onTextChanged: (String value) {
store.maxEntries = value;
},
);
},
),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.sortingFilterSettings, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.maxNumberEntries,
switchValue: store.maxEntriesEnabled,
onSwitchChanged: (bool value) {
store.maxEntriesEnabled = value;
},
textValue: store.maxEntries,
onTextChanged: (String value) {
store.maxEntries = value;
},
);
},
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
orderCriterionTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
orderCriterionTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
value: value,
groupValue: store.orderCriterion.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null)
store.orderCriterion =
HomePlaylistsOrder.values[newValue];
});
},
);
}).toList(),
);
},
),
),
value: value,
groupValue: store.orderCriterion.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null)
store.orderCriterion =
HomePlaylistsOrder.values[newValue];
});
},
);
}).toList(),
);
},
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
orderDirectionTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
orderDirectionTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
value: value,
groupValue: store.orderDirection.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null)
store.orderDirection = OrderDirection.values[newValue];
});
},
);
}).toList(),
);
},
),
),
value: value,
groupValue: store.orderDirection.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null)
store.orderDirection = OrderDirection.values[newValue];
});
},
);
}).toList(),
);
},
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
filterTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
filterTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
value: value,
groupValue: store.filter.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null)
store.filter = HomePlaylistsFilter.values[newValue];
});
},
);
}).toList(),
);
},
),
),
value: value,
groupValue: store.filter.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null)
store.filter = HomePlaylistsFilter.values[newValue];
});
},
);
}).toList(),
);
},
),
),
],
),
),
],
),
],
),
),
],
),
),
),

View file

@ -39,80 +39,78 @@ class _ShuffleAllFormPageState extends State<ShuffleAllFormPage> {
Widget build(BuildContext context) {
final NavigationStore navStore = GetIt.I<NavigationStore>();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.shuffleAll,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.shuffleAll,
style: TEXT_HEADER,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.playback, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[1, 2].map<RadioListTile<int>>(
(int value) {
return RadioListTile<int>(
title: Text(
ShuffleMode.values[value].toText(context),
style: const TextStyle(
fontSize: 14.0,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
// store.validateAll();
// if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
// }
},
),
],
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.playback, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[1, 2].map<RadioListTile<int>>(
(int value) {
return RadioListTile<int>(
title: Text(
ShuffleMode.values[value].toText(context),
style: const TextStyle(
fontSize: 14.0,
),
value: value,
groupValue: store.shuffleMode.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue];
});
},
);
},
).toList(),
);
},
),
),
value: value,
groupValue: store.shuffleMode.index,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue];
});
},
);
},
).toList(),
);
},
),
),
],
),
),
],
),
],
),
),
],
),
),
),

View file

@ -1,5 +1,5 @@
/// Flutter icons MuckeIcons
/// Copyright (C) 2022 by original authors @ fluttericon.com, fontello.com
/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
import 'package:flutter/widgets.dart';
@ -22,7 +22,10 @@ class MuckeIcons {
static const IconData favorite_2_3 =
IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData shuffle_heart =
IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData shuffle_none =
IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData link_both = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData link_next = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData link_prev = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View file

@ -50,154 +50,153 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
final AudioStore audioStore = GetIt.I<AudioStore>();
return Scaffold(
body: Observer(
builder: (BuildContext context) {
final album = widget.album;
final songs = store.albumSongStream.value ?? [];
final totalDuration =
songs.fold(const Duration(milliseconds: 0), (Duration d, s) => d + s.duration);
final songsByDisc = _songsByDisc(store.albumSongStream.value ?? []);
final discSongNums = [0];
for (int i = 0; i < songsByDisc.length - 1; i++) {
discSongNums.add(songsByDisc[i].length + discSongNums[i]);
}
body: Material(
child: Observer(
builder: (BuildContext context) {
final album = widget.album;
final songs = store.albumSongStream.value ?? [];
final totalDuration =
songs.fold(const Duration(milliseconds: 0), (Duration d, s) => d + s.duration);
final songsByDisc = _songsByDisc(store.albumSongStream.value ?? []);
final discSongNums = [0];
for (int i = 0; i < songsByDisc.length - 1; i++) {
discSongNums.add(songsByDisc[i].length + discSongNums[i]);
}
return Scrollbar(
child: CustomScrollView(
slivers: <Widget>[
CoverSliverAppBar(
title: album.title,
subtitle: album.artist,
subtitle2:
'${album.pubYear.toString()}${L10n.of(context)!.nSongs(songs.length)}${utils.msToTimeString(totalDuration)}',
actions: [
Observer(
builder: (context) {
return Scrollbar(
child: CustomScrollView(
slivers: <Widget>[
CoverSliverAppBar(
title: album.title,
subtitle: album.artist,
subtitle2:
'${album.pubYear.toString()}${L10n.of(context)!.nSongs(songs.length)}${utils.msToTimeString(totalDuration)}',
actions: [
Observer(
builder: (context) {
final isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => _openMultiselectMenu(context),
);
return Container();
},
),
Observer(
builder: (context) {
final isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
final isAllSelected = store.selection.isAllSelected;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: isAllSelected
? const Icon(Icons.deselect_rounded)
: const Icon(Icons.select_all_rounded),
onPressed: () {
if (isAllSelected)
store.selection.deselectAll();
else
store.selection.selectAll();
},
);
return Container();
},
),
Observer(builder: (context) {
final isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => _openMultiselectMenu(context),
);
return Container();
},
return IconButton(
key: const ValueKey('ALBUM_MULTISELECT'),
icon: isMultiSelectEnabled
? const Icon(Icons.close_rounded)
: const Icon(Icons.checklist_rtl_rounded),
onPressed: () => store.selection.toggleMultiSelect(),
);
})
],
cover: Image(
image: utils.getAlbumImage(album.albumArtPath),
fit: BoxFit.cover,
),
Observer(
builder: (context) {
final isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
final isAllSelected = store.selection.isAllSelected;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: isAllSelected
? const Icon(Icons.deselect_rounded)
: const Icon(Icons.select_all_rounded),
onPressed: () {
if (isAllSelected)
store.selection.deselectAll();
else
store.selection.selectAll();
},
);
return Container();
},
),
Observer(builder: (context) {
final isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
return IconButton(
key: const ValueKey('ALBUM_MULTISELECT'),
icon: isMultiSelectEnabled
? const Icon(Icons.close_rounded)
: const Icon(Icons.checklist_rtl_rounded),
onPressed: () => store.selection.toggleMultiSelect(),
);
})
],
cover: Image(
image: utils.getAlbumImage(album.albumArtPath),
fit: BoxFit.cover,
),
background: Image(
image: utils.getAlbumImage(album.albumArtPath),
fit: BoxFit.cover,
),
button: SizedBox(
width: 48,
child: ElevatedButton(
onPressed: () => audioStore.playAlbum(album),
child: const Icon(Icons.play_arrow_rounded),
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
backgroundColor: LIGHT1,
fixedSize: const Size.fromHeight(48),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
backgroundColor: utils.bgColor(album.color),
button: SizedBox(
width: 48,
child: Center(
child: ElevatedButton(
onPressed: () => audioStore.playAlbum(album),
child: const Icon(Icons.play_arrow_rounded),
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
backgroundColor: LIGHT1,
fixedSize: const Size.fromHeight(48),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
),
),
),
),
),
),
for (int d = 0; d < songsByDisc.length; d++)
Observer(
builder: (context) {
final bool isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
final List<bool> isSelected = store.selection.isSelected.toList();
for (int d = 0; d < songsByDisc.length; d++)
Observer(
builder: (context) {
final bool isMultiSelectEnabled = store.selection.isMultiSelectEnabled;
final List<bool> isSelected = store.selection.isSelected.toList();
return SliverList(
delegate: SliverChildListDelegate(
[
if (songsByDisc.length > 1 && d > 0) Container(height: 8.0),
if (songsByDisc.length > 1)
ListTile(
title: Text('${L10n.of(context)!.disc} ${d + 1}', style: TEXT_HEADER),
leading:
const SizedBox(width: 40, child: Icon(Icons.album_rounded)),
contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
),
if (songsByDisc.length > 1)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING,
),
child: Container(
height: 1.0,
color: Colors.white10,
),
),
for (int s = 0; s < songsByDisc[d].length; s++)
SongListTileNumbered(
song: songsByDisc[d][s],
isSelectEnabled: isMultiSelectEnabled,
isSelected: isMultiSelectEnabled && isSelected[s + discSongNums[d]],
onTap: () => audioStore.playAlbumFromIndex(
widget.album,
s + _calcOffset(d, songsByDisc),
),
onTapMore: () => showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SongBottomSheet(
song: songsByDisc[d][s],
enableGoToAlbum: false,
return SliverList(
delegate: SliverChildListDelegate(
[
if (songsByDisc.length > 1 && d > 0) Container(height: 8.0),
if (songsByDisc.length > 1)
ListTile(
title: Text(
'${L10n.of(context)!.disc} ${d + 1}',
style: TEXT_HEADER,
),
leading: const SizedBox(
width: 40,
child: Icon(Icons.album_rounded),
),
contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
),
onSelect: (bool selected) =>
store.selection.setSelected(selected, s + discSongNums[d]),
)
],
),
);
},
),
],
),
);
},
if (songsByDisc.length > 1)
const Divider(indent: HORIZONTAL_PADDING, endIndent: HORIZONTAL_PADDING),
for (int s = 0; s < songsByDisc[d].length; s++)
SongListTileNumbered(
song: songsByDisc[d][s],
isSelectEnabled: isMultiSelectEnabled,
isSelected:
isMultiSelectEnabled && isSelected[s + discSongNums[d]],
onTap: () => audioStore.playAlbumFromIndex(
widget.album,
s + _calcOffset(d, songsByDisc),
),
onTapMore: () => showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SongBottomSheet(
song: songsByDisc[d][s],
enableGoToAlbum: false,
),
),
onSelect: (bool selected) =>
store.selection.setSelected(selected, s + discSongNums[d]),
)
],
),
);
},
),
],
),
);
},
),
),
);
}

View file

@ -48,8 +48,8 @@ class _ArtistDetailsPageState extends State<ArtistDetailsPage> {
final AudioStore audioStore = GetIt.I<AudioStore>();
return Observer(
builder: (BuildContext context) => SafeArea(
child: CustomScrollView(
builder: (BuildContext context) => Scaffold(
body: CustomScrollView(
slivers: [
ArtistHeader(artist: widget.artist),
SliverList(

View file

@ -15,46 +15,44 @@ class BlockedFilesPage extends StatelessWidget {
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
final NavigationStore navStore = GetIt.I<NavigationStore>();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.blockedFiles,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
titleSpacing: 0.0,
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.blockedFiles,
style: TEXT_HEADER,
),
body: Observer(
builder: (context) {
final paths = settingsStore.blockedFilesStream.value?.toList() ?? [];
return ListView.separated(
itemCount: paths.length,
itemBuilder: (_, int index) {
final String path = paths[index];
final split = path.split('/');
return ListTile(
title: Text(split.last),
subtitle: Text(
split.sublist(0, split.length - 1).join('/'),
style: TEXT_SMALL_SUBTITLE,
),
trailing: IconButton(
icon: const Icon(Icons.delete_rounded),
onPressed: () => settingsStore.removeBlockedFiles([path]),
),
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
);
},
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
titleSpacing: 0.0,
),
body: Observer(
builder: (context) {
final paths = settingsStore.blockedFilesStream.value?.toList() ?? [];
return ListView.separated(
itemCount: paths.length,
itemBuilder: (_, int index) {
final String path = paths[index];
final split = path.split('/');
return ListTile(
title: Text(split.last),
subtitle: Text(
split.sublist(0, split.length - 1).join('/'),
style: TEXT_SMALL_SUBTITLE,
),
trailing: IconButton(
icon: const Icon(Icons.delete_rounded),
onPressed: () => settingsStore.removeBlockedFiles([path]),
),
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
);
},
),
);
}

View file

@ -22,112 +22,110 @@ class _CoverCustomizationPageState extends State<CoverCustomizationPage> {
Widget build(BuildContext context) {
print('CoverCustomizationPage.build');
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.customizeCover,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.customizeCover,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () => Navigator.of(context).pop(),
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () => Navigator.of(context).pop(),
),
],
),
body: Observer(
builder: (context) => Column(
children: [
Container(
color: DARK1,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: PlaylistCover(
size: 120,
gradient: widget.store.gradient,
icon: CUSTOM_ICONS[widget.store.iconString]!,
),
],
),
body: Observer(
builder: (context) => Column(
children: [
Container(
color: DARK1,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: PlaylistCover(
size: 120,
gradient: widget.store.gradient,
icon: CUSTOM_ICONS[widget.store.iconString]!,
),
),
),
Expanded(
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING,
vertical: HORIZONTAL_PADDING,
),
sliver: SliverGrid.count(
crossAxisCount: 5,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
children: [
for (final gradient in CUSTOM_GRADIENTS.entries)
GestureDetector(
onTap: () => widget.store.setGradient(gradient.key),
child: Container(
decoration: BoxDecoration(
gradient: gradient.value,
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
border: Border.all(
color: gradient.key == widget.store.gradientString
? Colors.white
: Colors.transparent,
width: 3.0,
)),
),
)
],
),
),
Expanded(
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING,
vertical: HORIZONTAL_PADDING,
),
SliverPadding(
padding: const EdgeInsets.only(
left: HORIZONTAL_PADDING,
right: HORIZONTAL_PADDING,
bottom: HORIZONTAL_PADDING,
),
sliver: SliverGrid.count(
crossAxisCount: 5,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
children: [
for (final icon in CUSTOM_ICONS.entries)
GestureDetector(
onTap: () => widget.store.setIconString(icon.key),
child: Container(
child: Center(
child: Icon(icon.value),
),
decoration: BoxDecoration(
color: Colors.white10,
sliver: SliverGrid.count(
crossAxisCount: 5,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
children: [
for (final gradient in CUSTOM_GRADIENTS.entries)
GestureDetector(
onTap: () => widget.store.setGradient(gradient.key),
child: Container(
decoration: BoxDecoration(
gradient: gradient.value,
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
border: Border.all(
color: icon.key == widget.store.iconString
color: gradient.key == widget.store.gradientString
? Colors.white
: Colors.transparent,
width: 3.0,
),
)),
),
)
],
),
),
SliverPadding(
padding: const EdgeInsets.only(
left: HORIZONTAL_PADDING,
right: HORIZONTAL_PADDING,
bottom: HORIZONTAL_PADDING,
),
sliver: SliverGrid.count(
crossAxisCount: 5,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
children: [
for (final icon in CUSTOM_ICONS.entries)
GestureDetector(
onTap: () => widget.store.setIconString(icon.key),
child: Container(
child: Center(
child: Icon(icon.value),
),
decoration: BoxDecoration(
color: Colors.white10,
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
border: Border.all(
color: icon.key == widget.store.iconString
? Colors.white
: Colors.transparent,
width: 3.0,
),
),
)
],
),
),
)
],
),
],
),
),
],
),
),
],
),
),
],
),
),
);

View file

@ -2,6 +2,7 @@ import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:get_it/get_it.dart';
import 'package:text_scroll/text_scroll.dart';
import '../../domain/entities/song.dart';
import '../state/audio_store.dart';
@ -69,25 +70,41 @@ class CurrentlyPlayingPage extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0 + 12.0),
child: SizedBox(
width: double.infinity,
height: 74.0,
height: 58.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
TextScroll(
song.title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
mode: TextScrollMode.endless,
velocity: const Velocity(pixelsPerSecond: Offset(40, 0)),
delayBefore: const Duration(milliseconds: 500),
pauseBetween: const Duration(milliseconds: 2000),
pauseOnBounce: const Duration(milliseconds: 1000),
style: TEXT_BIG,
textAlign: TextAlign.left,
fadedBorder: true,
fadedBorderWidth: 0.02,
fadeBorderVisibility: FadeBorderVisibility.auto,
intervalSpaces: 30,
),
Text(
TextScroll(
'${song.artist}${song.album}',
mode: TextScrollMode.endless,
velocity: const Velocity(pixelsPerSecond: Offset(40, 0)),
delayBefore: const Duration(milliseconds: 500),
pauseBetween: const Duration(milliseconds: 2000),
pauseOnBounce: const Duration(milliseconds: 1000),
style: TextStyle(
color: Colors.grey[300],
fontSize: 18.0,
fontWeight: FontWeight.w300,
),
maxLines: 2,
textAlign: TextAlign.left,
fadedBorder: true,
fadedBorderWidth: 0.02,
fadeBorderVisibility: FadeBorderVisibility.auto,
intervalSpaces: 30,
),
],
),

View file

@ -46,85 +46,83 @@ class _HomePageInner extends StatelessWidget {
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
print('HomePage.build');
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(L10n.of(context)!.home),
actions: [
IconButton(
icon: const Icon(Icons.edit_rounded),
tooltip: L10n.of(context)!.customizeHomePage,
onPressed: () => navStore.push(
context,
MaterialPageRoute(builder: (context) => const HomeSettingsPage()),
),
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context)!.home),
actions: [
IconButton(
icon: const Icon(Icons.edit_rounded),
tooltip: L10n.of(context)!.customizeHomePage,
onPressed: () => navStore.push(
context,
MaterialPageRoute(builder: (context) => const HomeSettingsPage()),
),
IconButton(
icon: const Icon(Icons.settings_rounded),
tooltip: L10n.of(context)!.settings,
onPressed: () => navStore.push(
context,
MaterialPageRoute(builder: (context) => const SettingsPage()),
),
),
IconButton(
icon: const Icon(Icons.settings_rounded),
tooltip: L10n.of(context)!.settings,
onPressed: () => navStore.push(
context,
MaterialPageRoute(builder: (context) => const SettingsPage()),
),
],
),
body: Observer(
builder: (context) {
final songListIsEmpty = musicDataStore.songListIsEmpty;
if (songListIsEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.music_note_rounded, size: 64.0),
const SizedBox(height: 24.0),
Text(
L10n.of(context)!.noSongsYet,
style: TEXT_BIG,
textAlign: TextAlign.justify,
),
],
),
),
],
),
body: Observer(
builder: (context) {
final songListIsEmpty = musicDataStore.songListIsEmpty;
if (songListIsEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.music_note_rounded, size: 64.0),
const SizedBox(height: 24.0),
Text(
L10n.of(context)!.noSongsYet,
style: TEXT_BIG,
textAlign: TextAlign.justify,
),
],
),
);
}
final widgetEntities = store.homeWidgetsStream.value;
final List<Widget> widgets = [
const SliverPadding(
padding: EdgeInsets.only(top: 8.0),
),
];
);
}
for (final we in widgetEntities ?? <HomeWidgetRepr>[]) {
widgets.add(
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING - 8.0,
vertical: 8.0,
),
sliver: SliverToBoxAdapter(child: we.widget()),
),
);
}
final widgetEntities = store.homeWidgetsStream.value;
final List<Widget> widgets = [
const SliverPadding(
padding: EdgeInsets.only(top: 8.0),
),
];
for (final we in widgetEntities ?? <HomeWidgetRepr>[]) {
widgets.add(
const SliverPadding(
padding: EdgeInsets.only(bottom: 8.0),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING - 8.0,
vertical: 8.0,
),
sliver: SliverToBoxAdapter(child: we.widget()),
),
);
}
return Scrollbar(
child: CustomScrollView(
slivers: widgets,
),
);
},
),
widgets.add(
const SliverPadding(
padding: EdgeInsets.only(bottom: 8.0),
),
);
return Scrollbar(
child: CustomScrollView(
slivers: widgets,
),
);
},
),
);
}

View file

@ -23,100 +23,98 @@ class HomeSettingsPage extends StatelessWidget {
final homeStore = GetIt.I<HomePageStore>();
final navStore = GetIt.I<NavigationStore>();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.homeCustomization,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.add_rounded),
onPressed: () => _onTapAdd(context),
),
],
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.homeCustomization,
style: TEXT_HEADER,
),
body: Observer(
builder: (context) {
final widgetReprs = homeStore.homeWidgetsStream.value ?? <HomeWidgetRepr>[];
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.add_rounded),
onPressed: () => _onTapAdd(context),
),
],
),
body: Observer(
builder: (context) {
final widgetReprs = homeStore.homeWidgetsStream.value ?? <HomeWidgetRepr>[];
return Scrollbar(
child: CustomScrollView(
slivers: [
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(context, int index) {
return Dismissible(
key: UniqueKey(),
child: ListTile(
title: Text(widgetReprs[index].title(context)),
leading: Icon(widgetReprs[index].icon),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widgetReprs[index].hasParameters)
IconButton(
onPressed: () {
navStore.push(
context,
MaterialPageRoute(
builder: (context) => widgetReprs[index].formPage()!,
),
);
},
icon: const Icon(Icons.edit_rounded),
),
return Scrollbar(
child: CustomScrollView(
slivers: [
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(context, int index) {
return Dismissible(
key: UniqueKey(),
child: ListTile(
title: Text(widgetReprs[index].title(context)),
leading: Icon(widgetReprs[index].icon),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widgetReprs[index].hasParameters)
IconButton(
onPressed: () {
navStore.push(
context,
MaterialPageRoute(
builder: (context) => widgetReprs[index].formPage()!,
),
);
},
icon: const Icon(Icons.edit_rounded),
),
],
),
contentPadding: const EdgeInsets.fromLTRB(
HORIZONTAL_PADDING,
8.0,
0.0,
8.0,
),
),
onDismissed: (direction) {
homeStore.removeHomeWidget(widgetReprs[index].homeWidgetEntity);
},
background: Container(
width: double.infinity,
color: RED,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.delete_forever_rounded,
color: Colors.white,
),
Icon(
Icons.delete_forever_rounded,
color: Colors.white,
)
],
),
contentPadding: const EdgeInsets.fromLTRB(
HORIZONTAL_PADDING,
8.0,
0.0,
8.0,
),
),
onDismissed: (direction) {
homeStore.removeHomeWidget(widgetReprs[index].homeWidgetEntity);
},
background: Container(
width: double.infinity,
color: RED,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.delete_forever_rounded,
color: Colors.white,
),
Icon(
Icons.delete_forever_rounded,
color: Colors.white,
)
],
),
),
),
);
},
childCount: widgetReprs.length,
),
onReorder: (oldIndex, newIndex) {
homeStore.moveHomeWidget(oldIndex, newIndex);
),
);
},
childCount: widgetReprs.length,
),
],
),
);
},
),
onReorder: (oldIndex, newIndex) {
homeStore.moveHomeWidget(oldIndex, newIndex);
},
),
],
),
);
},
),
);
}

View file

@ -17,47 +17,45 @@ class LibraryFoldersPage extends StatelessWidget {
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
final NavigationStore navStore = GetIt.I<NavigationStore>();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.libraryFolders,
style: TEXT_HEADER,
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.libraryFolders,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.add_rounded),
onPressed: () => _openFilePicker(settingsStore),
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.add_rounded),
onPressed: () => _openFilePicker(settingsStore),
],
titleSpacing: 0.0,
),
body: Observer(
builder: (context) {
final folders = settingsStore.libraryFoldersStream.value ?? [];
return ListView.separated(
itemCount: folders.length,
itemBuilder: (_, int index) {
final String path = folders[index];
return ListTile(
title: Text(path),
trailing: IconButton(
icon: const Icon(Icons.delete_rounded),
onPressed: () => settingsStore.removeLibraryFolder(path),
),
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
],
titleSpacing: 0.0,
),
body: Observer(
builder: (context) {
final folders = settingsStore.libraryFoldersStream.value ?? [];
return ListView.separated(
itemCount: folders.length,
itemBuilder: (_, int index) {
final String path = folders[index];
return ListTile(
title: Text(path),
trailing: IconButton(
icon: const Icon(Icons.delete_rounded),
onPressed: () => settingsStore.removeLibraryFolder(path),
),
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
);
},
),
);
},
),
);
}

View file

@ -13,45 +13,45 @@ class LibraryTabContainer extends StatelessWidget {
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: SafeArea(
child: Column(
children: <Widget>[
Container(
color: Theme.of(context).primaryColor,
child: Padding(
padding: const EdgeInsets.only(top: 8.0, left: 4.0),
child: Row(
children: [
Expanded(
child: TabBar(
indicatorColor: Theme.of(context).highlightColor,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 3.0,
labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),
unselectedLabelColor: Colors.white30,
isScrollable: true,
tabs: [
Tab(text: L10n.of(context)!.artists),
Tab(text: L10n.of(context)!.albums),
Tab(text: L10n.of(context)!.songs),
Tab(text: L10n.of(context)!.playlists),
],
),
child: Scaffold(
appBar: AppBar(
toolbarHeight: 8.0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Align(
alignment: Alignment.centerLeft,
child: TabBar(
indicator: UnderlineTabIndicator(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(
color: Theme.of(context).highlightColor,
width: 3.0,
),
),
indicatorSize: TabBarIndicatorSize.label,
labelPadding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 3.0),
unselectedLabelColor: Colors.white30,
isScrollable: true,
dividerColor: Colors.transparent,
tabs: [
Tab(text: L10n.of(context)!.artists),
Tab(text: L10n.of(context)!.albums),
Tab(text: L10n.of(context)!.songs),
Tab(text: L10n.of(context)!.playlists),
],
),
),
),
const Expanded(
child: TabBarView(
children: <Widget>[
ArtistsPage(key: PageStorageKey('ArtistsPage')),
AlbumsPage(key: PageStorageKey('AlbumsPage')),
SongsPage(key: PageStorageKey('SongsPage')),
PlaylistsPage(key: PageStorageKey('PlaylistsPage'))
],
),
)
),
),
body: const TabBarView(
children: <Widget>[
ArtistsPage(key: PageStorageKey('ArtistsPage')),
AlbumsPage(key: PageStorageKey('AlbumsPage')),
SongsPage(key: PageStorageKey('SongsPage')),
PlaylistsPage(key: PageStorageKey('PlaylistsPage'))
],
),
),

View file

@ -8,8 +8,7 @@ import '../state/music_data_store.dart';
import '../state/navigation_store.dart';
import '../state/playlist_form_store.dart';
import '../theming.dart';
import '../widgets/playlist_cover.dart';
import 'cover_customization_page.dart';
import '../widgets/cover_customization_card.dart';
class PlaylistFormPage extends StatefulWidget {
const PlaylistFormPage({Key? key, this.playlist}) : super(key: key);
@ -52,159 +51,121 @@ class _PlaylistFormPageState extends State<PlaylistFormPage> {
L10n.of(context)!.favShuffleMode,
];
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
title,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
if (widget.playlist != null)
IconButton(
icon: const Icon(Icons.delete_rounded),
onPressed: () async {
// TODO: this works, but may only pop back to the smartlist page...
// can I use pop 2x here?
await musicDataStore.removePlaylist(widget.playlist!);
navStore.pop(context);
},
),
return Scaffold(
appBar: AppBar(
title: Text(
title,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
if (widget.playlist != null)
IconButton(
icon: const Icon(Icons.check_rounded),
icon: const Icon(Icons.delete_rounded),
onPressed: () async {
store.validateAll();
if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
}
// TODO: this works, but may only pop back to the smartlist page...
// can I use pop 2x here?
await musicDataStore.removePlaylist(widget.playlist!);
navStore.pop(context);
},
),
],
titleSpacing: 0.0,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Observer(
builder: (_) => TextFormField(
initialValue: store.name,
onChanged: (value) => store.name = value,
style: TEXT_HEADER,
decoration: InputDecoration(
labelText: L10n.of(context)!.name,
labelStyle: const TextStyle(color: Colors.white),
floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white),
errorText: store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null,
errorStyle: const TextStyle(color: RED),
filled: true,
fillColor: DARK35,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 12.0,
),
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
store.validateAll();
if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
}
},
),
],
titleSpacing: 0.0,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Observer(
builder: (_) => TextFormField(
initialValue: store.name,
onChanged: (value) => store.name = value,
style: TEXT_HEADER,
decoration: InputDecoration(
labelText: L10n.of(context)!.name,
labelStyle: const TextStyle(color: Colors.white),
floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white),
errorText:
store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null,
errorStyle: const TextStyle(color: RED),
filled: true,
fillColor: DARK35,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 12.0,
),
),
),
),
const SizedBox(height: 4.0),
Observer(
builder: (context) => Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
horizontal: 10.0,
),
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => CoverCustomizationPage(
store: store.cover,
),
const SizedBox(height: 4.0),
Observer(
builder: (context) => CoverCustomizationCard(store: store.cover),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
playbackModeTexts[value],
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
),
),
value: value,
groupValue: store.shuffleModeIndex,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.setShuffleModeIndex(newValue);
});
},
);
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
PlaylistCover(
size: 64.0,
gradient: store.cover.gradient,
icon: store.cover.icon,
),
const SizedBox(width: 16.0),
Text(L10n.of(context)!.customizeCover),
const Spacer(),
const SizedBox(
width: 56.0,
child: Icon(Icons.chevron_right_rounded),
),
const SizedBox(width: 6.0),
],
),
),
),
),
}).toList(),
);
},
),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
playbackModeTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
),
value: value,
groupValue: store.shuffleModeIndex,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.setShuffleModeIndex(newValue);
});
},
);
}).toList(),
);
},
),
),
),
],
),
),
],
),
],
),
),
],
),
),
),

View file

@ -19,6 +19,7 @@ import '../widgets/bottom_sheet/add_to_playlist.dart';
import '../widgets/bottom_sheet/remove_from_playlist.dart';
import '../widgets/cover_sliver_appbar.dart';
import '../widgets/custom_modal_bottom_sheet.dart';
import '../widgets/play_shuffle_button.dart';
import '../widgets/playlist_cover.dart';
import '../widgets/song_bottom_sheet.dart';
import '../widgets/song_list_tile.dart';
@ -156,31 +157,18 @@ class _PlaylistPageState extends State<PlaylistPage> {
),
],
title: playlist.name,
subtitle2: '${L10n.of(context)!.nSongs(songs.length).capitalize()}${utils.msToTimeString(totalDuration)}',
background: Container(
decoration: BoxDecoration(
gradient: playlist.gradient,
),
),
subtitle2:
'${L10n.of(context)!.nSongs(songs.length).capitalize()}${utils.msToTimeString(totalDuration)}',
backgroundColor: utils.bgColor(playlist.gradient.colors.first),
cover: PlaylistCover(
size: 120,
icon: playlist.icon,
gradient: playlist.gradient,
),
button: ElevatedButton(
button: PlayShuffleButton(
onPressed: () => audioStore.playPlaylist(playlist),
child: Row(
children: [
Expanded(child: Center(child: Text(L10n.of(context)!.play))),
Icon(playIcon),
],
),
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
backgroundColor: Colors.white10,
shadowColor: Colors.transparent,
),
shuffleMode: playlist.shuffleMode,
size: 56,
),
),
Observer(
@ -197,8 +185,6 @@ class _PlaylistPageState extends State<PlaylistPage> {
key: ValueKey(song.path),
child: SongListTile(
song: song,
showAlbum: true,
subtitle: Subtitle.artistAlbum,
onTap: () => audioStore.playSong(index, songs, playlist),
onTapMore: () => showModalBottomSheet(
context: context,

View file

@ -103,6 +103,7 @@ class _PlaylistsPageState extends State<PlaylistsPage> with AutomaticKeepAliveCl
),
floatingActionButton: SpeedDial(
child: const Icon(Icons.add_rounded),
backgroundColor: Theme.of(context).highlightColor,
activeChild: Transform.rotate(
angle: pi / 4,
child: const Icon(Icons.add_rounded),
@ -125,6 +126,7 @@ class _PlaylistsPageState extends State<PlaylistsPage> with AutomaticKeepAliveCl
builder: (BuildContext context) => const SmartListFormPage(),
),
),
shape: const CircleBorder(),
),
SpeedDialChild(
child: const Icon(Icons.playlist_add_rounded),
@ -137,6 +139,7 @@ class _PlaylistsPageState extends State<PlaylistsPage> with AutomaticKeepAliveCl
builder: (BuildContext context) => const PlaylistFormPage(),
),
),
shape: const CircleBorder(),
),
],
),

View file

@ -38,187 +38,181 @@ class QueuePage extends StatelessWidget {
final ScrollController _scrollController =
ScrollController(initialScrollOffset: initialIndex * 72.0);
return SafeArea(
child: Scaffold(
appBar: AppBar(
leading: Padding(
padding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
child: SizedBox(
width: 56.0,
child: IconButton(
icon: const Icon(Icons.expand_more_rounded),
onPressed: () => Navigator.pop(context),
),
return Scaffold(
appBar: AppBar(
leading: Padding(
padding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
child: SizedBox(
width: 56.0,
child: IconButton(
icon: const Icon(Icons.expand_more_rounded),
onPressed: () => Navigator.pop(context),
),
),
leadingWidth: 56 + HORIZONTAL_PADDING,
toolbarHeight: 80.0,
title: Observer(
builder: (context) {
final playable = audioStore.playableStream.value;
final numAvailableSongs = audioStore.numAvailableSongs;
Widget subTitle = Container();
if (playable != null) {
subTitle = Text(
playable.repr(context),
maxLines: 1,
);
}
String text = L10n.of(context)!.nSongsInQueue(audioStore.queueLength);
if (numAvailableSongs > 0) text += ', ${L10n.of(context)!.moreAvailable(numAvailableSongs)}';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
L10n.of(context)!.currentlyPlaying.toUpperCase(),
style: TEXT_SMALL_HEADLINE,
),
subTitle,
const SizedBox(height: 4.0),
Text(
text,
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white70),
),
],
);
},
),
centerTitle: false,
actions: [
Observer(
builder: (context) {
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => _openMultiselectMenu(context),
);
return Container();
},
),
Observer(
builder: (context) {
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
final isAllSelected = queuePageStore.isAllSelected;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: isAllSelected
? const Icon(Icons.deselect_rounded)
: const Icon(Icons.select_all_rounded),
onPressed: () {
if (isAllSelected)
queuePageStore.deselectAll();
else
queuePageStore.selectAll();
},
);
return Container();
},
),
Observer(builder: (context) {
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
return IconButton(
key: const ValueKey('QUEUE_MULTISELECT'),
icon: isMultiSelectEnabled
? const Icon(Icons.close_rounded)
: const Icon(Icons.checklist_rtl_rounded),
onPressed: () => queuePageStore.toggleMultiSelect(),
);
})
],
),
body: Observer(
builder: (BuildContext context) {
print('QueuePage.build -> Observer.build');
final queue = audioStore.queue;
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
final isSelected = queuePageStore.isSelected.toList();
leadingWidth: 56 + HORIZONTAL_PADDING,
toolbarHeight: 80.0,
title: Observer(
builder: (context) {
final playable = audioStore.playableStream.value;
final numAvailableSongs = audioStore.numAvailableSongs;
while (isSelected.length < queue.length) isSelected.add(false);
Widget subTitle = Container();
final int activeIndex = queueIndexStream.value ?? -1;
return Scrollbar(
child: CustomScrollView(
controller: _scrollController,
slivers: [
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(context, int index) {
final QueueItem? queueItem = queue[index];
if (queueItem == null) return Container();
return Dismissible(
key: ValueKey(queueItem.toString()),
child: SongListTile(
song: queueItem.song,
highlight: index == activeIndex,
source: queueItem.source,
isSelectEnabled: isMultiSelectEnabled,
isSelected: isMultiSelectEnabled && isSelected[index],
onTap: () async {
await audioStore.seekToIndex(index);
audioStore.play();
},
onTapMore: () => showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SongBottomSheet(
song: queueItem.song,
enableQueueActions: false,
numNavPop: 3,
),
),
onSelect: (bool selected) =>
queuePageStore.setSelected(selected, index),
),
background: Container(
width: double.infinity,
color: RED,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.playlist_remove_rounded,
color: Colors.white,
),
Icon(
Icons.playlist_remove_rounded,
color: Colors.white,
)
],
),
),
),
onDismissed: (direction) {
audioStore.removeQueueIndices([index]);
},
confirmDismiss: (direction) async => !isMultiSelectEnabled,
);
},
childCount: queue.length,
),
onReorder: (oldIndex, newIndex) => audioStore.moveQueueItem(oldIndex, newIndex),
)
],
),
if (playable != null) {
subTitle = Text(
playable.repr(),
maxLines: 1,
);
}
String text = L10n.of(context)!.nSongsInQueue(audioStore.queueLength);
if (numAvailableSongs > 0) text += ', ${L10n.of(context)!.moreAvailable(numAvailableSongs)}';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
subTitle,
const SizedBox(height: 4.0),
Text(
text,
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white70),
),
],
);
},
),
bottomNavigationBar: const QueueControlBar(),
centerTitle: false,
actions: [
Observer(
builder: (context) {
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => _openMultiselectMenu(context),
);
return Container();
},
),
Observer(
builder: (context) {
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
final isAllSelected = queuePageStore.isAllSelected;
if (isMultiSelectEnabled)
return IconButton(
key: GlobalKey(),
icon: isAllSelected
? const Icon(Icons.deselect_rounded)
: const Icon(Icons.select_all_rounded),
onPressed: () {
if (isAllSelected)
queuePageStore.deselectAll();
else
queuePageStore.selectAll();
},
);
return Container();
},
),
Observer(builder: (context) {
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
return IconButton(
key: const ValueKey('QUEUE_MULTISELECT'),
icon: isMultiSelectEnabled
? const Icon(Icons.close_rounded)
: const Icon(Icons.checklist_rtl_rounded),
onPressed: () => queuePageStore.toggleMultiSelect(),
);
})
],
),
body: Observer(
builder: (BuildContext context) {
print('QueuePage.build -> Observer.build');
final queue = audioStore.queue;
final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled;
final isSelected = queuePageStore.isSelected.toList();
while (isSelected.length < queue.length) isSelected.add(false);
final int activeIndex = queueIndexStream.value ?? -1;
return Scrollbar(
child: CustomScrollView(
controller: _scrollController,
slivers: [
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(context, int index) {
final QueueItem? queueItem = queue[index];
if (queueItem == null) return Container();
return Dismissible(
key: ValueKey(queueItem.toString()),
child: SongListTile(
song: queueItem.song,
highlight: index == activeIndex,
source: queueItem.source,
isSelectEnabled: isMultiSelectEnabled,
isSelected: isMultiSelectEnabled && isSelected[index],
onTap: () async {
await audioStore.seekToIndex(index);
audioStore.play();
},
onTapMore: () => showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SongBottomSheet(
song: queueItem.song,
enableQueueActions: false,
numNavPop: 3,
),
),
onSelect: (bool selected) =>
queuePageStore.setSelected(selected, index),
),
background: Container(
width: double.infinity,
color: RED,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(
Icons.playlist_remove_rounded,
color: Colors.white,
),
Icon(
Icons.playlist_remove_rounded,
color: Colors.white,
)
],
),
),
),
onDismissed: (direction) {
audioStore.removeQueueIndices([index]);
},
confirmDismiss: (direction) async => !isMultiSelectEnabled,
);
},
childCount: queue.length,
),
onReorder: (oldIndex, newIndex) => audioStore.moveQueueItem(oldIndex, newIndex),
)
],
),
);
},
),
bottomNavigationBar: const QueueControlBar(),
);
}

View file

@ -64,365 +64,358 @@ class _SearchPageState extends State<SearchPage> {
String searchText = '';
return SafeArea(
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 8.0,
left: HORIZONTAL_PADDING - 8.0,
right: HORIZONTAL_PADDING - 8.0,
bottom: 8.0,
),
child: StatefulBuilder(builder: (context, setState) {
return TextField(
controller: _textController,
decoration: InputDecoration(
hintText: L10n.of(context)!.search,
hintStyle: TEXT_HEADER.copyWith(color: Colors.white),
fillColor: Colors.white10,
filled: true,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0.0,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0.0,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
suffixIcon: searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
color: Colors.white,
onPressed: () {
setState(() {
searchText = '';
_textController.text = '';
});
searchStore.reset();
},
)
: const SizedBox.shrink(),
),
onChanged: (text) {
setState(() => searchText = text);
searchStore.search(text);
},
focusNode: _searchFocus,
);
}),
return Scaffold(
appBar: AppBar(
titleSpacing: 8.0,
title: Padding(
padding: const EdgeInsets.only(
top: 11.0,
bottom: 8.0,
),
child: StatefulBuilder(builder: (context, setState) {
return TextField(
controller: _textController,
decoration: InputDecoration(
hintText: L10n.of(context)!.search,
hintStyle: TEXT_HEADER.copyWith(color: Colors.white),
fillColor: Colors.white10,
filled: true,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0.0,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
Padding(
padding: const EdgeInsets.only(
left: HORIZONTAL_PADDING - 8.0,
right: HORIZONTAL_PADDING - 8.0,
bottom: 8.0,
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0.0,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
suffixIcon: searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
color: Colors.white,
onPressed: () {
setState(() {
searchText = '';
_textController.text = '';
});
searchStore.reset();
},
)
: const SizedBox.shrink(),
),
onChanged: (text) {
setState(() => searchText = text);
searchStore.search(text);
},
focusNode: _searchFocus,
);
}),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: Padding(
padding: const EdgeInsets.only(
left: HORIZONTAL_PADDING - 8.0,
right: HORIZONTAL_PADDING - 8.0,
bottom: 8.0,
),
child: Observer(
builder: (context) {
final artists = searchStore.searchResultsArtists;
final albums = searchStore.searchResultsAlbums;
final songs = searchStore.searchResultsSongs;
final smartlists = searchStore.searchResultsSmartLists;
final playlists = searchStore.searchResultsPlaylists;
final artistHeight =
artists.length * 56.0 + artists.isNotEmpty.toDouble() * (16.0 + 56.0);
final albumsHeight =
albums.length * 72.0 + albums.isNotEmpty.toDouble() * (16.0 + 56.0);
final songsHeight =
songs.length * 72.0 + songs.isNotEmpty.toDouble() * (16.0 + 56.0);
final smartListsHeight =
smartlists.length * 72.0 + smartlists.isNotEmpty.toDouble() * (16.0 + 56.0);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: artists.isEmpty
? null
: () => _scrollController.animateTo(
0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.person_rounded),
),
IconButton(
onPressed: albums.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.album_rounded),
),
IconButton(
onPressed: songs.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight + albumsHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.audiotrack_rounded),
),
IconButton(
onPressed: smartlists.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight + albumsHeight + songsHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.auto_awesome_rounded),
),
IconButton(
onPressed: playlists.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight + albumsHeight + songsHeight + smartListsHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.queue_music_rounded),
),
],
);
},
),
),
),
),
body: Observer(builder: (context) {
final artists = searchStore.searchResultsArtists;
final albums = searchStore.searchResultsAlbums;
final songs = searchStore.searchResultsSongs;
final smartlists = searchStore.searchResultsSmartLists;
final playlists = searchStore.searchResultsPlaylists;
if (_textController.text.isEmpty) {
return const Center(
child: Icon(
Icons.search_rounded,
size: 192,
color: Colors.white30,
),
);
}
return Scrollbar(
controller: _scrollController,
child: CustomScrollView(
controller: _scrollController,
slivers: [
if (artists.isNotEmpty) ...[
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.artists,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (final artist in artists)
ListTile(
title: Text(artist.name),
leading: const SizedBox(
child: Icon(Icons.person_rounded),
width: 56.0,
height: 56.0,
),
onTap: () {
_navStore.pushOnLibrary(
MaterialPageRoute<Widget>(
builder: (BuildContext context) => ArtistDetailsPage(
artist: artist,
),
),
);
},
),
const SizedBox(height: 16.0),
],
),
child: Observer(builder: (context) {
final artists = searchStore.searchResultsArtists;
final albums = searchStore.searchResultsAlbums;
final songs = searchStore.searchResultsSongs;
final smartlists = searchStore.searchResultsSmartLists;
final playlists = searchStore.searchResultsPlaylists;
final artistHeight =
artists.length * 56.0 + artists.isNotEmpty.toDouble() * (16.0 + 56.0);
final albumsHeight =
albums.length * 72.0 + albums.isNotEmpty.toDouble() * (16.0 + 56.0);
final songsHeight =
songs.length * 56.0 + songs.isNotEmpty.toDouble() * (16.0 + 56.0);
final smartListsHeight =
smartlists.length * 56.0 + smartlists.isNotEmpty.toDouble() * (16.0 + 56.0);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: artists.isEmpty
? null
: () => _scrollController.animateTo(
0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.person_rounded),
),
IconButton(
onPressed: albums.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.album_rounded),
),
IconButton(
onPressed: songs.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight + albumsHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.audiotrack_rounded),
),
IconButton(
onPressed: smartlists.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight + albumsHeight + songsHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.auto_awesome_rounded),
),
IconButton(
onPressed: playlists.isEmpty
? null
: () => _scrollController.animateTo(
artistHeight + albumsHeight + songsHeight + smartListsHeight,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
icon: const Icon(Icons.queue_music_rounded),
),
],
);
}),
),
],
),
),
Expanded(
child: Observer(builder: (context) {
final artists = searchStore.searchResultsArtists;
final albums = searchStore.searchResultsAlbums;
final songs = searchStore.searchResultsSongs;
final smartlists = searchStore.searchResultsSmartLists;
final playlists = searchStore.searchResultsPlaylists;
if (_textController.text.isEmpty) {
return const Center(
child: Icon(
Icons.search_rounded,
size: 192,
color: Colors.white30,
if (albums.isNotEmpty) ...[
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.albums,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (final album in albums)
AlbumArtListTile(
title: album.title,
subtitle: album.artist,
albumArtPath: album.albumArtPath,
onTap: () {
_navStore.pushOnLibrary(
MaterialPageRoute<Widget>(
builder: (BuildContext context) => AlbumDetailsPage(
album: album,
),
),
);
},
),
const SizedBox(height: 16.0),
],
),
);
}
return Scrollbar(
controller: _scrollController,
child: CustomScrollView(
controller: _scrollController,
slivers: [
if (artists.isNotEmpty) ...[
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.artists,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (final artist in artists)
ListTile(
title: Text(artist.name),
leading: const SizedBox(
child: Icon(Icons.person_rounded),
width: 56.0,
height: 56.0,
),
onTap: () {
_navStore.pushOnLibrary(
MaterialPageRoute<Widget>(
builder: (BuildContext context) => ArtistDetailsPage(
artist: artist,
),
),
);
},
),
const SizedBox(height: 16.0),
],
),
),
],
if (albums.isNotEmpty) ...[
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.albums,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (final album in albums)
AlbumArtListTile(
title: album.title,
subtitle: album.artist,
albumArtPath: album.albumArtPath,
onTap: () {
_navStore.pushOnLibrary(
MaterialPageRoute<Widget>(
builder: (BuildContext context) => AlbumDetailsPage(
album: album,
),
),
);
},
),
const SizedBox(height: 16.0),
],
),
),
],
if (songs.isNotEmpty) ...[
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.songs,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (int i in songs.asMap().keys)
SongListTile(
song: songs[i],
onTap: () => audioStore.playSong(
i,
songs,
SearchQuery(searchText),
),
onTapMore: () => showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SongBottomSheet(
song: songs[i],
),
),
onSelect: () {},
),
const SizedBox(height: 16.0),
],
),
),
],
if (smartlists.isNotEmpty)
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.smartlists,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (int i in smartlists.asMap().keys)
ListTile(
title: Text(smartlists[i].name),
leading: PlaylistCover(
size: 56,
gradient: smartlists[i].gradient,
icon: smartlists[i].icon,
),
onTap: () => _navStore.pushOnLibrary(
MaterialPageRoute(
builder: (BuildContext context) =>
SmartListPage(smartList: smartlists[i]),
),
),
trailing: PlayShuffleButton(
size: 48.0,
shuffleMode: smartlists[i].shuffleMode,
onPressed: () => audioStore.playSmartList(smartlists[i]),
),
),
const SizedBox(height: 16.0),
],
),
),
if (playlists.isNotEmpty)
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.playlists,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (int i in playlists.asMap().keys)
ListTile(
title: Text(playlists[i].name),
leading: PlaylistCover(
size: 56,
gradient: playlists[i].gradient,
icon: playlists[i].icon,
),
onTap: () => _navStore.pushOnLibrary(
MaterialPageRoute(
builder: (BuildContext context) =>
PlaylistPage(playlist: playlists[i]),
),
),
trailing: PlayShuffleButton(
size: 48.0,
onPressed: () => audioStore.playPlaylist(playlists[i]),
shuffleMode: playlists[i].shuffleMode,
),
),
const SizedBox(height: 16.0),
],
),
),
],
),
);
}),
],
if (songs.isNotEmpty) ...[
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.songs,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (int i in songs.asMap().keys)
SongListTile(
song: songs[i],
onTap: () => audioStore.playSong(
i,
songs,
SearchQuery(searchText),
),
onTapMore: () => showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => SongBottomSheet(
song: songs[i],
),
),
onSelect: () {},
),
const SizedBox(height: 16.0),
],
),
),
],
if (smartlists.isNotEmpty)
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.smartlists,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (int i in smartlists.asMap().keys)
ListTile(
title: Text(smartlists[i].name),
leading: PlaylistCover(
size: 56,
gradient: smartlists[i].gradient,
icon: smartlists[i].icon,
),
onTap: () => _navStore.pushOnLibrary(
MaterialPageRoute(
builder: (BuildContext context) =>
SmartListPage(smartList: smartlists[i]),
),
),
trailing: PlayShuffleButton(
size: 40.0,
shuffleMode: smartlists[i].shuffleMode,
onPressed: () => audioStore.playSmartList(smartlists[i]),
),
contentPadding: const EdgeInsets.fromLTRB(HORIZONTAL_PADDING, 8.0, 4.0, 8.0),
),
const SizedBox(height: 16.0),
],
),
),
if (playlists.isNotEmpty)
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(
L10n.of(context)!.playlists,
style: TEXT_HEADER.underlined(
textColor: Colors.white,
underlineColor: LIGHT1,
thickness: 4,
distance: 8,
),
),
),
for (int i in playlists.asMap().keys)
ListTile(
title: Text(playlists[i].name),
leading: PlaylistCover(
size: 56,
gradient: playlists[i].gradient,
icon: playlists[i].icon,
),
onTap: () => _navStore.pushOnLibrary(
MaterialPageRoute(
builder: (BuildContext context) =>
PlaylistPage(playlist: playlists[i]),
),
),
trailing: PlayShuffleButton(
size: 40.0,
onPressed: () => audioStore.playPlaylist(playlists[i]),
shuffleMode: playlists[i].shuffleMode,
),
contentPadding: const EdgeInsets.fromLTRB(HORIZONTAL_PADDING, 8.0, 4.0, 8.0),
),
const SizedBox(height: 16.0),
],
),
),
],
),
],
),
);
}),
);
}
}

View file

@ -24,154 +24,152 @@ class SettingsPage extends StatelessWidget {
final TextEditingController _textController = TextEditingController();
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.settings,
style: TEXT_HEADER,
),
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
centerTitle: true,
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.settings,
style: TEXT_HEADER,
),
body: ListView(
children: [
SettingsSection(text: L10n.of(context)!.library),
ListTile(
title: Text(L10n.of(context)!.updateLibrary),
subtitle: Observer(builder: (_) {
final int artistCount = musicDataStore.artistStream.value?.length ?? 0;
final int albumCount = musicDataStore.albumStream.value?.length ?? 0;
final int songCount = musicDataStore.songStream.value?.length ?? 0;
return Text(
L10n.of(context)!.artistsAlbumsSongs(artistCount, albumCount, songCount),
);
}),
onTap: () => musicDataStore.updateDatabase(),
trailing: Observer(builder: (_) {
if (musicDataStore.isUpdatingDatabase) {
return const CircularProgressIndicator();
}
return Container(
height: 0,
width: 0,
);
}),
),
const Divider(
height: 4.0,
),
ListTile(
title: Text(L10n.of(context)!.manageLibraryFolders),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => navStore.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => const LibraryFoldersPage(),
),
leading: IconButton(
icon: const Icon(Icons.chevron_left_rounded),
onPressed: () => navStore.pop(context),
),
centerTitle: true,
),
body: ListView(
children: [
SettingsSection(text: L10n.of(context)!.library),
ListTile(
title: Text(L10n.of(context)!.updateLibrary),
subtitle: Observer(builder: (_) {
final int artistCount = musicDataStore.artistStream.value?.length ?? 0;
final int albumCount = musicDataStore.albumStream.value?.length ?? 0;
final int songCount = musicDataStore.songStream.value?.length ?? 0;
return Text(
L10n.of(context)!.artistsAlbumsSongs(artistCount, albumCount, songCount),
);
}),
onTap: () => musicDataStore.updateDatabase(),
trailing: Observer(builder: (_) {
if (musicDataStore.isUpdatingDatabase) {
return const CircularProgressIndicator();
}
return Container(
height: 0,
width: 0,
);
}),
),
const Divider(
height: 4.0,
),
ListTile(
title: Text(L10n.of(context)!.manageLibraryFolders),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => navStore.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => const LibraryFoldersPage(),
),
),
const Divider(
height: 4.0,
),
const Divider(
height: 4.0,
),
ListTile(
title: Text(L10n.of(context)!.allowedFileExtensions),
subtitle: Text(
L10n.of(context)!.allowedFileExtensionsDescription,
style: TEXT_SMALL_SUBTITLE,
),
ListTile(
title: Text(L10n.of(context)!.allowedFileExtensions),
subtitle: Text(
L10n.of(context)!.allowedFileExtensionsDescription,
style: TEXT_SMALL_SUBTITLE,
),
),
Padding(
padding: const EdgeInsets.only(
left: HORIZONTAL_PADDING,
right: HORIZONTAL_PADDING,
bottom: 16.0,
),
Padding(
padding: const EdgeInsets.only(
left: HORIZONTAL_PADDING,
right: HORIZONTAL_PADDING,
bottom: 16.0,
),
child: Observer(
builder: (context) {
final text = settingsStore.fileExtensionsStream.value;
if (text == null) return Container();
if (_textController.text == '') _textController.text = text;
child: Observer(
builder: (context) {
final text = settingsStore.fileExtensionsStream.value;
if (text == null) return Container();
if (_textController.text == '') _textController.text = text;
return Row(
children: [
Expanded(
child: TextFormField(
controller: _textController,
onChanged: (value) => settingsStore.setFileExtensions(value),
textAlign: TextAlign.start,
decoration: const InputDecoration(
filled: true,
fillColor: DARK35,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
errorStyle: TextStyle(height: 0, fontSize: 0),
contentPadding: EdgeInsets.only(
top: 0.0,
bottom: 0.0,
left: 6.0,
right: 6.0,
),
return Row(
children: [
Expanded(
child: TextFormField(
controller: _textController,
onChanged: (value) => settingsStore.setFileExtensions(value),
textAlign: TextAlign.start,
decoration: const InputDecoration(
filled: true,
fillColor: DARK35,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
errorStyle: TextStyle(height: 0, fontSize: 0),
contentPadding: EdgeInsets.only(
top: 0.0,
bottom: 0.0,
left: 6.0,
right: 6.0,
),
),
),
const SizedBox(width: 8.0),
IconButton(
icon: const Icon(Icons.settings_backup_restore_rounded),
onPressed: () {
_textController.text = ALLOWED_FILE_EXTENSIONS;
settingsStore.setFileExtensions(ALLOWED_FILE_EXTENSIONS);
},
),
],
);
},
),
),
const Divider(
height: 4.0,
),
Observer(
builder: (_) {
final blockedFiles = settingsStore.numBlockedFiles;
return ListTile(
title: Text(L10n.of(context)!.manageBlockedFiles),
subtitle: Text(L10n.of(context)!.numberOfBlockedFiles(blockedFiles)),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => navStore.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => const BlockedFilesPage(),
),
),
const SizedBox(width: 8.0),
IconButton(
icon: const Icon(Icons.settings_backup_restore_rounded),
onPressed: () {
_textController.text = ALLOWED_FILE_EXTENSIONS;
settingsStore.setFileExtensions(ALLOWED_FILE_EXTENSIONS);
},
),
],
);
},
),
SettingsSection(text: L10n.of(context)!.playback),
Observer(
builder: (context) => SwitchListTile(
value: settingsStore.playAlbumsInOrderStream.value ?? false,
onChanged: settingsStore.setPlayAlbumsInOrder,
title: Text(L10n.of(context)!.playAlbumsInOrder),
subtitle: Text(
L10n.of(context)!.playAlbumsInOrderDescription,
style: TEXT_SMALL_SUBTITLE,
),
const Divider(
height: 4.0,
),
Observer(
builder: (_) {
final blockedFiles = settingsStore.numBlockedFiles;
return ListTile(
title: Text(L10n.of(context)!.manageBlockedFiles),
subtitle: Text(L10n.of(context)!.numberOfBlockedFiles(blockedFiles)),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => navStore.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => const BlockedFilesPage(),
),
),
isThreeLine: true,
);
},
),
SettingsSection(text: L10n.of(context)!.playback),
Observer(
builder: (context) => SwitchListTile(
value: settingsStore.playAlbumsInOrderStream.value ?? false,
onChanged: settingsStore.setPlayAlbumsInOrder,
title: Text(L10n.of(context)!.playAlbumsInOrder),
subtitle: Text(
L10n.of(context)!.playAlbumsInOrderDescription,
style: TEXT_SMALL_SUBTITLE,
),
isThreeLine: true,
),
const Divider(
height: 4.0,
),
PercentageSlider(settingsStore),
],
),
),
const Divider(
height: 4.0,
),
PercentageSlider(settingsStore),
],
),
);
}

View file

@ -12,9 +12,8 @@ import '../state/music_data_store.dart';
import '../state/navigation_store.dart';
import '../state/smart_list_form_store.dart';
import '../theming.dart';
import '../widgets/playlist_cover.dart';
import '../widgets/cover_customization_card.dart';
import '../widgets/switch_text_listtile.dart';
import 'cover_customization_page.dart';
import 'smart_lists_artists_page.dart';
class SmartListFormPage extends StatefulWidget {
@ -67,489 +66,457 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
L10n.of(context)!.favShuffleMode,
];
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
title,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
if (widget.smartList != null)
IconButton(
icon: const Icon(Icons.delete_rounded),
onPressed: () async {
// TODO: this works, but may only pop back to the smartlist page...
// can I use pop 2x here?
await musicDataStore.removeSmartList(widget.smartList!);
navStore.pop(context);
},
),
return Scaffold(
appBar: AppBar(
title: Text(
title,
style: TEXT_HEADER,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => navStore.pop(context),
),
actions: [
if (widget.smartList != null)
IconButton(
icon: const Icon(Icons.check_rounded),
icon: const Icon(Icons.delete_rounded),
onPressed: () async {
store.validateAll();
if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
}
// TODO: this works, but may only pop back to the smartlist page...
// can I use pop 2x here?
await musicDataStore.removeSmartList(widget.smartList!);
navStore.pop(context);
},
),
],
titleSpacing: 0.0,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Observer(
builder: (_) => TextFormField(
initialValue: store.name,
onChanged: (value) => store.name = value,
style: TEXT_HEADER,
decoration: InputDecoration(
labelText: L10n.of(context)!.name,
labelStyle: const TextStyle(color: Colors.white),
floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white),
errorText: store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null,
errorStyle: const TextStyle(color: RED),
filled: true,
fillColor: DARK35,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 12.0,
),
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
store.validateAll();
if (!store.error.hasErrors) {
await store.save();
navStore.pop(context);
}
},
),
],
titleSpacing: 0.0,
),
body: ListTileTheme(
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
child: Scrollbar(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Observer(
builder: (_) => TextFormField(
initialValue: store.name,
onChanged: (value) => store.name = value,
style: TEXT_HEADER,
decoration: InputDecoration(
labelText: L10n.of(context)!.name,
labelStyle: const TextStyle(color: Colors.white),
floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white),
errorText:
store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null,
errorStyle: const TextStyle(color: RED),
filled: true,
fillColor: DARK35,
border: const OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 12.0,
),
),
),
),
const SizedBox(height: 4.0),
Observer(
builder: (context) => Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
horizontal: 10.0,
),
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => CoverCustomizationPage(
store: store.cover,
),
const SizedBox(height: 4.0),
Observer(
builder: (context) => CoverCustomizationCard(store: store.cover),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.filterSettings, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2.0,
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
final RangeValues _currentRangeValues = RangeValues(
store.minLikeCount.toDouble(),
store.maxLikeCount.toDouble(),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
left: 10.0,
top: 4.0,
),
child: Text(
L10n.of(context)!.filterLikes(
store.minLikeCount,
store.maxLikeCount,
),
),
),
RangeSlider(
values: _currentRangeValues,
min: 0,
max: MAX_LIKE_COUNT.toDouble(),
divisions: MAX_LIKE_COUNT,
labels: RangeLabels(
_currentRangeValues.start.round().toString(),
_currentRangeValues.end.round().toString(),
),
onChanged: (RangeValues values) {
store.minLikeCount = values.start.toInt();
store.maxLikeCount = values.end.toInt();
},
),
],
);
},
),
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Column(
children: [
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.minPlayCount,
switchValue: store.minPlayCountEnabled,
onSwitchChanged: (bool value) {
store.minPlayCountEnabled = value;
},
textValue: store.minPlayCount,
onTextChanged: (String value) {
store.minPlayCount = value;
},
);
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
PlaylistCover(
size: 64.0,
gradient: store.cover.gradient,
icon: store.cover.icon,
),
const SizedBox(height: CARD_SPACING),
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.maxPlayCount,
switchValue: store.maxPlayCountEnabled,
onSwitchChanged: (bool value) {
store.maxPlayCountEnabled = value;
},
textValue: store.maxPlayCount,
onTextChanged: (String value) {
store.maxPlayCount = value;
},
);
},
),
],
),
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
blockLevelTexts[value],
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
),
const SizedBox(width: 16.0),
Text(L10n.of(context)!.customizeCover),
const Spacer(),
const SizedBox(
width: 56.0,
child: Icon(Icons.chevron_right_rounded),
),
value: value,
groupValue: store.blockLevel,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.blockLevel = newValue;
});
},
);
}).toList(),
);
},
),
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Column(
children: [
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.minYear,
switchValue: store.minYearEnabled,
onSwitchChanged: (bool value) {
store.minYearEnabled = value;
},
textValue: store.minYear,
onTextChanged: (String value) {
store.minYear = value;
},
);
},
),
const SizedBox(height: CARD_SPACING),
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.maxYear,
switchValue: store.maxYearEnabled,
onSwitchChanged: (bool value) {
store.maxYearEnabled = value;
},
textValue: store.maxYear,
onTextChanged: (String value) {
store.maxYear = value;
},
);
},
),
],
),
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Column(
children: [
Observer(
builder: (_) => GestureDetector(
onTap: () {
navStore.push(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) =>
SmartListArtistsPage(formStore: store),
),
const SizedBox(width: 6.0),
],
);
},
child: Padding(
padding: const EdgeInsets.only(
left: 6.0,
right: 8.0,
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
store.excludeArtists
? L10n.of(context)!.selectArtistsExclude(
store.selectedArtists.length)
: L10n.of(context)!.selectArtistsInclude(
store.selectedArtists.length),
),
Text(
L10n.of(context)!.includeAllArtists,
style: TEXT_SMALL_SUBTITLE.copyWith(
color: Colors.white70,
),
),
],
),
),
),
const SizedBox(
width: 56.0,
child: Icon(Icons.chevron_right_rounded),
),
],
),
),
),
),
),
),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.filterSettings, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2.0,
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
final RangeValues _currentRangeValues = RangeValues(
store.minLikeCount.toDouble(),
store.maxLikeCount.toDouble(),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: CARD_SPACING),
Padding(
padding: const EdgeInsets.only(
left: 8.0 - 4.0,
right: 8.0,
),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(
left: 10.0,
top: 4.0,
),
child: Text(
L10n.of(context)!
.filterLikes(store.minLikeCount, store.maxLikeCount),
SizedBox(
width: 60.0,
child: Observer(
builder: (_) {
return Switch(
value: store.excludeArtists,
onChanged: (bool value) => store.excludeArtists = value,
);
},
),
),
RangeSlider(
values: _currentRangeValues,
min: 0,
max: MAX_LIKE_COUNT.toDouble(),
divisions: MAX_LIKE_COUNT,
labels: RangeLabels(
_currentRangeValues.start.round().toString(),
_currentRangeValues.end.round().toString(),
),
onChanged: (RangeValues values) {
store.minLikeCount = values.start.toInt();
store.maxLikeCount = values.end.toInt();
},
),
const SizedBox(width: 6.0),
Text(L10n.of(context)!.excludeArtists),
const Spacer(),
],
);
},
),
),
),
],
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Column(
children: [
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.minPlayCount,
switchValue: store.minPlayCountEnabled,
onSwitchChanged: (bool value) {
store.minPlayCountEnabled = value;
},
textValue: store.minPlayCount,
onTextChanged: (String value) {
store.minPlayCount = value;
},
);
},
),
const SizedBox(height: CARD_SPACING),
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.maxPlayCount,
switchValue: store.maxPlayCountEnabled,
onSwitchChanged: (bool value) {
store.maxPlayCountEnabled = value;
},
textValue: store.maxPlayCount,
onTextChanged: (String value) {
store.maxPlayCount = value;
},
);
},
),
],
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.limitSongs,
switchValue: store.limitEnabled,
onSwitchChanged: (bool value) {
store.limitEnabled = value;
},
textValue: store.limit,
onTextChanged: (String value) {
store.limit = value;
},
);
},
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
blockLevelTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
),
value: value,
groupValue: store.blockLevel,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.blockLevel = newValue;
});
},
);
}).toList(),
);
},
),
),
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.orderSettings, style: TEXT_HEADER),
subtitle: Text(L10n.of(context)!.orderSettingsDescription),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
horizontal: 8.0,
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Column(
children: [
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.minYear,
switchValue: store.minYearEnabled,
onSwitchChanged: (bool value) {
store.minYearEnabled = value;
},
textValue: store.minYear,
onTextChanged: (String value) {
store.minYear = value;
},
);
},
),
const SizedBox(height: CARD_SPACING),
Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.maxYear,
switchValue: store.maxYearEnabled,
onSwitchChanged: (bool value) {
store.maxYearEnabled = value;
},
textValue: store.maxYear,
onTextChanged: (String value) {
store.maxYear = value;
},
);
},
),
],
),
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Column(
children: [
Observer(
builder: (_) => GestureDetector(
onTap: () {
navStore.push(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) =>
SmartListArtistsPage(formStore: store),
),
);
},
child: Observer(
builder: (_) => ReorderableColumn(
children: List<Widget>.generate(
store.orderState.length,
(i) => Padding(
key: ValueKey(i),
padding: const EdgeInsets.symmetric(vertical: 1.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
color: DARK25,
),
child: Padding(
padding: const EdgeInsets.only(
left: 6.0,
right: HORIZONTAL_PADDING,
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING - 8.0,
vertical: 12.0,
),
child: Row(
children: [
const SizedBox(width: 60.0 + 6.0, height: 48.0),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
store.excludeArtists
? L10n.of(context)!.selectArtistsExclude(
store.selectedArtists.length)
: L10n.of(context)!.selectArtistsInclude(
store.selectedArtists.length),
),
Text(
L10n.of(context)!.includeAllArtists,
style: TEXT_SMALL_SUBTITLE.copyWith(
color: Colors.white70,
),
),
],
Switch(
value: store.orderState[i].enabled,
onChanged: (bool value) =>
store.setOrderEnabled(i, value),
),
const SizedBox(width: 6.0),
Text(store.orderState[i].orderCriterion.toText(context)),
const Spacer(),
const SizedBox(
width: 56.0,
child: Icon(Icons.chevron_right_rounded),
IconButton(
icon: store.orderState[i].orderDirection ==
OrderDirection.ascending
? const Icon(Icons.arrow_upward_rounded)
: const Icon(Icons.arrow_downward_rounded),
padding: const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 8.0,
),
visualDensity: VisualDensity.compact,
onPressed: () => store.toggleOrderDirection(i),
),
],
),
),
),
),
const SizedBox(height: CARD_SPACING),
Padding(
padding: const EdgeInsets.only(
left: 6.0,
right: HORIZONTAL_PADDING,
),
child: Row(
children: [
SizedBox(
width: 60.0,
child: Observer(
builder: (_) {
return Switch(
value: store.excludeArtists,
onChanged: (bool value) => store.excludeArtists = value,
);
},
),
),
const SizedBox(width: 6.0),
Text(L10n.of(context)!.excludeArtists),
const Spacer(),
],
),
),
],
),
onReorder: (oldIndex, newIndex) =>
store.reorderOrderState(oldIndex, newIndex),
),
),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return SwitchTextListTile(
title: L10n.of(context)!.limitSongs,
switchValue: store.limitEnabled,
onSwitchChanged: (bool value) {
store.limitEnabled = value;
},
textValue: store.limit,
onTextChanged: (String value) {
store.limit = value;
},
);
},
),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
),
const SizedBox(height: 16.0),
ListTile(
title: Text(L10n.of(context)!.orderSettings, style: TEXT_HEADER),
subtitle: Text(L10n.of(context)!.orderSettingsDescription),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
horizontal: 8.0,
),
child: Observer(
builder: (_) => ReorderableColumn(
children: List<Widget>.generate(
store.orderState.length,
(i) => Padding(
key: ValueKey(i),
padding: const EdgeInsets.symmetric(vertical: 1.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
color: DARK25,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING - 8.0,
vertical: 12.0,
),
child: Row(
children: [
Switch(
value: store.orderState[i].enabled,
onChanged: (bool value) =>
store.setOrderEnabled(i, value),
),
const SizedBox(width: 6.0),
Text(store.orderState[i].orderCriterion.toText(context)),
const Spacer(),
IconButton(
icon: store.orderState[i].orderDirection ==
OrderDirection.ascending
? const Icon(Icons.arrow_upward_rounded)
: const Icon(Icons.arrow_downward_rounded),
padding: const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 8.0,
),
visualDensity: VisualDensity.compact,
onPressed: () => store.toggleOrderDirection(i),
),
],
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
playbackModeTexts[value],
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
),
),
),
),
onReorder: (oldIndex, newIndex) =>
store.reorderOrderState(oldIndex, newIndex),
),
),
value: value,
groupValue: store.shuffleModeIndex,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.setShuffleModeIndex(newValue);
});
},
);
}).toList(),
);
},
),
),
const SizedBox(height: 8.0),
ListTile(
title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER),
),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: CARD_PADDING,
),
child: Observer(
builder: (_) {
return Column(
children: <int>[0, 1, 2, 3].map<RadioListTile<int>>((int value) {
return RadioListTile<int>(
title: Text(
playbackModeTexts[value],
style: const TextStyle(
fontSize: 14.0,
),
),
value: value,
groupValue: store.shuffleModeIndex,
onChanged: (int? newValue) {
setState(() {
if (newValue != null) store.setShuffleModeIndex(newValue);
});
},
);
}).toList(),
);
},
),
),
),
],
),
),
],
),
],
),
),
],
),
),
),

View file

@ -12,11 +12,11 @@ import '../state/audio_store.dart';
import '../state/music_data_store.dart';
import '../state/navigation_store.dart';
import '../state/smart_list_page_store.dart';
import '../theming.dart';
import '../utils.dart' as utils;
import '../widgets/bottom_sheet/add_to_playlist.dart';
import '../widgets/cover_sliver_appbar.dart';
import '../widgets/custom_modal_bottom_sheet.dart';
import '../widgets/play_shuffle_button.dart';
import '../widgets/playlist_cover.dart';
import '../widgets/song_bottom_sheet.dart';
import '../widgets/song_list_tile.dart';
@ -153,30 +153,18 @@ class _SmartListPageState extends State<SmartListPage> {
),
],
title: smartList.name,
subtitle2: '${L10n.of(context)!.nSongs(songs.length).capitalize()}${utils.msToTimeString(totalDuration)}',
background: Container(
decoration: BoxDecoration(
gradient: smartList.gradient,
),
),
subtitle2:
'${L10n.of(context)!.nSongs(songs.length).capitalize()}${utils.msToTimeString(totalDuration)}',
backgroundColor: utils.bgColor(smartList.gradient.colors.first),
cover: PlaylistCover(
size: 120,
icon: smartList.icon,
gradient: smartList.gradient,
),
button: ElevatedButton(
button: PlayShuffleButton(
onPressed: () => audioStore.playSmartList(smartList),
child: Row(
children: [
Expanded(child: Center(child: Text(L10n.of(context)!.play))),
Icon(playIcon),
],
),
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
backgroundColor: LIGHT1,
),
shuffleMode: smartList.shuffleMode,
size: 56,
),
),
Observer(
@ -190,8 +178,6 @@ class _SmartListPageState extends State<SmartListPage> {
final Song song = songs[index];
return SongListTile(
song: song,
showAlbum: true,
subtitle: Subtitle.artistAlbum,
isSelectEnabled: isMultiSelectEnabled,
isSelected: isMultiSelectEnabled && isSelected[index],
onTap: () => audioStore.playSong(index, songs, smartList),

View file

@ -21,61 +21,59 @@ class SmartListArtistsPage extends StatelessWidget {
final NavigationStore navStore = GetIt.I<NavigationStore>();
final initialSet = Set<Artist>.from(store.selectedArtists);
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.selectArtists,
style: TEXT_HEADER,
),
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
store.selectedArtists.clear();
store.selectedArtists.addAll(initialSet);
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context)!.selectArtists,
style: TEXT_HEADER,
),
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
store.selectedArtists.clear();
store.selectedArtists.addAll(initialSet);
navStore.pop(context);
},
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
navStore.pop(context);
},
),
actions: [
IconButton(
icon: const Icon(Icons.check_rounded),
onPressed: () async {
navStore.pop(context);
},
),
],
titleSpacing: 0.0,
),
body: Observer(builder: (_) {
final List<Artist> artists = musicDataStore.artistStream.value ?? [];
final selectedArtists = store.selectedArtists.toSet();
return Scrollbar(
child: ListView.separated(
itemCount: artists.length,
itemBuilder: (_, int index) {
final Artist artist = artists[index];
return CheckboxListTile(
title: Text(artist.name),
value: selectedArtists.contains(artist),
onChanged: (bool? value) {
if (value != null) {
if (value) {
store.addArtist(artist);
} else {
store.removeArtist(artist);
}
}
},
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
),
);
}),
],
titleSpacing: 0.0,
),
body: Observer(builder: (_) {
final List<Artist> artists = musicDataStore.artistStream.value ?? [];
final selectedArtists = store.selectedArtists.toSet();
return Scrollbar(
child: ListView.separated(
itemCount: artists.length,
itemBuilder: (_, int index) {
final Artist artist = artists[index];
return CheckboxListTile(
title: Text(artist.name),
value: selectedArtists.contains(artist),
onChanged: (bool? value) {
if (value != null) {
if (value) {
store.addArtist(artist);
} else {
store.removeArtist(artist);
}
}
},
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
),
);
}),
);
}
}

View file

@ -37,15 +37,13 @@ class _SongsPageState extends State<SongsPage> with AutomaticKeepAliveClientMixi
final List<Song> songs = songStream.value ?? [];
return Scrollbar(
controller: _scrollController,
child: ListView.separated(
child: ListView.builder(
controller: _scrollController,
itemCount: songs.length,
itemBuilder: (_, int index) {
final Song song = songs[index];
return SongListTile(
song: song,
showAlbum: true,
subtitle: Subtitle.artistAlbum,
onTap: () => audioStore.playSong(index, songs, AllSongs()),
onTapMore: () => showModalBottomSheet(
context: context,
@ -59,9 +57,6 @@ class _SongsPageState extends State<SongsPage> with AutomaticKeepAliveClientMixi
onSelect: () {},
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(
height: 4.0,
),
),
);
case StreamStatus.waiting:

View file

@ -138,4 +138,9 @@ abstract class _MusicDataStore with Store {
Future<void> removeSmartList(SmartList smartList) async {
await _musicDataRepository.removeSmartList(smartList);
}
Future<List<bool>> isSongFirstLast(Song? song) async {
if (song == null) return Future.value([false, false]);
return await _musicDataRepository.isSongFirstLast(song);
}
}

View file

@ -14,17 +14,17 @@ const Color RED = Colors.red;
const double HORIZONTAL_PADDING = 16.0;
ThemeData theme() => ThemeData(
// useMaterial3: true,
useMaterial3: true,
colorScheme: const ColorScheme(
primary: DARK2,
secondary: LIGHT2,
secondary: LIGHT1,
surface: DARK3,
background: DARK2,
error: RED,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Colors.white,
onBackground: Colors.white,
onBackground: Colors.white10, // only seen used in Switch so far
onError: Colors.white,
brightness: Brightness.dark,
),
@ -36,6 +36,7 @@ ThemeData theme() => ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: LIGHT1,
foregroundColor: Colors.white,
),
),
progressIndicatorTheme: const ProgressIndicatorThemeData(color: LIGHT2),
@ -79,17 +80,21 @@ ThemeData theme() => ThemeData(
tabBarTheme: const TabBarTheme(
labelColor: Colors.white,
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w800,
fontSize: 20.0,
),
unselectedLabelStyle: TextStyle(
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w800,
fontSize: 20.0,
),
),
iconTheme: const IconThemeData(
color: Colors.white,
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: Colors.white,
)),
appBarTheme: const AppBarTheme(
color: DARK1,
elevation: 0.0,
@ -109,6 +114,7 @@ ThemeData theme() => ThemeData(
indent: HORIZONTAL_PADDING,
endIndent: HORIZONTAL_PADDING,
space: 0.0,
color: Colors.white10,
),
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith<Color>((states) {
@ -132,11 +138,23 @@ ThemeData theme() => ThemeData(
thumbColor: MaterialStateProperty.all(Colors.white12),
interactive: true,
),
listTileTheme: const ListTileThemeData(
iconColor: Colors.white,
),
radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.white30;
} else if (states.contains(MaterialState.selected)) {
return LIGHT1;
}
return Colors.white;
})),
);
const TextStyle TEXT_HEADER = TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w900,
fontWeight: FontWeight.w800,
);
const TextStyle TEXT_HEADER_S = TextStyle(
@ -155,12 +173,12 @@ const TextStyle TEXT_SUBTITLE = TextStyle(
);
const TextStyle TEXT_SMALL_HEADLINE = TextStyle(
fontSize: 12.0,
fontSize: 13.0,
fontWeight: FontWeight.normal,
);
const TextStyle TEXT_SMALL_SUBTITLE = TextStyle(
fontSize: 12.0,
fontSize: 13.0,
fontWeight: FontWeight.w300,
);

View file

@ -6,7 +6,6 @@ import '../domain/entities/album.dart';
import '../domain/entities/playable.dart';
import '../domain/entities/playlist.dart';
import '../domain/entities/smart_list.dart';
import '../domain/entities/song.dart';
import 'gradients.dart';
import 'mucke_icons.dart';
import 'theming.dart';
@ -46,6 +45,8 @@ String? validateNumber(bool enabled, String number) {
return int.tryParse(number) == null ? 'Error' : null;
}
Color bgColor(Color? color) => Color.lerp(DARK3, color, 0.4) ?? DARK3;
IconData blockLevelIcon(int blockLevel) {
switch (blockLevel) {
case 1:
@ -75,15 +76,26 @@ IconData likeCountIcon(int likeCount) {
}
Color likeCountColor(int likeCount) {
return likeCount == 3 ? LIGHT2 : Colors.white.withOpacity(0.24 + 0.18 * likeCount);
return likeCount == 3 ? LIGHT1 : Colors.white.withOpacity(0.24 + 0.18 * likeCount);
}
Color linkColor(Song song) {
if (song.next && song.previous) {
return LIGHT2;
} else if (song.next) {
IconData linkIcon(bool prev, bool next) {
if (prev && next) {
return MuckeIcons.link_both;
} else if (prev) {
return MuckeIcons.link_prev;
} else if (next) {
return MuckeIcons.link_next;
}
return Icons.link_off_rounded;
}
Color linkColor(bool prev, bool next) {
if (next && prev) {
return LIGHT1;
} else if (next) {
return Colors.red;
} else if (song.previous) {
} else if (prev) {
return Colors.blue;
}
return Colors.white24;

View file

@ -68,7 +68,7 @@ class _AlbumBackgroundState extends State<AlbumBackground> {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color.lerp(DARK3, color, 0.4) ?? DARK3, DARK1],
colors: [bgColor(color), DARK1],
stops: const [0.0, 1.0],
),
),

View file

@ -30,17 +30,13 @@ class ArtistHeader extends StatelessWidget {
),
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
titlePadding: const EdgeInsets.symmetric(horizontal: 48.0),
titlePadding: EdgeInsets.only(left: 48.0, right: 48.0, top: MediaQuery.of(context).padding.top),
title: Container(
alignment: Alignment.center,
height: height * 0.66,
child: Text(
artist.name,
style: Theme.of(context).textTheme.displayLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 24.0,
color: Colors.white,
),
style: Theme.of(context).textTheme.displaySmall,
textAlign: TextAlign.center,
maxLines: 3,
),

View file

@ -31,8 +31,7 @@ class ArtistHighlightedSongs extends StatelessWidget {
final Song song = songsHead[index];
return SongListTile(
song: song,
showAlbum: true,
subtitle: Subtitle.stats,
showPlayCount: true,
onTap: () => audioStore.playSong(index, songs, artistPageStore.artist),
onTapMore: () => showModalBottomSheet(
context: context,

View file

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/localizations.dart';
import '../pages/cover_customization_page.dart';
import '../state/cover_customization_store.dart';
import 'playlist_cover.dart';
class CoverCustomizationCard extends StatelessWidget {
const CoverCustomizationCard({
Key? key,
required this.store,
}) : super(key: key);
final CoverCustomizationStore store;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => CoverCustomizationPage(
store: store,
),
),
);
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
PlaylistCover(
size: 64.0,
gradient: store.gradient,
icon: store.icon,
),
const SizedBox(width: 16.0),
Text(L10n.of(context)!.customizeCover),
const Spacer(),
const SizedBox(
width: 56.0,
child: Icon(Icons.chevron_right_rounded),
),
],
),
),
),
),
);
}
}

View file

@ -14,7 +14,7 @@ class CoverSliverAppBar extends StatefulWidget {
this.subtitle2,
required this.actions,
required this.cover,
required this.background,
required this.backgroundColor,
this.button,
}) : super(key: key);
@ -23,7 +23,7 @@ class CoverSliverAppBar extends StatefulWidget {
final String? subtitle2;
final List<Widget> actions;
final Widget cover;
final Widget background;
final Color backgroundColor;
final Widget? button;
@override
@ -49,7 +49,7 @@ class _CoverSliverAppBarState extends State<CoverSliverAppBar> {
flexibleSpace: Header(
minHeight: minHeight,
maxHeight: maxHeight,
background: widget.background,
backgroundColor: widget.backgroundColor,
cover: widget.cover,
title: widget.title,
subtitle: widget.subtitle,
@ -61,6 +61,8 @@ class _CoverSliverAppBarState extends State<CoverSliverAppBar> {
onPressed: () => navStore.pop(context),
),
actions: widget.actions,
snap: true,
floating: true,
);
}
}
@ -74,7 +76,7 @@ class Header extends StatelessWidget {
this.subtitle,
this.subtitle2,
required this.cover,
required this.background,
required this.backgroundColor,
this.button,
}) : super(key: key);
@ -82,7 +84,7 @@ class Header extends StatelessWidget {
final String? subtitle;
final String? subtitle2;
final Widget cover;
final Widget background;
final Color backgroundColor;
final double maxHeight;
final double minHeight;
final Widget? button;
@ -99,10 +101,6 @@ class Header extends StatelessWidget {
return Stack(
fit: StackFit.expand,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: _buildBackground(background, animation, maxHeight, minHeight),
),
_buildGradient(animation, context),
_buildImage(animation, context),
if (button != null) _buildButton(animation, context),
@ -149,7 +147,7 @@ class Header extends StatelessWidget {
// TODO: padding right for enabled multi select
return Align(
alignment: AlignmentTween(
begin: Alignment.centerLeft,
begin: Alignment.center,
end: Alignment.topLeft,
).evaluate(animation),
child: Container(
@ -178,6 +176,7 @@ class Header extends StatelessWidget {
FontWeight.w600,
Tween<double>(begin: 0, end: 1).evaluate(animation),
),
height: 1.1,
),
),
Container(
@ -215,7 +214,7 @@ class Header extends StatelessWidget {
Widget _buildButton(Animation<double> animation, BuildContext context) {
return Positioned(
width: 96,
height: 48,
height: 56,
right: HORIZONTAL_PADDING,
top: Tween<double>(
begin: kToolbarHeight + MediaQuery.of(context).padding.top + 120,
@ -252,13 +251,9 @@ class Header extends StatelessWidget {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
backgroundColor,
ColorTween(
begin: Theme.of(context).primaryColor,
end: Theme.of(context).primaryColor.withOpacity(0.2),
).evaluate(animation) ??
Colors.transparent,
ColorTween(
begin: Theme.of(context).primaryColor,
begin: backgroundColor,
end: Theme.of(context).scaffoldBackgroundColor,
).evaluate(animation) ??
Colors.transparent,
@ -270,19 +265,4 @@ class Header extends StatelessWidget {
),
);
}
Widget _buildBackground(
Widget background,
Animation<double> animation,
double maxHeight,
double minHeight,
) {
return ClipRect(
clipBehavior: Clip.hardEdge,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 24.0, sigmaY: 24.0),
child: background,
),
);
}
}

View file

@ -29,12 +29,7 @@ class CurrentlyPlayingBar extends StatelessWidget {
if (song == null) return Container();
return Padding(
padding: const EdgeInsets.only(
bottom: 0.0,
top: 8.0,
left: 4.0,
right: 4.0,
),
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: <Widget>[
Padding(
@ -54,7 +49,7 @@ class CurrentlyPlayingBar extends StatelessWidget {
Text(
song.title,
overflow: TextOverflow.ellipsis,
maxLines: 2,
maxLines: 1,
),
Text(
song.artist,

View file

@ -43,6 +43,7 @@ class PlaylistCover extends StatelessWidget {
child: Icon(
icon,
size: size / 2.0,
color: Colors.white,
),
),
decoration: deco,

View file

@ -7,6 +7,7 @@ import '../../domain/entities/album.dart';
import '../../domain/entities/artist.dart';
import '../../domain/entities/song.dart';
import '../l10n_utils.dart';
import '../mucke_icons.dart';
import '../pages/album_details_page.dart';
import '../pages/artist_details_page.dart';
import '../state/audio_store.dart';
@ -66,11 +67,10 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
final NavigationStore navStore = GetIt.I<NavigationStore>();
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
int optionIndex = 0;
return Observer(builder: (context) {
final song = store.songStream.value;
if (song == null) return Container();
final firstLast = musicDataStore.isSongFirstLast(song);
final albums = musicDataStore.albumStream.value;
final artists = musicDataStore.artistStream.value;
@ -84,42 +84,18 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
artist = artists.singleWhere((a) => a.name == album!.artist);
}
final options = [
const SizedBox.shrink(),
Container(
// color: DARK3,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: SwitchListTile(
title: Text(L10n.of(context)!.previousSong.capitalize()),
value: song.previous,
onChanged: (_) => musicDataStore.togglePreviousSongLink(song),
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
),
),
Container(width: 1.0, height: 24.0, color: DARK2),
Expanded(
child: SwitchListTile(
title: Text(L10n.of(context)!.nextSong.capitalize()),
value: song.next,
onChanged: (_) => musicDataStore.toggleNextSongLink(song),
contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
),
),
],
),
),
ExcludeLevelOptions(songs: [song], musicDataStore: musicDataStore),
];
final List<Widget> widgets = [
Container(
color: DARK2,
child: Padding(
padding: const EdgeInsets.all(HORIZONTAL_PADDING),
padding: const EdgeInsets.fromLTRB(
HORIZONTAL_PADDING,
HORIZONTAL_PADDING,
HORIZONTAL_PADDING - 14.0,
HORIZONTAL_PADDING,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 64.0,
@ -149,18 +125,28 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
song.title,
style: TEXT_HEADER_S,
),
const SizedBox(height: 4.0),
const SizedBox(height: 2.0),
Text(
'#${song.trackNumber}${utils.msToTimeString(song.duration)}${song.year}',
style: TEXT_SMALL_SUBTITLE,
),
Text(
L10n.of(context)!.playedNTimes(song.playCount).capitalize(),
style: TEXT_SMALL_SUBTITLE,
style: TEXT_SMALL_SUBTITLE.copyWith(height: 1.2),
),
],
),
)
),
SizedBox(
height: 64.0,
child: Center(
child: LikeButton(
song: song,
iconSize: 28.0,
visualDensity: VisualDensity.standard,
),
),
),
],
),
),
@ -169,7 +155,6 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
title: Text('${song.album}'),
leading: const Icon(Icons.album_rounded),
trailing: widget.enableGoToAlbum ? const Icon(Icons.open_in_new_rounded) : null,
visualDensity: VisualDensity.compact,
onTap: widget.enableGoToAlbum
? () {
if (album != null) {
@ -190,7 +175,6 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
title: Text(song.artist),
leading: const Icon(Icons.person_rounded),
trailing: widget.enableGoToArtist ? const Icon(Icons.open_in_new_rounded) : null,
visualDensity: VisualDensity.compact,
onTap: widget.enableGoToArtist
? () {
if (artist != null) {
@ -208,51 +192,62 @@ class _SongBottomSheetState extends State<SongBottomSheet> {
: () {},
),
if (widget.enableSongCustomization)
StatefulBuilder(
builder: (context, setState) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: HORIZONTAL_PADDING - 12.0, vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
if (optionIndex == 1)
setState(() => optionIndex = 0);
else
setState(() => optionIndex = 1);
},
icon: const Icon(Icons.link_rounded),
color: utils.linkColor(song),
),
LikeButton(song: song),
IconButton(
onPressed: () {
if (optionIndex == 2)
setState(() => optionIndex = 0);
else
setState(() => optionIndex = 2);
},
icon: Icon(utils.blockLevelIcon(song.blockLevel)),
color: utils.blockLevelColor(song.blockLevel),
),
],
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
transitionBuilder: (child, animation) => SizeTransition(
sizeFactor: animation,
child: child,
),
child: options[optionIndex],
),
],
),
),
ExcludeLevelOptions(songs: [song], musicDataStore: musicDataStore),
if (widget.enableSongCustomization)
FutureBuilder(
future: firstLast,
builder: (context, AsyncSnapshot<List<bool>> snapshot) {
if (snapshot.hasData)
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 5,
child: SwitchListTile(
secondary: Icon(
MuckeIcons.link_prev,
color: song.previous
? utils.linkColor(true, false)
: utils.linkColor(true, false).withOpacity(0.5),
),
value: song.previous,
onChanged: snapshot.data![0]
? null
: (_) => musicDataStore.togglePreviousSongLink(song),
contentPadding:
const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
),
),
Expanded(
flex: 2,
child: Center(
child: Container(width: 1.0, height: 24.0, color: DARK2),
),
),
Expanded(
flex: 5,
child: SwitchListTile(
secondary: Icon(
MuckeIcons.link_next,
color: song.next
? utils.linkColor(false, true)
: utils.linkColor(false, true).withOpacity(0.5),
),
value: song.next,
onChanged: snapshot.data![1]
? null
: (_) => musicDataStore.toggleNextSongLink(song),
contentPadding:
const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
),
),
],
),
);
else
return Container();
}),
if (widget.enableQueueActions) ...[
ListTile(
title: Text(L10n.of(context)!.playNext),

View file

@ -31,8 +31,8 @@ class SongCustomizationButtons extends StatelessWidget {
children: [
IconButton(
icon: Icon(
song.next || song.previous ? Icons.link_rounded : Icons.link_off_rounded,
color: linkColor(song),
linkIcon(song.previous, song.next),
color: linkColor(song.previous, song.next),
),
iconSize: 24.0,
onPressed: () => _editLinks(context),
@ -72,24 +72,42 @@ class SongCustomizationButtons extends StatelessWidget {
Observer(builder: (context) {
final song = audioStore.currentSongStream.value;
if (song == null) return Container();
return SwitchListTile(
title: Text(L10n.of(context)!.alwaysPlayPrevious),
value: song.previous,
onChanged: (bool value) {
musicDataStore.togglePreviousSongLink(song);
},
);
final firstLast = musicDataStore.isSongFirstLast(song);
return FutureBuilder(
future: firstLast,
builder: (context, AsyncSnapshot<List<bool>> snapshot) {
if (snapshot.hasData)
return SwitchListTile(
title: Text(L10n.of(context)!.alwaysPlayPrevious),
value: song.previous,
onChanged: snapshot.data![0]
? null
: (bool value) {
musicDataStore.togglePreviousSongLink(song);
},
);
return Container();
});
}),
Observer(builder: (context) {
final song = audioStore.currentSongStream.value;
if (song == null) return Container();
return SwitchListTile(
title: Text(L10n.of(context)!.alwaysPlayNext),
value: song.next,
onChanged: (bool value) {
musicDataStore.toggleNextSongLink(song);
},
);
final firstLast = musicDataStore.isSongFirstLast(song);
return FutureBuilder(
future: firstLast,
builder: (context, AsyncSnapshot<List<bool>> snapshot) {
if (snapshot.hasData)
return SwitchListTile(
title: Text(L10n.of(context)!.alwaysPlayNext),
value: song.next,
onChanged: snapshot.data![1]
? null
: (bool value) {
musicDataStore.toggleNextSongLink(song);
},
);
return Container();
});
}),
],
),

View file

@ -3,11 +3,8 @@ import 'package:flutter/material.dart';
import '../../domain/entities/queue_item.dart';
import '../../domain/entities/song.dart';
import '../theming.dart';
import '../utils.dart' as utils;
import '../utils.dart';
enum Subtitle { artist, artistAlbum, stats }
class SongListTile extends StatelessWidget {
const SongListTile({
Key? key,
@ -15,8 +12,7 @@ class SongListTile extends StatelessWidget {
required this.onTap,
required this.onTapMore,
this.highlight = false,
this.showAlbum = true,
this.subtitle = Subtitle.artist,
this.showPlayCount = false,
this.source = QueueItemSource.original,
required this.onSelect,
this.isSelectEnabled = false,
@ -27,8 +23,7 @@ class SongListTile extends StatelessWidget {
final Function onTap;
final Function onTapMore;
final bool highlight;
final bool showAlbum;
final Subtitle subtitle;
final bool showPlayCount;
final QueueItemSource source;
final bool isSelectEnabled;
final bool isSelected;
@ -36,101 +31,92 @@ class SongListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Widget leading = showAlbum
? Image(
image: utils.getAlbumImage(song.albumArtPath),
fit: BoxFit.cover,
)
: Center(child: Text('${song.trackNumber}'));
Widget subtitleWidget;
switch (subtitle) {
case Subtitle.artist:
subtitleWidget = Text(
song.artist,
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
break;
case Subtitle.artistAlbum:
subtitleWidget = Text(
'${song.artist}${song.album}',
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
break;
case Subtitle.stats:
subtitleWidget = Row(
children: [
const Icon(
Icons.favorite_rounded,
size: 12.0,
),
const SizedBox(width: 2.0),
Text(
'${song.likeCount}',
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white),
),
const SizedBox(width: 8.0),
const Icon(
Icons.play_arrow_rounded,
size: 12.0,
),
const SizedBox(width: 2.0),
Text(
'${song.playCount}',
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white),
),
],
);
}
if (source != QueueItemSource.original) {
final icon = source == QueueItemSource.added ? Icons.add_circle_rounded : Icons.link_rounded;
subtitleWidget = Row(
children: [
Icon(icon, color: LIGHT1, size: 14.0),
const SizedBox(width: 4.0),
subtitleWidget,
],
);
}
return ListTile(
contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
minVerticalPadding: 8.0,
leading: SizedBox(
height: 56,
width: 56,
child: leading,
child: Image(
image: getAlbumImage(song.albumArtPath),
fit: BoxFit.cover,
),
),
title: Text(
song.title,
maxLines: 2,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: subtitleWidget,
subtitle: Row(
children: [
if (showPlayCount)
Row(
children: [
Text(
'${song.playCount}',
style: TEXT_SMALL_SUBTITLE,
),
const Padding(
padding: EdgeInsets.only(right: 2.0),
child: Icon(
Icons.play_arrow_rounded,
size: 13.0,
),
),
],),
if (source != QueueItemSource.original)
Icon(
source == QueueItemSource.added ? Icons.add_circle_rounded : Icons.link_rounded,
color: LIGHT1,
size: 13.0,
),
if (source != QueueItemSource.original)
const Text(
'',
style: TEXT_SMALL_SUBTITLE,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Icon(
likeCountIcon(song.likeCount),
size: 13.0,
color: likeCountColor(song.likeCount),
),
if (song.blockLevel > 0)
Padding(
padding: const EdgeInsets.only(left: 2.0),
child: Icon(
blockLevelIcon(song.blockLevel),
size: 13.0,
color: Colors.white38,
),
),
if (song.next || song.previous)
Padding(
padding: const EdgeInsets.only(left: 2.0),
child: Icon(
linkIcon(song.previous, song.next),
size: 13.0,
color: linkColor(song.previous, song.next).withOpacity(0.7),
),
),
Expanded(
child: Text(
'${song.artist}',
style: TEXT_SMALL_SUBTITLE,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () => onTap(),
tileColor: highlight ? Colors.white10 : Colors.transparent,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (song.blockLevel > 0)
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Icon(
blockLevelIcon(song.blockLevel),
size: 16.0,
color: Colors.white38,
),
),
Icon(
likeCountIcon(song.likeCount),
size: 16.0,
color: utils.likeCountColor(song.likeCount),
),
if (!isSelectEnabled)
IconButton(
icon: const Icon(Icons.more_vert_rounded),

View file

@ -36,34 +36,51 @@ class SongListTileNumbered extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${msToTimeString(song.duration)}${song.artist}',
style: const TextStyle(
fontWeight: FontWeight.w300,
),
subtitle: Row(
children: [
Text(
'${msToTimeString(song.duration)}',
style: const TextStyle(
fontWeight: FontWeight.w300,
),
),
Icon(
likeCountIcon(song.likeCount),
size: 13.0,
color: likeCountColor(song.likeCount),
),
if (song.blockLevel > 0)
Padding(
padding: const EdgeInsets.only(left: 2.0),
child: Icon(
blockLevelIcon(song.blockLevel),
size: 13.0,
color: Colors.white38,
),
),
if (song.next || song.previous)
Padding(
padding: const EdgeInsets.only(left: 2.0),
child: Icon(
linkIcon(song.previous, song.next),
size: 13.0,
color: linkColor(song.previous, song.next).withOpacity(0.7),
),
),
Expanded(
child: Text(
'${song.artist}',
style: const TextStyle(
fontWeight: FontWeight.w300,
),
),
),
],
),
onTap: () => onTap(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (song.blockLevel > 0)
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Icon(
blockLevelIcon(song.blockLevel),
size: 16.0,
color: Colors.white38,
),
),
Icon(
likeCountIcon(song.likeCount),
size: 16.0,
color: song.likeCount == 3
? LIGHT2
: Colors.white.withOpacity(
0.2 + 0.18 * song.likeCount,
),
),
if (!isSelectEnabled)
IconButton(
icon: const Icon(Icons.more_vert_rounded),

View file

@ -23,7 +23,7 @@ class SwitchTextListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 6.0, right: HORIZONTAL_PADDING),
padding: const EdgeInsets.only(left: 8.0 - 4.0, right: 8.0),
child: Row(
children: [
SizedBox(

View file

@ -460,4 +460,11 @@ class MusicDataDao extends DatabaseAccessor<MainDatabase>
}
}
}
@override
Future<List<SongModel>> getSongsFromSameAlbum(SongModel song) async {
return (select(songs)..where((tbl) => tbl.albumId.equals(song.albumId)))
.get()
.then((driftSongs) => driftSongs.map(SongModel.fromDrift).toList());
}
}

View file

@ -10,6 +10,7 @@ abstract class MusicDataSource {
Future<SongModel?> getSongByPath(String path);
Future<SongModel?> getPredecessor(SongModel song);
Future<SongModel?> getSuccessor(SongModel song);
Future<List<SongModel>> getSongsFromSameAlbum(SongModel song);
Stream<List<AlbumModel>> get albumStream;
Stream<List<AlbumModel>> getArtistAlbumStream(ArtistModel artist);

View file

@ -562,4 +562,11 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
Future<void> removeBlockedFiles(List<String> paths) async {
await _musicDataSource.removeBlockedFiles(paths);
}
@override
Future<List<bool>> isSongFirstLast(Song song) async {
final songs = await _musicDataSource.getSongsFromSameAlbum(song as SongModel).then(_sortAlbumSongs);
return [songs.indexOf(song) == 0, songs.indexOf(song) == songs.length - 1];
}
}

View file

@ -914,6 +914,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.16"
text_scroll:
dependency: "direct main"
description:
name: text_scroll
sha256: "7869d86a6fdd725dee56bdd150216a99f0372b82fbfcac319214dbd5f36e1908"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
timing:
dependency: transitive
description:

View file

@ -37,6 +37,7 @@ dependencies:
reorderables: ^0.6.0 # MIT
sqlite3_flutter_libs: ^0.5.0 # MIT
string_similarity: ^2.0.0 # MIT
text_scroll: ^0.2.0 # MIT
dev_dependencies:
build_runner: ^2.3.3 # BSD 3