album of the day

This commit is contained in:
Moritz Weber 2021-06-26 19:18:07 +02:00
parent 7e21135bfe
commit 3fd97c882f
15 changed files with 394 additions and 86 deletions

View file

@ -46,7 +46,7 @@ linter:
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
- always_declare_return_types
- always_put_control_body_on_new_line
# - always_put_control_body_on_new_line
# - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
- always_require_non_null_named_parameters
##### - always_specify_types

View file

@ -13,6 +13,8 @@ abstract class MusicDataInfoRepository {
Stream<List<Album>> get albumStream;
Stream<List<Album>> getArtistAlbumStream(Artist artist);
// TODO: make this a stream? or call everytime on home screen?
Future<Album?> getAlbumOfDay();
Stream<List<Artist>> get artistStream;
}

View file

@ -73,6 +73,9 @@ abstract class _MusicDataStore with Store {
@observable
bool isUpdatingDatabase = false;
@observable
late ObservableFuture<Album?> albumOfDay = _musicDataInfoRepository.getAlbumOfDay().asObservable();
@action
Future<void> updateDatabase() async {
isUpdatingDatabase = true;

View file

@ -70,6 +70,21 @@ mixin _$MusicDataStore on _MusicDataStore, Store {
});
}
final _$albumOfDayAtom = Atom(name: '_MusicDataStore.albumOfDay');
@override
ObservableFuture<Album?> get albumOfDay {
_$albumOfDayAtom.reportRead();
return super.albumOfDay;
}
@override
set albumOfDay(ObservableFuture<Album?> value) {
_$albumOfDayAtom.reportWrite(value, super.albumOfDay, () {
super.albumOfDay = value;
});
}
final _$updateDatabaseAsyncAction =
AsyncAction('_MusicDataStore.updateDatabase');
@ -84,7 +99,8 @@ mixin _$MusicDataStore on _MusicDataStore, Store {
songStream: ${songStream},
albumStream: ${albumStream},
artistStream: ${artistStream},
isUpdatingDatabase: ${isUpdatingDatabase}
isUpdatingDatabase: ${isUpdatingDatabase},
albumOfDay: ${albumOfDay}
''';
}
}

View file

@ -1,78 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:get_it/get_it.dart';
import '../../domain/entities/album.dart';
import '../state/music_data_store.dart';
import '../theming.dart';
import '../utils.dart';
class Highlight extends StatelessWidget {
const Highlight({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 10,
child: AspectRatio(
aspectRatio: 1,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1.0),
),
child: const Image(
image: AssetImage('assets/no_cover.png'),
fit: BoxFit.cover,
final MusicDataStore store = GetIt.I<MusicDataStore>();
return Observer(
builder: (context) {
final Album? album = store.albumOfDay.value;
if (album == null) return Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 10,
child: AspectRatio(
aspectRatio: 1,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1.0),
),
child: Image(
image: getAlbumImage(album.albumArtPath),
fit: BoxFit.cover,
),
),
),
),
),
Expanded(
flex: 23,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Album of the Day'.toUpperCase(),
style: TEXT_SMALL_HEADLINE,
),
Container(height: 6.0),
Text(
'All Our Gods Have Abandoned Us',
style: Theme.of(context).textTheme.headline4,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Text(
'Architects',
style: TEXT_SMALL_SUBTITLE,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
Expanded(
flex: 23,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Album of the Day'.toUpperCase(),
style: TEXT_SMALL_HEADLINE,
),
Container(height: 6.0),
Text(
album.title,
style: Theme.of(context).textTheme.headline4,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Text(
album.artist,
style: TEXT_SMALL_SUBTITLE,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
IconButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.play_circle_fill_rounded,
size: 48.0,
IconButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.play_circle_fill_rounded,
size: 48.0,
),
iconSize: 48.0,
onPressed: () {},
),
iconSize: 48.0,
onPressed: () {},
),
],
)
],
),
],
)
],
);
},
);
}
}

View file

@ -8,7 +8,7 @@ import '../music_data_source_contract.dart';
part 'music_data_dao.g.dart';
@UseDao(tables: [Albums, Artists, Songs])
@UseDao(tables: [Albums, Artists, Songs, MoorAlbumOfDay])
class MusicDataDao extends DatabaseAccessor<MoorDatabase>
with _$MusicDataDaoMixin
implements MusicDataSource {
@ -97,8 +97,8 @@ class MusicDataDao extends DatabaseAccessor<MoorDatabase>
@override
Future<SongModel> getSongByPath(String path) async {
return (select(songs)..where((t) => t.path.equals(path))).getSingle().then(
(moorSong) => SongModel.fromMoor(moorSong),
);
(moorSong) => SongModel.fromMoor(moorSong),
);
}
@override
@ -270,4 +270,36 @@ class MusicDataDao extends DatabaseAccessor<MoorDatabase>
await (update(songs)..where((tbl) => tbl.path.equals(songModel.path)))
.write(songModel.toSongsCompanion());
}
@override
Future<AlbumOfDay?> getAlbumOfDay() async {
final query = select(moorAlbumOfDay)
.join([innerJoin(albums, albums.id.equalsExp(moorAlbumOfDay.albumId))]);
return (query..limit(1)).getSingleOrNull().then(
(result) {
if (result == null)
return null;
return AlbumOfDay(
AlbumModel.fromMoor(result.readTable(albums)),
DateTime.fromMillisecondsSinceEpoch(
result.readTable(moorAlbumOfDay).milliSecSinceEpoch,
),
);
},
);
}
@override
Future<void> setAlbumOfDay(AlbumOfDay albumOfDay) async {
transaction(() async {
await delete(moorAlbumOfDay).go();
into(moorAlbumOfDay).insert(
MoorAlbumOfDayCompanion(
albumId: Value(albumOfDay.albumModel.id),
milliSecSinceEpoch: Value(albumOfDay.date.millisecondsSinceEpoch),
),
);
});
}
}

View file

@ -10,4 +10,5 @@ mixin _$MusicDataDaoMixin on DatabaseAccessor<MoorDatabase> {
$AlbumsTable get albums => attachedDatabase.albums;
$ArtistsTable get artists => attachedDatabase.artists;
$SongsTable get songs => attachedDatabase.songs;
$MoorAlbumOfDayTable get moorAlbumOfDay => attachedDatabase.moorAlbumOfDay;
}

View file

@ -114,17 +114,19 @@ class PersistentStateDao extends DatabaseAccessor<MoorDatabase>
@override
Future<int> get currentIndex async {
return select(persistentIndex).getSingle().then((event) => event.index ?? 0);
return (select(persistentIndex)..limit(1)).getSingleOrNull().then((event) => event?.index ?? 0);
}
@override
Future<void> setCurrentIndex(int index) async {
await delete(persistentIndex).go();
into(persistentIndex).insert(PersistentIndexCompanion(index: Value(index)));
transaction(() async {
await delete(persistentIndex).go();
into(persistentIndex).insert(PersistentIndexCompanion(index: Value(index)));
});
}
@override
Future<LoopMode> get loopMode {
Future<LoopMode> get loopMode async {
return select(persistentLoopMode).getSingle().then((event) => event.loopMode.toLoopMode());
}

View file

@ -104,6 +104,14 @@ class LibraryFolders extends Table {
TextColumn get path => text()();
}
class MoorAlbumOfDay extends Table {
IntColumn get albumId => integer()();
IntColumn get milliSecSinceEpoch => integer()();
@override
Set<Column> get primaryKey => {albumId};
}
@UseMoor(
tables: [
Albums,
@ -116,6 +124,7 @@ class LibraryFolders extends Table {
PersistentLoopMode,
PersistentShuffleMode,
Songs,
MoorAlbumOfDay,
],
daos: [
PersistentStateDao,

View file

@ -2555,6 +2555,202 @@ class $SongsTable extends Songs with TableInfo<$SongsTable, MoorSong> {
}
}
class MoorAlbumOfDayData extends DataClass
implements Insertable<MoorAlbumOfDayData> {
final int albumId;
final int milliSecSinceEpoch;
MoorAlbumOfDayData({required this.albumId, required this.milliSecSinceEpoch});
factory MoorAlbumOfDayData.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
{String? prefix}) {
final effectivePrefix = prefix ?? '';
return MoorAlbumOfDayData(
albumId: const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}album_id'])!,
milliSecSinceEpoch: const IntType().mapFromDatabaseResponse(
data['${effectivePrefix}milli_sec_since_epoch'])!,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['album_id'] = Variable<int>(albumId);
map['milli_sec_since_epoch'] = Variable<int>(milliSecSinceEpoch);
return map;
}
MoorAlbumOfDayCompanion toCompanion(bool nullToAbsent) {
return MoorAlbumOfDayCompanion(
albumId: Value(albumId),
milliSecSinceEpoch: Value(milliSecSinceEpoch),
);
}
factory MoorAlbumOfDayData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return MoorAlbumOfDayData(
albumId: serializer.fromJson<int>(json['albumId']),
milliSecSinceEpoch: serializer.fromJson<int>(json['milliSecSinceEpoch']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'albumId': serializer.toJson<int>(albumId),
'milliSecSinceEpoch': serializer.toJson<int>(milliSecSinceEpoch),
};
}
MoorAlbumOfDayData copyWith({int? albumId, int? milliSecSinceEpoch}) =>
MoorAlbumOfDayData(
albumId: albumId ?? this.albumId,
milliSecSinceEpoch: milliSecSinceEpoch ?? this.milliSecSinceEpoch,
);
@override
String toString() {
return (StringBuffer('MoorAlbumOfDayData(')
..write('albumId: $albumId, ')
..write('milliSecSinceEpoch: $milliSecSinceEpoch')
..write(')'))
.toString();
}
@override
int get hashCode =>
$mrjf($mrjc(albumId.hashCode, milliSecSinceEpoch.hashCode));
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MoorAlbumOfDayData &&
other.albumId == this.albumId &&
other.milliSecSinceEpoch == this.milliSecSinceEpoch);
}
class MoorAlbumOfDayCompanion extends UpdateCompanion<MoorAlbumOfDayData> {
final Value<int> albumId;
final Value<int> milliSecSinceEpoch;
const MoorAlbumOfDayCompanion({
this.albumId = const Value.absent(),
this.milliSecSinceEpoch = const Value.absent(),
});
MoorAlbumOfDayCompanion.insert({
this.albumId = const Value.absent(),
required int milliSecSinceEpoch,
}) : milliSecSinceEpoch = Value(milliSecSinceEpoch);
static Insertable<MoorAlbumOfDayData> custom({
Expression<int>? albumId,
Expression<int>? milliSecSinceEpoch,
}) {
return RawValuesInsertable({
if (albumId != null) 'album_id': albumId,
if (milliSecSinceEpoch != null)
'milli_sec_since_epoch': milliSecSinceEpoch,
});
}
MoorAlbumOfDayCompanion copyWith(
{Value<int>? albumId, Value<int>? milliSecSinceEpoch}) {
return MoorAlbumOfDayCompanion(
albumId: albumId ?? this.albumId,
milliSecSinceEpoch: milliSecSinceEpoch ?? this.milliSecSinceEpoch,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (albumId.present) {
map['album_id'] = Variable<int>(albumId.value);
}
if (milliSecSinceEpoch.present) {
map['milli_sec_since_epoch'] = Variable<int>(milliSecSinceEpoch.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('MoorAlbumOfDayCompanion(')
..write('albumId: $albumId, ')
..write('milliSecSinceEpoch: $milliSecSinceEpoch')
..write(')'))
.toString();
}
}
class $MoorAlbumOfDayTable extends MoorAlbumOfDay
with TableInfo<$MoorAlbumOfDayTable, MoorAlbumOfDayData> {
final GeneratedDatabase _db;
final String? _alias;
$MoorAlbumOfDayTable(this._db, [this._alias]);
final VerificationMeta _albumIdMeta = const VerificationMeta('albumId');
@override
late final GeneratedIntColumn albumId = _constructAlbumId();
GeneratedIntColumn _constructAlbumId() {
return GeneratedIntColumn(
'album_id',
$tableName,
false,
);
}
final VerificationMeta _milliSecSinceEpochMeta =
const VerificationMeta('milliSecSinceEpoch');
@override
late final GeneratedIntColumn milliSecSinceEpoch =
_constructMilliSecSinceEpoch();
GeneratedIntColumn _constructMilliSecSinceEpoch() {
return GeneratedIntColumn(
'milli_sec_since_epoch',
$tableName,
false,
);
}
@override
List<GeneratedColumn> get $columns => [albumId, milliSecSinceEpoch];
@override
$MoorAlbumOfDayTable get asDslTable => this;
@override
String get $tableName => _alias ?? 'moor_album_of_day';
@override
final String actualTableName = 'moor_album_of_day';
@override
VerificationContext validateIntegrity(Insertable<MoorAlbumOfDayData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('album_id')) {
context.handle(_albumIdMeta,
albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta));
}
if (data.containsKey('milli_sec_since_epoch')) {
context.handle(
_milliSecSinceEpochMeta,
milliSecSinceEpoch.isAcceptableOrUnknown(
data['milli_sec_since_epoch']!, _milliSecSinceEpochMeta));
} else if (isInserting) {
context.missing(_milliSecSinceEpochMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {albumId};
@override
MoorAlbumOfDayData map(Map<String, dynamic> data, {String? tablePrefix}) {
return MoorAlbumOfDayData.fromData(data, _db,
prefix: tablePrefix != null ? '$tablePrefix.' : null);
}
@override
$MoorAlbumOfDayTable createAlias(String alias) {
return $MoorAlbumOfDayTable(_db, alias);
}
}
abstract class _$MoorDatabase extends GeneratedDatabase {
_$MoorDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
_$MoorDatabase.connect(DatabaseConnection c) : super.connect(c);
@ -2573,6 +2769,7 @@ abstract class _$MoorDatabase extends GeneratedDatabase {
late final $PersistentShuffleModeTable persistentShuffleMode =
$PersistentShuffleModeTable(this);
late final $SongsTable songs = $SongsTable(this);
late final $MoorAlbumOfDayTable moorAlbumOfDay = $MoorAlbumOfDayTable(this);
late final PersistentStateDao persistentStateDao =
PersistentStateDao(this as MoorDatabase);
late final SettingsDao settingsDao = SettingsDao(this as MoorDatabase);
@ -2590,6 +2787,7 @@ abstract class _$MoorDatabase extends GeneratedDatabase {
persistentIndex,
persistentLoopMode,
persistentShuffleMode,
songs
songs,
moorAlbumOfDay
];
}

View file

@ -39,4 +39,8 @@ abstract class MusicDataSource {
Future<int> insertArtist(ArtistModel artistModel);
Future<void> insertArtists(List<ArtistModel> artistModels);
Future<void> deleteAllArtists();
// TODO: is this the right place? maybe persistent state?
Future<void> setAlbumOfDay(AlbumOfDay albumOfDay);
Future<AlbumOfDay?> getAlbumOfDay();
}

View file

@ -84,3 +84,10 @@ class AlbumModel extends Album {
}
}
}
class AlbumOfDay {
AlbumOfDay(this.albumModel, this.date);
final AlbumModel albumModel;
final DateTime date;
}

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:fimber/fimber.dart';
import 'package:rxdart/rxdart.dart';
@ -198,4 +200,24 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
return -a.pubYear!.compareTo(b.pubYear!);
});
}
@override
Future<Album?> getAlbumOfDay() async {
final storedAlbum = await _musicDataSource.getAlbumOfDay();
if (storedAlbum == null || !_isAlbumValid(storedAlbum)) {
final albums = await _musicDataSource.getAlbums();
if (albums.isNotEmpty) {
final rng = Random();
final index = rng.nextInt(albums.length);
_musicDataSource.setAlbumOfDay(AlbumOfDay(albums[index], DateTime.now()));
return albums[index];
}
} else {
return storedAlbum.albumModel;
}
}
bool _isAlbumValid(AlbumOfDay albumOfDay) {
return albumOfDay.date.difference(DateTime.now()).inDays < 1;
}
}

View file

@ -270,9 +270,9 @@ packages:
flutter_mobx:
dependency: "direct main"
description:
name: flutter_mobx
url: "https://pub.dartlang.org"
source: hosted
path: "../mobx.dart/flutter_mobx"
relative: true
source: path
version: "2.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
@ -420,16 +420,16 @@ packages:
mobx:
dependency: "direct main"
description:
name: mobx
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
path: "../mobx.dart/mobx"
relative: true
source: path
version: "2.0.1+1"
mobx_codegen:
dependency: "direct dev"
description:
name: mobx_codegen
url: "https://pub.dartlang.org"
source: hosted
path: "../mobx.dart/mobx_codegen"
relative: true
source: path
version: "2.0.1+3"
mockito:
dependency: "direct dev"

View file

@ -22,12 +22,12 @@ dependencies:
sdk: flutter
flutter_fimber: ^0.6.3
flutter_fimber_filelogger: ^2.0.0
flutter_mobx: ^2.0.0
# path: ../mobx.dart/flutter_mobx
flutter_mobx: # ^2.0.0
path: ../mobx.dart/flutter_mobx
get_it: ^7.1.3
just_audio: ^0.7.5
mobx: ^2.0.1
# path: ../mobx.dart/mobx
mobx: # ^2.0.1
path: ../mobx.dart/mobx
moor: ^4.3.2
path: ^1.8.0
path_provider: ^2.0.2
@ -39,8 +39,8 @@ dev_dependencies:
build_runner: ^2.0.4
flutter_test:
sdk: flutter
mobx_codegen: ^2.0.1+3
# path: ../mobx.dart/mobx_codegen
mobx_codegen: #^2.0.1+3
path: ../mobx.dart/mobx_codegen
mockito: ^5.0.10
moor_generator: ^4.3.1