diff --git a/lib/domain/entities/song.dart b/lib/domain/entities/song.dart index b6516ca..50ee294 100644 --- a/lib/domain/entities/song.dart +++ b/lib/domain/entities/song.dart @@ -9,6 +9,7 @@ class Song extends Equatable { @required this.path, @required this.duration, @required this.blocked, + this.discNumber, this.trackNumber, this.albumArtPath, }); @@ -19,6 +20,7 @@ class Song extends Equatable { final String path; /// Duration in milliseconds. final int duration; + final int discNumber; final int trackNumber; final String albumArtPath; /// Is this song blocked in shuffle mode? diff --git a/lib/presentation/pages/album_details_page.dart b/lib/presentation/pages/album_details_page.dart index 5d4622f..3dcf4b0 100644 --- a/lib/presentation/pages/album_details_page.dart +++ b/lib/presentation/pages/album_details_page.dart @@ -7,7 +7,7 @@ import '../../domain/entities/song.dart'; import '../state/audio_store.dart'; import '../state/music_data_store.dart'; import '../utils.dart' as utils; -import '../widgets/album_art_list_tile.dart'; +import '../widgets/song_list_tile.dart'; class AlbumDetailsPage extends StatelessWidget { const AlbumDetailsPage({Key key, @required this.album}) : super(key: key); @@ -69,10 +69,9 @@ class AlbumDetailsPage extends StatelessWidget { final songIndex = (index / 2).round(); final Song song = musicDataStore.albumSongs[songIndex]; - return AlbumArtListTile( - title: song.title, - subtitle: '${song.artist}', - albumArtPath: song.albumArtPath, + return SongListTile( + song: song, + inAlbum: true, onTap: () => audioStore.playSong(songIndex, musicDataStore.albumSongs), ); } diff --git a/lib/presentation/pages/songs_page.dart b/lib/presentation/pages/songs_page.dart index 6e709ae..ca64110 100644 --- a/lib/presentation/pages/songs_page.dart +++ b/lib/presentation/pages/songs_page.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import '../../domain/entities/song.dart'; import '../state/audio_store.dart'; import '../state/music_data_store.dart'; -import '../widgets/album_art_list_tile.dart'; +import '../widgets/song_list_tile.dart'; class SongsPage extends StatefulWidget { const SongsPage({Key key}) : super(key: key); @@ -42,11 +42,11 @@ class _SongsPageState extends State itemCount: songs.length, itemBuilder: (_, int index) { final Song song = songs[index]; - return AlbumArtListTile( - title: song.title, - subtitle: '${song.artist} • ${song.album}', - albumArtPath: song.albumArtPath, + return SongListTile( + song: song, + inAlbum: false, onTap: () => audioStore.playSong(index, songs), + onTapMore: () => print('Hello World'), ); }, separatorBuilder: (BuildContext context, int index) => const Divider( diff --git a/lib/presentation/widgets/song_list_tile.dart b/lib/presentation/widgets/song_list_tile.dart new file mode 100644 index 0000000..73c2743 --- /dev/null +++ b/lib/presentation/widgets/song_list_tile.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import '../../domain/entities/song.dart'; +import '../theming.dart'; +import '../utils.dart' as utils; + +class SongListTile extends StatelessWidget { + const SongListTile( + {Key key, this.song, this.onTap, this.inAlbum, this.onTapMore}) + : super(key: key); + + final Song song; + final bool inAlbum; + final Function onTap; + final Function onTapMore; + + @override + Widget build(BuildContext context) { + final Widget leading = inAlbum + ? Center(child: Text('${song.discNumber} - ${song.trackNumber}')) + : Image( + image: utils.getAlbumImage(null), // FIXME + fit: BoxFit.cover, + ); + + final Widget subtitle = inAlbum + ? Text('${song.artist}') + : Text('${song.artist} • ${song.album}'); + + final EdgeInsets padding = (onTapMore != null) + ? const EdgeInsets.only(left: 8.0) + : const EdgeInsets.only(left: 8.0, right: 16.0); + + return ListTile( + contentPadding: padding, + leading: SizedBox( + height: 56, + width: 56, + child: leading, + ), + title: Text( + song.title, + ), + subtitle: subtitle, + onTap: () => onTap(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (song.blocked) + Icon( + Icons.remove_circle_outline, + size: 14.0, + color: RASPBERRY.withOpacity(0.4), + ), + if (onTapMore != null) + IconButton( + icon: const Icon(Icons.more_vert), + iconSize: 20.0, + onPressed: () => print('Hello'), + ), + ], + ), + ); + } +} diff --git a/lib/system/datasources/audio_player_task.dart b/lib/system/datasources/audio_player_task.dart index 97da99f..0bc34c7 100644 --- a/lib/system/datasources/audio_player_task.dart +++ b/lib/system/datasources/audio_player_task.dart @@ -51,18 +51,15 @@ class AudioPlayerTask extends BackgroundAudioTask { MediaControl.pause, MediaControl.skipToNext ], - playing: true, // FIXME: not necessarily true + playing: audioPlayer.playing, processingState: AudioProcessingState.ready, updateTime: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch), - position: const Duration( - milliseconds: 0), // FIXME: not true for shuffle mode changes + position: audioPlayer.position, ); } } - Duration position; - @override Future onStop() async { await audioPlayer.stop(); @@ -70,13 +67,6 @@ class AudioPlayerTask extends BackgroundAudioTask { await super.onStop(); } - // @override - // Future onClose() async { - // audioPlayer.stop(); - // AudioServiceBackground.setState(controls: null, processingState: null, playing: false); - // AudioServiceBackground.setMediaItem(null); - // } - @override Future onPlay() async { audioPlayer.play(); @@ -120,7 +110,6 @@ class AudioPlayerTask extends BackgroundAudioTask { Future init() async { print('AudioPlayerTask.init'); - audioPlayer.positionStream.listen((position) => handlePosition(position)); audioPlayer.playerStateStream.listen((event) => handlePlayerState(event)); audioPlayer.sequenceStateStream .listen((event) => playbackIndex = event?.currentIndex); @@ -144,7 +133,6 @@ class AudioPlayerTask extends BackgroundAudioTask { {SET_SHUFFLE_MODE: shuffleMode.toString()}); } - // FIXME: position (duration) reset when changing mode Future setShuffleMode(ShuffleMode mode) async { shuffleMode = mode; @@ -197,10 +185,6 @@ class AudioPlayerTask extends BackgroundAudioTask { audioPlayer.play(); } - void handlePosition(Duration position) { - this.position = position; - } - void handlePlayerState(PlayerState ps) { if (ps.processingState == ProcessingState.ready && ps.playing) { AudioServiceBackground.setState( @@ -213,7 +197,7 @@ class AudioPlayerTask extends BackgroundAudioTask { processingState: AudioProcessingState.ready, updateTime: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch), - position: position, + position: audioPlayer.position, ); } else if (ps.processingState == ProcessingState.ready && !ps.playing) { AudioServiceBackground.setState( @@ -225,7 +209,7 @@ class AudioPlayerTask extends BackgroundAudioTask { processingState: AudioProcessingState.ready, updateTime: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch), - position: position, + position: audioPlayer.position, playing: false, ); } diff --git a/lib/system/datasources/moor_music_data_source.dart b/lib/system/datasources/moor_music_data_source.dart index 6dd4bf1..7786f56 100644 --- a/lib/system/datasources/moor_music_data_source.dart +++ b/lib/system/datasources/moor_music_data_source.dart @@ -42,6 +42,7 @@ class Songs extends Table { TextColumn get path => text()(); IntColumn get duration => integer().nullable()(); TextColumn get albumArtPath => text().nullable()(); + IntColumn get discNumber => integer().nullable()(); IntColumn get trackNumber => integer().nullable()(); BoolColumn get blocked => boolean().withDefault(const Constant(false))(); BoolColumn get present => boolean().withDefault(const Constant(true))(); @@ -90,7 +91,10 @@ class MoorMusicDataSource extends _$MoorMusicDataSource Future> getSongsFromAlbum(AlbumModel album) { return (select(songs) ..where((tbl) => tbl.albumTitle.equals(album.title)) - ..orderBy([(t) => OrderingTerm(expression: t.trackNumber)])) + ..orderBy([ + (t) => OrderingTerm(expression: t.discNumber), + (t) => OrderingTerm(expression: t.trackNumber) + ])) .get() .then((moorSongList) => moorSongList .map((moorSong) => SongModel.fromMoorSong(moorSong)) @@ -143,15 +147,12 @@ class MoorMusicDataSource extends _$MoorMusicDataSource @override Future insertSongs(List songModels) async { - await update(songs).write(const SongsCompanion(present: Value(false))); await batch((batch) { batch.insertAllOnConflictUpdate( songs, - songModels - .map((e) => e.toMoorInsert()) - .toList(), + songModels.map((e) => e.toMoorInsert()).toList(), ); }); diff --git a/lib/system/datasources/moor_music_data_source.g.dart b/lib/system/datasources/moor_music_data_source.g.dart index 112afa3..ebcbe39 100644 --- a/lib/system/datasources/moor_music_data_source.g.dart +++ b/lib/system/datasources/moor_music_data_source.g.dart @@ -496,6 +496,7 @@ class MoorSong extends DataClass implements Insertable { final String path; final int duration; final String albumArtPath; + final int discNumber; final int trackNumber; final bool blocked; final bool present; @@ -507,6 +508,7 @@ class MoorSong extends DataClass implements Insertable { @required this.path, this.duration, this.albumArtPath, + this.discNumber, this.trackNumber, @required this.blocked, @required this.present}); @@ -530,6 +532,8 @@ class MoorSong extends DataClass implements Insertable { intType.mapFromDatabaseResponse(data['${effectivePrefix}duration']), albumArtPath: stringType .mapFromDatabaseResponse(data['${effectivePrefix}album_art_path']), + discNumber: intType + .mapFromDatabaseResponse(data['${effectivePrefix}disc_number']), trackNumber: intType .mapFromDatabaseResponse(data['${effectivePrefix}track_number']), blocked: @@ -562,6 +566,9 @@ class MoorSong extends DataClass implements Insertable { if (!nullToAbsent || albumArtPath != null) { map['album_art_path'] = Variable(albumArtPath); } + if (!nullToAbsent || discNumber != null) { + map['disc_number'] = Variable(discNumber); + } if (!nullToAbsent || trackNumber != null) { map['track_number'] = Variable(trackNumber); } @@ -593,6 +600,9 @@ class MoorSong extends DataClass implements Insertable { albumArtPath: albumArtPath == null && nullToAbsent ? const Value.absent() : Value(albumArtPath), + discNumber: discNumber == null && nullToAbsent + ? const Value.absent() + : Value(discNumber), trackNumber: trackNumber == null && nullToAbsent ? const Value.absent() : Value(trackNumber), @@ -616,6 +626,7 @@ class MoorSong extends DataClass implements Insertable { path: serializer.fromJson(json['path']), duration: serializer.fromJson(json['duration']), albumArtPath: serializer.fromJson(json['albumArtPath']), + discNumber: serializer.fromJson(json['discNumber']), trackNumber: serializer.fromJson(json['trackNumber']), blocked: serializer.fromJson(json['blocked']), present: serializer.fromJson(json['present']), @@ -632,6 +643,7 @@ class MoorSong extends DataClass implements Insertable { 'path': serializer.toJson(path), 'duration': serializer.toJson(duration), 'albumArtPath': serializer.toJson(albumArtPath), + 'discNumber': serializer.toJson(discNumber), 'trackNumber': serializer.toJson(trackNumber), 'blocked': serializer.toJson(blocked), 'present': serializer.toJson(present), @@ -646,6 +658,7 @@ class MoorSong extends DataClass implements Insertable { String path, int duration, String albumArtPath, + int discNumber, int trackNumber, bool blocked, bool present}) => @@ -657,6 +670,7 @@ class MoorSong extends DataClass implements Insertable { path: path ?? this.path, duration: duration ?? this.duration, albumArtPath: albumArtPath ?? this.albumArtPath, + discNumber: discNumber ?? this.discNumber, trackNumber: trackNumber ?? this.trackNumber, blocked: blocked ?? this.blocked, present: present ?? this.present, @@ -671,6 +685,7 @@ class MoorSong extends DataClass implements Insertable { ..write('path: $path, ') ..write('duration: $duration, ') ..write('albumArtPath: $albumArtPath, ') + ..write('discNumber: $discNumber, ') ..write('trackNumber: $trackNumber, ') ..write('blocked: $blocked, ') ..write('present: $present') @@ -694,9 +709,11 @@ class MoorSong extends DataClass implements Insertable { $mrjc( albumArtPath.hashCode, $mrjc( - trackNumber.hashCode, - $mrjc(blocked.hashCode, - present.hashCode)))))))))); + discNumber.hashCode, + $mrjc( + trackNumber.hashCode, + $mrjc(blocked.hashCode, + present.hashCode))))))))))); @override bool operator ==(dynamic other) => identical(this, other) || @@ -708,6 +725,7 @@ class MoorSong extends DataClass implements Insertable { other.path == this.path && other.duration == this.duration && other.albumArtPath == this.albumArtPath && + other.discNumber == this.discNumber && other.trackNumber == this.trackNumber && other.blocked == this.blocked && other.present == this.present); @@ -721,6 +739,7 @@ class SongsCompanion extends UpdateCompanion { final Value path; final Value duration; final Value albumArtPath; + final Value discNumber; final Value trackNumber; final Value blocked; final Value present; @@ -732,6 +751,7 @@ class SongsCompanion extends UpdateCompanion { this.path = const Value.absent(), this.duration = const Value.absent(), this.albumArtPath = const Value.absent(), + this.discNumber = const Value.absent(), this.trackNumber = const Value.absent(), this.blocked = const Value.absent(), this.present = const Value.absent(), @@ -744,6 +764,7 @@ class SongsCompanion extends UpdateCompanion { @required String path, this.duration = const Value.absent(), this.albumArtPath = const Value.absent(), + this.discNumber = const Value.absent(), this.trackNumber = const Value.absent(), this.blocked = const Value.absent(), this.present = const Value.absent(), @@ -760,6 +781,7 @@ class SongsCompanion extends UpdateCompanion { Expression path, Expression duration, Expression albumArtPath, + Expression discNumber, Expression trackNumber, Expression blocked, Expression present, @@ -772,6 +794,7 @@ class SongsCompanion extends UpdateCompanion { if (path != null) 'path': path, if (duration != null) 'duration': duration, if (albumArtPath != null) 'album_art_path': albumArtPath, + if (discNumber != null) 'disc_number': discNumber, if (trackNumber != null) 'track_number': trackNumber, if (blocked != null) 'blocked': blocked, if (present != null) 'present': present, @@ -786,6 +809,7 @@ class SongsCompanion extends UpdateCompanion { Value path, Value duration, Value albumArtPath, + Value discNumber, Value trackNumber, Value blocked, Value present}) { @@ -797,6 +821,7 @@ class SongsCompanion extends UpdateCompanion { path: path ?? this.path, duration: duration ?? this.duration, albumArtPath: albumArtPath ?? this.albumArtPath, + discNumber: discNumber ?? this.discNumber, trackNumber: trackNumber ?? this.trackNumber, blocked: blocked ?? this.blocked, present: present ?? this.present, @@ -827,6 +852,9 @@ class SongsCompanion extends UpdateCompanion { if (albumArtPath.present) { map['album_art_path'] = Variable(albumArtPath.value); } + if (discNumber.present) { + map['disc_number'] = Variable(discNumber.value); + } if (trackNumber.present) { map['track_number'] = Variable(trackNumber.value); } @@ -849,6 +877,7 @@ class SongsCompanion extends UpdateCompanion { ..write('path: $path, ') ..write('duration: $duration, ') ..write('albumArtPath: $albumArtPath, ') + ..write('discNumber: $discNumber, ') ..write('trackNumber: $trackNumber, ') ..write('blocked: $blocked, ') ..write('present: $present') @@ -947,6 +976,18 @@ class $SongsTable extends Songs with TableInfo<$SongsTable, MoorSong> { ); } + final VerificationMeta _discNumberMeta = const VerificationMeta('discNumber'); + GeneratedIntColumn _discNumber; + @override + GeneratedIntColumn get discNumber => _discNumber ??= _constructDiscNumber(); + GeneratedIntColumn _constructDiscNumber() { + return GeneratedIntColumn( + 'disc_number', + $tableName, + true, + ); + } + final VerificationMeta _trackNumberMeta = const VerificationMeta('trackNumber'); GeneratedIntColumn _trackNumber; @@ -988,6 +1029,7 @@ class $SongsTable extends Songs with TableInfo<$SongsTable, MoorSong> { path, duration, albumArtPath, + discNumber, trackNumber, blocked, present @@ -1045,6 +1087,12 @@ class $SongsTable extends Songs with TableInfo<$SongsTable, MoorSong> { albumArtPath.isAcceptableOrUnknown( data['album_art_path'], _albumArtPathMeta)); } + if (data.containsKey('disc_number')) { + context.handle( + _discNumberMeta, + discNumber.isAcceptableOrUnknown( + data['disc_number'], _discNumberMeta)); + } if (data.containsKey('track_number')) { context.handle( _trackNumberMeta, diff --git a/lib/system/models/song_model.dart b/lib/system/models/song_model.dart index cee1bf3..3c9ba75 100644 --- a/lib/system/models/song_model.dart +++ b/lib/system/models/song_model.dart @@ -15,6 +15,7 @@ class SongModel extends Song { @required String path, @required int duration, @required bool blocked, + int discNumber, int trackNumber, String albumArtPath}) : super( @@ -24,6 +25,7 @@ class SongModel extends Song { path: path, duration: duration, blocked: blocked, + discNumber: discNumber, trackNumber: trackNumber, albumArtPath: albumArtPath, ); @@ -36,12 +38,14 @@ class SongModel extends Song { path: moorSong.path, duration: moorSong.duration, blocked: moorSong.blocked, + discNumber: moorSong.discNumber, trackNumber: moorSong.trackNumber, albumArtPath: moorSong.albumArtPath, ); factory SongModel.fromSongInfo(SongInfo songInfo) { final String duration = songInfo.duration; + final List numbers = _parseTrackNumber(songInfo.track); return SongModel( title: songInfo.title, @@ -51,8 +55,9 @@ class SongModel extends Song { path: songInfo.filePath, duration: duration == null ? null : int.parse(duration), blocked: false, + discNumber: numbers[0], + trackNumber: numbers[1], albumArtPath: songInfo.albumArtwork, - trackNumber: _parseTrackNumber(songInfo.track), ); } @@ -62,6 +67,9 @@ class SongModel extends Song { } final String artUri = mediaItem.artUri?.replaceFirst('file://', ''); + + final dn = mediaItem.extras['discNumber']; + int discNumber; final tn = mediaItem.extras['trackNumber']; int trackNumber; @@ -71,6 +79,12 @@ class SongModel extends Song { trackNumber = tn as int; } + if (dn == null) { + discNumber = null; + } else { + discNumber = dn as int; + } + return SongModel( title: mediaItem.title, album: mediaItem.album, @@ -79,8 +93,9 @@ class SongModel extends Song { path: mediaItem.id, duration: mediaItem.duration.inMilliseconds, blocked: mediaItem.extras['blocked'] == 'true', - albumArtPath: artUri, + discNumber: discNumber, trackNumber: trackNumber, + albumArtPath: artUri, ); } @@ -98,6 +113,7 @@ class SongModel extends Song { String path, int duration, bool blocked, + int discNumber, int trackNumber, String albumArtPath, int albumId, @@ -109,6 +125,7 @@ class SongModel extends Song { path: path ?? this.path, title: title ?? this.title, blocked: blocked ?? this.blocked, + discNumber: discNumber ?? this.discNumber, trackNumber: trackNumber ?? this.trackNumber, albumArtPath: albumArtPath ?? this.albumArtPath, albumId: albumId ?? this.albumId, @@ -122,8 +139,9 @@ class SongModel extends Song { path: Value(path), duration: Value(duration), blocked: Value(blocked), - albumArtPath: Value(albumArtPath), + discNumber: Value(discNumber), trackNumber: Value(trackNumber), + albumArtPath: Value(albumArtPath), ); SongsCompanion toMoorInsert() => SongsCompanion( @@ -134,6 +152,7 @@ class SongModel extends Song { path: Value(path), duration: Value(duration), albumArtPath: Value(albumArtPath), + discNumber: Value(discNumber), trackNumber: Value(trackNumber), // blocked: Value(blocked), present: const Value(true), @@ -149,21 +168,28 @@ class SongModel extends Song { extras: { 'albumId': albumId, 'blocked': blocked.toString(), + 'discNumber': discNumber, 'trackNumber': trackNumber, }); - static int _parseTrackNumber(String trackNumberString) { + static List _parseTrackNumber(String trackNumberString) { + int discNumber = 1; int trackNumber; + if (trackNumberString == null) { - return null; + return [null, null]; } trackNumber = int.tryParse(trackNumberString); if (trackNumber == null) { if (trackNumberString.contains('/')) { - trackNumber = int.tryParse(trackNumberString.split('/')[0]); + discNumber = int.tryParse(trackNumberString.split('/')[0]); + trackNumber = int.tryParse(trackNumberString.split('/')[1]); } + } else if (trackNumber > 1000) { + discNumber = int.tryParse(trackNumberString.substring(0, 1)); + trackNumber = int.tryParse(trackNumberString.substring(1)); } - return trackNumber; + return [discNumber, trackNumber]; } }