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,8 +39,7 @@ class _ArtistOfDayFormPageState extends State<ArtistOfDayFormPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final NavigationStore navStore = GetIt.I<NavigationStore>();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.artistOfTheDay,
|
||||
|
@ -115,7 +114,6 @@ class _ArtistOfDayFormPageState extends State<ArtistOfDayFormPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,8 +37,7 @@ class _HistoryFormPageState extends State<HistoryFormPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final NavigationStore navStore = GetIt.I<NavigationStore>();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.history,
|
||||
|
@ -135,7 +134,6 @@ class _HistoryFormPageState extends State<HistoryFormPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,8 +59,7 @@ class _PlaylistsFormPageState extends State<PlaylistsFormPage> {
|
|||
L10n.of(context)!.smartlistsOnly,
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.playlists,
|
||||
|
@ -247,7 +246,6 @@ class _PlaylistsFormPageState extends State<PlaylistsFormPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,8 +39,7 @@ class _ShuffleAllFormPageState extends State<ShuffleAllFormPage> {
|
|||
Widget build(BuildContext context) {
|
||||
final NavigationStore navStore = GetIt.I<NavigationStore>();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.shuffleAll,
|
||||
|
@ -115,7 +114,6 @@ class _ShuffleAllFormPageState extends State<ShuffleAllFormPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +50,8 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
final AudioStore audioStore = GetIt.I<AudioStore>();
|
||||
|
||||
return Scaffold(
|
||||
body: Observer(
|
||||
body: Material(
|
||||
child: Observer(
|
||||
builder: (BuildContext context) {
|
||||
final album = widget.album;
|
||||
final songs = store.albumSongStream.value ?? [];
|
||||
|
@ -122,12 +123,10 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
image: utils.getAlbumImage(album.albumArtPath),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
background: Image(
|
||||
image: utils.getAlbumImage(album.albumArtPath),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
backgroundColor: utils.bgColor(album.color),
|
||||
button: SizedBox(
|
||||
width: 48,
|
||||
child: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => audioStore.playAlbum(album),
|
||||
child: const Icon(Icons.play_arrow_rounded),
|
||||
|
@ -140,6 +139,7 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (int d = 0; d < songsByDisc.length; d++)
|
||||
Observer(
|
||||
builder: (context) {
|
||||
|
@ -152,26 +152,24 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
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)),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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]],
|
||||
isSelected:
|
||||
isMultiSelectEnabled && isSelected[s + discSongNums[d]],
|
||||
onTap: () => audioStore.playAlbumFromIndex(
|
||||
widget.album,
|
||||
s + _calcOffset(d, songsByDisc),
|
||||
|
@ -199,6 +197,7 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,8 +15,7 @@ class BlockedFilesPage extends StatelessWidget {
|
|||
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
|
||||
final NavigationStore navStore = GetIt.I<NavigationStore>();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.blockedFiles,
|
||||
|
@ -55,7 +54,6 @@ class BlockedFilesPage extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,7 @@ class _CoverCustomizationPageState extends State<CoverCustomizationPage> {
|
|||
Widget build(BuildContext context) {
|
||||
print('CoverCustomizationPage.build');
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.customizeCover,
|
||||
|
@ -129,7 +128,6 @@ class _CoverCustomizationPageState extends State<CoverCustomizationPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +46,7 @@ class _HomePageInner extends StatelessWidget {
|
|||
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
|
||||
|
||||
print('HomePage.build');
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context)!.home),
|
||||
actions: [
|
||||
|
@ -125,7 +124,6 @@ class _HomePageInner extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,7 @@ class HomeSettingsPage extends StatelessWidget {
|
|||
final homeStore = GetIt.I<HomePageStore>();
|
||||
final navStore = GetIt.I<NavigationStore>();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.homeCustomization,
|
||||
|
@ -117,7 +116,6 @@ class HomeSettingsPage extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,7 @@ class LibraryFoldersPage extends StatelessWidget {
|
|||
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
|
||||
final NavigationStore navStore = GetIt.I<NavigationStore>();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.libraryFolders,
|
||||
|
@ -58,7 +57,6 @@ class LibraryFoldersPage extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,23 +13,28 @@ 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: Scaffold(
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 8.0,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(48),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0, left: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TabBar(
|
||||
indicatorColor: Theme.of(context).highlightColor,
|
||||
indicator: UnderlineTabIndicator(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).highlightColor,
|
||||
width: 3.0,
|
||||
),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicatorWeight: 3.0,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
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),
|
||||
|
@ -38,12 +43,10 @@ class LibraryTabContainer extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(
|
||||
child: TabBarView(
|
||||
body: const TabBarView(
|
||||
children: <Widget>[
|
||||
ArtistsPage(key: PageStorageKey('ArtistsPage')),
|
||||
AlbumsPage(key: PageStorageKey('AlbumsPage')),
|
||||
|
@ -51,9 +54,6 @@ class LibraryTabContainer extends StatelessWidget {
|
|||
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,8 +51,7 @@ class _PlaylistFormPageState extends State<PlaylistFormPage> {
|
|||
L10n.of(context)!.favShuffleMode,
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
title,
|
||||
|
@ -108,7 +106,8 @@ class _PlaylistFormPageState extends State<PlaylistFormPage> {
|
|||
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,
|
||||
errorText:
|
||||
store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null,
|
||||
errorStyle: const TextStyle(color: RED),
|
||||
filled: true,
|
||||
fillColor: DARK35,
|
||||
|
@ -126,45 +125,7 @@ class _PlaylistFormPageState extends State<PlaylistFormPage> {
|
|||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
builder: (context) => CoverCustomizationCard(store: store.cover),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
ListTile(
|
||||
|
@ -184,6 +145,7 @@ class _PlaylistFormPageState extends State<PlaylistFormPage> {
|
|||
playbackModeTexts[value],
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
value: value,
|
||||
|
@ -207,7 +169,6 @@ class _PlaylistFormPageState extends State<PlaylistFormPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +38,7 @@ class QueuePage extends StatelessWidget {
|
|||
final ScrollController _scrollController =
|
||||
ScrollController(initialScrollOffset: initialIndex * 72.0);
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
|
||||
|
@ -62,7 +61,7 @@ class QueuePage extends StatelessWidget {
|
|||
|
||||
if (playable != null) {
|
||||
subTitle = Text(
|
||||
playable.repr(context),
|
||||
playable.repr(),
|
||||
maxLines: 1,
|
||||
);
|
||||
}
|
||||
|
@ -73,10 +72,6 @@ class QueuePage extends StatelessWidget {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context)!.currentlyPlaying.toUpperCase(),
|
||||
style: TEXT_SMALL_HEADLINE,
|
||||
),
|
||||
subTitle,
|
||||
const SizedBox(height: 4.0),
|
||||
Text(
|
||||
|
@ -218,7 +213,6 @@ class QueuePage extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
bottomNavigationBar: const QueueControlBar(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -64,20 +64,12 @@ class _SearchPageState extends State<SearchPage> {
|
|||
|
||||
String searchText = '';
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 8.0,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
left: HORIZONTAL_PADDING - 8.0,
|
||||
right: HORIZONTAL_PADDING - 8.0,
|
||||
top: 11.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: StatefulBuilder(builder: (context, setState) {
|
||||
|
@ -121,13 +113,16 @@ class _SearchPageState extends State<SearchPage> {
|
|||
);
|
||||
}),
|
||||
),
|
||||
Padding(
|
||||
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) {
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
final artists = searchStore.searchResultsArtists;
|
||||
final albums = searchStore.searchResultsAlbums;
|
||||
final songs = searchStore.searchResultsSongs;
|
||||
|
@ -139,9 +134,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||
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);
|
||||
songs.length * 72.0 + songs.isNotEmpty.toDouble() * (16.0 + 56.0);
|
||||
final smartListsHeight =
|
||||
smartlists.length * 56.0 + smartlists.isNotEmpty.toDouble() * (16.0 + 56.0);
|
||||
smartlists.length * 72.0 + smartlists.isNotEmpty.toDouble() * (16.0 + 56.0);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
@ -198,13 +193,12 @@ class _SearchPageState extends State<SearchPage> {
|
|||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(builder: (context) {
|
||||
),
|
||||
),
|
||||
body: Observer(builder: (context) {
|
||||
final artists = searchStore.searchResultsArtists;
|
||||
final albums = searchStore.searchResultsAlbums;
|
||||
final songs = searchStore.searchResultsSongs;
|
||||
|
@ -368,10 +362,11 @@ class _SearchPageState extends State<SearchPage> {
|
|||
),
|
||||
),
|
||||
trailing: PlayShuffleButton(
|
||||
size: 48.0,
|
||||
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),
|
||||
],
|
||||
|
@ -407,10 +402,11 @@ class _SearchPageState extends State<SearchPage> {
|
|||
),
|
||||
),
|
||||
trailing: PlayShuffleButton(
|
||||
size: 48.0,
|
||||
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),
|
||||
],
|
||||
|
@ -420,9 +416,6 @@ class _SearchPageState extends State<SearchPage> {
|
|||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,7 @@ class SettingsPage extends StatelessWidget {
|
|||
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.settings,
|
||||
|
@ -172,7 +171,6 @@ class SettingsPage extends StatelessWidget {
|
|||
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,8 +66,7 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
L10n.of(context)!.favShuffleMode,
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
title,
|
||||
|
@ -123,7 +121,8 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
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,
|
||||
errorText:
|
||||
store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null,
|
||||
errorStyle: const TextStyle(color: RED),
|
||||
filled: true,
|
||||
fillColor: DARK35,
|
||||
|
@ -141,45 +140,7 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
builder: (context) => CoverCustomizationCard(store: store.cover),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
ListTile(
|
||||
|
@ -206,8 +167,10 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
top: 4.0,
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(context)!
|
||||
.filterLikes(store.minLikeCount, store.maxLikeCount),
|
||||
L10n.of(context)!.filterLikes(
|
||||
store.minLikeCount,
|
||||
store.maxLikeCount,
|
||||
),
|
||||
),
|
||||
),
|
||||
RangeSlider(
|
||||
|
@ -286,6 +249,7 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
blockLevelTexts[value],
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
value: value,
|
||||
|
@ -365,12 +329,14 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: HORIZONTAL_PADDING,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 60.0 + 6.0, height: 48.0),
|
||||
Column(
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
|
@ -388,7 +354,8 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 56.0,
|
||||
child: Icon(Icons.chevron_right_rounded),
|
||||
|
@ -401,8 +368,8 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
const SizedBox(height: CARD_SPACING),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: HORIZONTAL_PADDING,
|
||||
left: 8.0 - 4.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
@ -529,6 +496,7 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
playbackModeTexts[value],
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
value: value,
|
||||
|
@ -552,7 +520,6 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +21,7 @@ class SmartListArtistsPage extends StatelessWidget {
|
|||
final NavigationStore navStore = GetIt.I<NavigationStore>();
|
||||
final initialSet = Set<Artist>.from(store.selectedArtists);
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
L10n.of(context)!.selectArtists,
|
||||
|
@ -75,7 +74,6 @@ class SmartListArtistsPage extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
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: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (optionIndex == 1)
|
||||
setState(() => optionIndex = 0);
|
||||
else
|
||||
setState(() => optionIndex = 1);
|
||||
},
|
||||
icon: const Icon(Icons.link_rounded),
|
||||
color: utils.linkColor(song),
|
||||
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),
|
||||
),
|
||||
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],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
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();
|
||||
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: (bool value) {
|
||||
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();
|
||||
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: (bool value) {
|
||||
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}',
|
||||
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