parent
5411d47924
commit
a3a6c7cc0f
51 changed files with 2779 additions and 2510 deletions
|
@ -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
|
||||
|
|
93
src/assets/icons/link_both.svg
Normal file
93
src/assets/icons/link_both.svg
Normal 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 |
93
src/assets/icons/link_next.svg
Normal file
93
src/assets/icons/link_next.svg
Normal 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 |
98
src/assets/icons/link_prev.svg
Normal file
98
src/assets/icons/link_prev.svg
Normal 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.
|
@ -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);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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]),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
54
src/lib/presentation/widgets/cover_customization_card.dart
Normal file
54
src/lib/presentation/widgets/cover_customization_card.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -43,6 +43,7 @@ class PlaylistCover extends StatelessWidget {
|
|||
child: Icon(
|
||||
icon,
|
||||
size: size / 2.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
decoration: deco,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue