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

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

View file

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

View file

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

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

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

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

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

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,8 +8,7 @@ import '../state/music_data_store.dart';
import '../state/navigation_store.dart';
import '../state/playlist_form_store.dart';
import '../theming.dart';
import '../widgets/playlist_cover.dart';
import 'cover_customization_page.dart';
import '../widgets/cover_customization_card.dart';
class PlaylistFormPage extends StatefulWidget {
const PlaylistFormPage({Key? key, this.playlist}) : super(key: key);
@ -52,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> {
),
),
),
),
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,9 +12,8 @@ import '../state/music_data_store.dart';
import '../state/navigation_store.dart';
import '../state/smart_list_form_store.dart';
import '../theming.dart';
import '../widgets/playlist_cover.dart';
import '../widgets/cover_customization_card.dart';
import '../widgets/switch_text_listtile.dart';
import 'cover_customization_page.dart';
import 'smart_lists_artists_page.dart';
class SmartListFormPage extends StatefulWidget {
@ -67,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> {
),
),
),
),
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,8 +31,8 @@ class SongCustomizationButtons extends StatelessWidget {
children: [
IconButton(
icon: Icon(
song.next || song.previous ? Icons.link_rounded : Icons.link_off_rounded,
color: linkColor(song),
linkIcon(song.previous, song.next),
color: linkColor(song.previous, song.next),
),
iconSize: 24.0,
onPressed: () => _editLinks(context),
@ -72,24 +72,42 @@ class SongCustomizationButtons extends StatelessWidget {
Observer(builder: (context) {
final song = audioStore.currentSongStream.value;
if (song == null) return Container();
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();
});
}),
],
),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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