mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor(stats): migrate stats to use drift db
This commit is contained in:
parent
44418868ad
commit
08ac29c979
@ -8,3 +8,10 @@ targets:
|
|||||||
options:
|
options:
|
||||||
any_map: true
|
any_map: true
|
||||||
explicit_to_json: true
|
explicit_to_json: true
|
||||||
|
drift_dev:
|
||||||
|
options:
|
||||||
|
sql:
|
||||||
|
dialect: sqlite
|
||||||
|
options:
|
||||||
|
modules:
|
||||||
|
- json1
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/models/spotify/home_feed.dart';
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
import 'package:spotube/provider/history/summary.dart';
|
||||||
|
|
||||||
abstract class FakeData {
|
abstract class FakeData {
|
||||||
static final Image image = Image()
|
static final Image image = Image()
|
||||||
@ -222,4 +224,36 @@ abstract class FakeData {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static const historySummary = PlaybackHistorySummary(
|
||||||
|
albums: 1,
|
||||||
|
artists: 1,
|
||||||
|
duration: Duration(seconds: 1),
|
||||||
|
playlists: 1,
|
||||||
|
tracks: 1,
|
||||||
|
fees: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedPlaylist = HistoryTableData(
|
||||||
|
id: 0,
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
itemId: "1",
|
||||||
|
data: playlist.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedAlbum = HistoryTableData(
|
||||||
|
id: 0,
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
itemId: "1",
|
||||||
|
data: album.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final historyRecentlyPlayedItems = List.generate(
|
||||||
|
10,
|
||||||
|
(index) => index % 2 == 0
|
||||||
|
? historyRecentlyPlayedPlaylist
|
||||||
|
: historyRecentlyPlayedAlbum,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
final audioSource =
|
final audioSource =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
|||||||
|
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.watch(playbackHistoryActionsProvider);
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
@ -27,6 +27,7 @@ part 'tables/scrobbler.dart';
|
|||||||
part 'tables/skip_segment.dart';
|
part 'tables/skip_segment.dart';
|
||||||
part 'tables/source_match.dart';
|
part 'tables/source_match.dart';
|
||||||
part 'tables/audio_player_state.dart';
|
part 'tables/audio_player_state.dart';
|
||||||
|
part 'tables/history.dart';
|
||||||
|
|
||||||
part 'typeconverters/color.dart';
|
part 'typeconverters/color.dart';
|
||||||
part 'typeconverters/locale.dart';
|
part 'typeconverters/locale.dart';
|
||||||
@ -45,6 +46,7 @@ part 'typeconverters/map.dart';
|
|||||||
AudioPlayerStateTable,
|
AudioPlayerStateTable,
|
||||||
PlaylistTable,
|
PlaylistTable,
|
||||||
PlaylistMediaTable,
|
PlaylistMediaTable,
|
||||||
|
HistoryTable,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
@ -3414,6 +3414,301 @@ class PlaylistMediaTableCompanion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class $HistoryTableTable extends HistoryTable
|
||||||
|
with TableInfo<$HistoryTableTable, HistoryTableData> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$HistoryTableTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints:
|
||||||
|
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||||
|
static const VerificationMeta _createdAtMeta =
|
||||||
|
const VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||||
|
'created_at', aliasedName, false,
|
||||||
|
type: DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: currentDateAndTime);
|
||||||
|
static const VerificationMeta _typeMeta = const VerificationMeta('type');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumnWithTypeConverter<HistoryEntryType, String> type =
|
||||||
|
GeneratedColumn<String>('type', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||||
|
.withConverter<HistoryEntryType>($HistoryTableTable.$convertertype);
|
||||||
|
static const VerificationMeta _itemIdMeta = const VerificationMeta('itemId');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> itemId = GeneratedColumn<String>(
|
||||||
|
'item_id', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _dataMeta = const VerificationMeta('data');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumnWithTypeConverter<Map<String, dynamic>, String>
|
||||||
|
data = GeneratedColumn<String>('data', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||||
|
.withConverter<Map<String, dynamic>>(
|
||||||
|
$HistoryTableTable.$converterdata);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [id, createdAt, type, itemId, data];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'history_table';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(Insertable<HistoryTableData> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
context.handle(_typeMeta, const VerificationResult.success());
|
||||||
|
if (data.containsKey('item_id')) {
|
||||||
|
context.handle(_itemIdMeta,
|
||||||
|
itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_itemIdMeta);
|
||||||
|
}
|
||||||
|
context.handle(_dataMeta, const VerificationResult.success());
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
HistoryTableData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return HistoryTableData(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
type: $HistoryTableTable.$convertertype.fromSql(attachedDatabase
|
||||||
|
.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}type'])!),
|
||||||
|
itemId: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}item_id'])!,
|
||||||
|
data: $HistoryTableTable.$converterdata.fromSql(attachedDatabase
|
||||||
|
.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}data'])!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$HistoryTableTable createAlias(String alias) {
|
||||||
|
return $HistoryTableTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsonTypeConverter2<HistoryEntryType, String, String> $convertertype =
|
||||||
|
const EnumNameConverter<HistoryEntryType>(HistoryEntryType.values);
|
||||||
|
static TypeConverter<Map<String, dynamic>, String> $converterdata =
|
||||||
|
const MapTypeConverter();
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryTableData extends DataClass
|
||||||
|
implements Insertable<HistoryTableData> {
|
||||||
|
final int id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final HistoryEntryType type;
|
||||||
|
final String itemId;
|
||||||
|
final Map<String, dynamic> data;
|
||||||
|
const HistoryTableData(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.type,
|
||||||
|
required this.itemId,
|
||||||
|
required this.data});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['id'] = Variable<int>(id);
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
|
{
|
||||||
|
map['type'] =
|
||||||
|
Variable<String>($HistoryTableTable.$convertertype.toSql(type));
|
||||||
|
}
|
||||||
|
map['item_id'] = Variable<String>(itemId);
|
||||||
|
{
|
||||||
|
map['data'] =
|
||||||
|
Variable<String>($HistoryTableTable.$converterdata.toSql(data));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryTableCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return HistoryTableCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
type: Value(type),
|
||||||
|
itemId: Value(itemId),
|
||||||
|
data: Value(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory HistoryTableData.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return HistoryTableData(
|
||||||
|
id: serializer.fromJson<int>(json['id']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
type: $HistoryTableTable.$convertertype
|
||||||
|
.fromJson(serializer.fromJson<String>(json['type'])),
|
||||||
|
itemId: serializer.fromJson<String>(json['itemId']),
|
||||||
|
data: serializer.fromJson<Map<String, dynamic>>(json['data']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<int>(id),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'type': serializer
|
||||||
|
.toJson<String>($HistoryTableTable.$convertertype.toJson(type)),
|
||||||
|
'itemId': serializer.toJson<String>(itemId),
|
||||||
|
'data': serializer.toJson<Map<String, dynamic>>(data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryTableData copyWith(
|
||||||
|
{int? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
HistoryEntryType? type,
|
||||||
|
String? itemId,
|
||||||
|
Map<String, dynamic>? data}) =>
|
||||||
|
HistoryTableData(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
type: type ?? this.type,
|
||||||
|
itemId: itemId ?? this.itemId,
|
||||||
|
data: data ?? this.data,
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('HistoryTableData(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('type: $type, ')
|
||||||
|
..write('itemId: $itemId, ')
|
||||||
|
..write('data: $data')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, createdAt, type, itemId, data);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is HistoryTableData &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.type == this.type &&
|
||||||
|
other.itemId == this.itemId &&
|
||||||
|
other.data == this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryTableCompanion extends UpdateCompanion<HistoryTableData> {
|
||||||
|
final Value<int> id;
|
||||||
|
final Value<DateTime> createdAt;
|
||||||
|
final Value<HistoryEntryType> type;
|
||||||
|
final Value<String> itemId;
|
||||||
|
final Value<Map<String, dynamic>> data;
|
||||||
|
const HistoryTableCompanion({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.type = const Value.absent(),
|
||||||
|
this.itemId = const Value.absent(),
|
||||||
|
this.data = const Value.absent(),
|
||||||
|
});
|
||||||
|
HistoryTableCompanion.insert({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
required HistoryEntryType type,
|
||||||
|
required String itemId,
|
||||||
|
required Map<String, dynamic> data,
|
||||||
|
}) : type = Value(type),
|
||||||
|
itemId = Value(itemId),
|
||||||
|
data = Value(data);
|
||||||
|
static Insertable<HistoryTableData> custom({
|
||||||
|
Expression<int>? id,
|
||||||
|
Expression<DateTime>? createdAt,
|
||||||
|
Expression<String>? type,
|
||||||
|
Expression<String>? itemId,
|
||||||
|
Expression<String>? data,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (type != null) 'type': type,
|
||||||
|
if (itemId != null) 'item_id': itemId,
|
||||||
|
if (data != null) 'data': data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryTableCompanion copyWith(
|
||||||
|
{Value<int>? id,
|
||||||
|
Value<DateTime>? createdAt,
|
||||||
|
Value<HistoryEntryType>? type,
|
||||||
|
Value<String>? itemId,
|
||||||
|
Value<Map<String, dynamic>>? data}) {
|
||||||
|
return HistoryTableCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
type: type ?? this.type,
|
||||||
|
itemId: itemId ?? this.itemId,
|
||||||
|
data: data ?? this.data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = Variable<int>(id.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (type.present) {
|
||||||
|
map['type'] =
|
||||||
|
Variable<String>($HistoryTableTable.$convertertype.toSql(type.value));
|
||||||
|
}
|
||||||
|
if (itemId.present) {
|
||||||
|
map['item_id'] = Variable<String>(itemId.value);
|
||||||
|
}
|
||||||
|
if (data.present) {
|
||||||
|
map['data'] =
|
||||||
|
Variable<String>($HistoryTableTable.$converterdata.toSql(data.value));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('HistoryTableCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('type: $type, ')
|
||||||
|
..write('itemId: $itemId, ')
|
||||||
|
..write('data: $data')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
_$AppDatabase(QueryExecutor e) : super(e);
|
_$AppDatabase(QueryExecutor e) : super(e);
|
||||||
_$AppDatabaseManager get managers => _$AppDatabaseManager(this);
|
_$AppDatabaseManager get managers => _$AppDatabaseManager(this);
|
||||||
@ -3432,6 +3727,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
late final $PlaylistTableTable playlistTable = $PlaylistTableTable(this);
|
late final $PlaylistTableTable playlistTable = $PlaylistTableTable(this);
|
||||||
late final $PlaylistMediaTableTable playlistMediaTable =
|
late final $PlaylistMediaTableTable playlistMediaTable =
|
||||||
$PlaylistMediaTableTable(this);
|
$PlaylistMediaTableTable(this);
|
||||||
|
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
||||||
late final Index uniqueBlacklist = Index('unique_blacklist',
|
late final Index uniqueBlacklist = Index('unique_blacklist',
|
||||||
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
||||||
late final Index uniqTrackMatch = Index('uniq_track_match',
|
late final Index uniqTrackMatch = Index('uniq_track_match',
|
||||||
@ -3450,6 +3746,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
audioPlayerStateTable,
|
audioPlayerStateTable,
|
||||||
playlistTable,
|
playlistTable,
|
||||||
playlistMediaTable,
|
playlistMediaTable,
|
||||||
|
historyTable,
|
||||||
uniqueBlacklist,
|
uniqueBlacklist,
|
||||||
uniqTrackMatch
|
uniqTrackMatch
|
||||||
];
|
];
|
||||||
@ -5053,6 +5350,148 @@ class $$PlaylistMediaTableTableOrderingComposer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef $$HistoryTableTableInsertCompanionBuilder = HistoryTableCompanion
|
||||||
|
Function({
|
||||||
|
Value<int> id,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
required HistoryEntryType type,
|
||||||
|
required String itemId,
|
||||||
|
required Map<String, dynamic> data,
|
||||||
|
});
|
||||||
|
typedef $$HistoryTableTableUpdateCompanionBuilder = HistoryTableCompanion
|
||||||
|
Function({
|
||||||
|
Value<int> id,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<HistoryEntryType> type,
|
||||||
|
Value<String> itemId,
|
||||||
|
Value<Map<String, dynamic>> data,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $$HistoryTableTableTableManager extends RootTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$HistoryTableTable,
|
||||||
|
HistoryTableData,
|
||||||
|
$$HistoryTableTableFilterComposer,
|
||||||
|
$$HistoryTableTableOrderingComposer,
|
||||||
|
$$HistoryTableTableProcessedTableManager,
|
||||||
|
$$HistoryTableTableInsertCompanionBuilder,
|
||||||
|
$$HistoryTableTableUpdateCompanionBuilder> {
|
||||||
|
$$HistoryTableTableTableManager(_$AppDatabase db, $HistoryTableTable table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
filteringComposer:
|
||||||
|
$$HistoryTableTableFilterComposer(ComposerState(db, table)),
|
||||||
|
orderingComposer:
|
||||||
|
$$HistoryTableTableOrderingComposer(ComposerState(db, table)),
|
||||||
|
getChildManagerBuilder: (p) =>
|
||||||
|
$$HistoryTableTableProcessedTableManager(p),
|
||||||
|
getUpdateCompanionBuilder: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<HistoryEntryType> type = const Value.absent(),
|
||||||
|
Value<String> itemId = const Value.absent(),
|
||||||
|
Value<Map<String, dynamic>> data = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
HistoryTableCompanion(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
type: type,
|
||||||
|
itemId: itemId,
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
getInsertCompanionBuilder: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
required HistoryEntryType type,
|
||||||
|
required String itemId,
|
||||||
|
required Map<String, dynamic> data,
|
||||||
|
}) =>
|
||||||
|
HistoryTableCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
type: type,
|
||||||
|
itemId: itemId,
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$HistoryTableTableProcessedTableManager extends ProcessedTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$HistoryTableTable,
|
||||||
|
HistoryTableData,
|
||||||
|
$$HistoryTableTableFilterComposer,
|
||||||
|
$$HistoryTableTableOrderingComposer,
|
||||||
|
$$HistoryTableTableProcessedTableManager,
|
||||||
|
$$HistoryTableTableInsertCompanionBuilder,
|
||||||
|
$$HistoryTableTableUpdateCompanionBuilder> {
|
||||||
|
$$HistoryTableTableProcessedTableManager(super.$state);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$HistoryTableTableFilterComposer
|
||||||
|
extends FilterComposer<_$AppDatabase, $HistoryTableTable> {
|
||||||
|
$$HistoryTableTableFilterComposer(super.$state);
|
||||||
|
ColumnFilters<int> get id => $state.composableBuilder(
|
||||||
|
column: $state.table.id,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
|
||||||
|
column: $state.table.createdAt,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnWithTypeConverterFilters<HistoryEntryType, HistoryEntryType, String>
|
||||||
|
get type => $state.composableBuilder(
|
||||||
|
column: $state.table.type,
|
||||||
|
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||||
|
column,
|
||||||
|
joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnFilters<String> get itemId => $state.composableBuilder(
|
||||||
|
column: $state.table.itemId,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnWithTypeConverterFilters<Map<String, dynamic>, Map<String, dynamic>,
|
||||||
|
String>
|
||||||
|
get data => $state.composableBuilder(
|
||||||
|
column: $state.table.data,
|
||||||
|
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||||
|
column,
|
||||||
|
joinBuilders: joinBuilders));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$HistoryTableTableOrderingComposer
|
||||||
|
extends OrderingComposer<_$AppDatabase, $HistoryTableTable> {
|
||||||
|
$$HistoryTableTableOrderingComposer(super.$state);
|
||||||
|
ColumnOrderings<int> get id => $state.composableBuilder(
|
||||||
|
column: $state.table.id,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
|
||||||
|
column: $state.table.createdAt,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get type => $state.composableBuilder(
|
||||||
|
column: $state.table.type,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get itemId => $state.composableBuilder(
|
||||||
|
column: $state.table.itemId,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get data => $state.composableBuilder(
|
||||||
|
column: $state.table.data,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||||
|
}
|
||||||
|
|
||||||
class _$AppDatabaseManager {
|
class _$AppDatabaseManager {
|
||||||
final _$AppDatabase _db;
|
final _$AppDatabase _db;
|
||||||
_$AppDatabaseManager(this._db);
|
_$AppDatabaseManager(this._db);
|
||||||
@ -5074,4 +5513,6 @@ class _$AppDatabaseManager {
|
|||||||
$$PlaylistTableTableTableManager(_db, _db.playlistTable);
|
$$PlaylistTableTableTableManager(_db, _db.playlistTable);
|
||||||
$$PlaylistMediaTableTableTableManager get playlistMediaTable =>
|
$$PlaylistMediaTableTableTableManager get playlistMediaTable =>
|
||||||
$$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable);
|
$$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable);
|
||||||
|
$$HistoryTableTableTableManager get historyTable =>
|
||||||
|
$$HistoryTableTableTableManager(_db, _db.historyTable);
|
||||||
}
|
}
|
||||||
|
25
lib/models/database/tables/history.dart
Normal file
25
lib/models/database/tables/history.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
part of '../database.dart';
|
||||||
|
|
||||||
|
enum HistoryEntryType {
|
||||||
|
playlist,
|
||||||
|
album,
|
||||||
|
track,
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryTable extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
TextColumn get type => textEnum<HistoryEntryType>()();
|
||||||
|
TextColumn get itemId => text()();
|
||||||
|
TextColumn get data =>
|
||||||
|
text().map(const MapTypeConverter<String, dynamic>())();
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HistoryItemParseExtension on HistoryTableData {
|
||||||
|
PlaylistSimple? get playlist =>
|
||||||
|
type == HistoryEntryType.playlist ? PlaylistSimple.fromJson(data) : null;
|
||||||
|
AlbumSimple? get album =>
|
||||||
|
type == HistoryEntryType.album ? AlbumSimple.fromJson(data) : null;
|
||||||
|
Track? get track =>
|
||||||
|
type == HistoryEntryType.track ? Track.fromJson(data) : null;
|
||||||
|
}
|
@ -35,7 +35,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.read(playbackHistoryActionsProvider);
|
||||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
|
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/history/recent.dart';
|
import 'package:spotube/provider/history/recent.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
|
||||||
|
|
||||||
class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
||||||
const HomeRecentlyPlayedSection({super.key});
|
const HomeRecentlyPlayedSection({super.key});
|
||||||
@ -10,23 +12,28 @@ class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final history = ref.watch(recentlyPlayedItems);
|
final history = ref.watch(recentlyPlayedItems);
|
||||||
|
final historyData =
|
||||||
|
history.asData?.value ?? FakeData.historyRecentlyPlayedItems;
|
||||||
|
|
||||||
if (history.isEmpty) {
|
if (history.asData?.value.isEmpty == true) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView(
|
return Skeletonizer(
|
||||||
title: const Text('Recently Played'),
|
enabled: history.isLoading,
|
||||||
items: [
|
child: HorizontalPlaybuttonCardView(
|
||||||
for (final item in history)
|
title: const Text('Recently Played'),
|
||||||
if (item is PlaybackHistoryPlaylist)
|
items: [
|
||||||
item.playlist
|
for (final item in historyData)
|
||||||
else if (item is PlaybackHistoryAlbum)
|
if (item.playlist != null)
|
||||||
item.album
|
item.playlist
|
||||||
],
|
else if (item.album != null)
|
||||||
hasNextPage: false,
|
item.album
|
||||||
isLoadingNextPage: false,
|
],
|
||||||
onFetchMore: () {},
|
hasNextPage: false,
|
||||||
|
isLoadingNextPage: false,
|
||||||
|
onFetchMore: () {},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
final playlistQueue = ref.watch(audioPlayerProvider);
|
final playlistQueue = ref.watch(audioPlayerProvider);
|
||||||
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
|
||||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
final historyNotifier = ref.read(playbackHistoryActionsProvider);
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/modules/stats/summary/summary_card.dart';
|
import 'package:spotube/modules/stats/summary/summary_card.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
@ -18,83 +20,87 @@ class StatsPageSummarySection extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final summary = ref.watch(playbackHistorySummaryProvider);
|
final summary = ref.watch(playbackHistorySummaryProvider);
|
||||||
|
final summaryData = summary.asData?.value ?? FakeData.historySummary;
|
||||||
|
|
||||||
return SliverPadding(
|
return Skeletonizer.sliver(
|
||||||
padding: const EdgeInsets.all(10),
|
enabled: summary.isLoading,
|
||||||
sliver: SliverLayoutBuilder(builder: (context, constrains) {
|
child: SliverPadding(
|
||||||
return SliverGrid(
|
padding: const EdgeInsets.all(10),
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
sliver: SliverLayoutBuilder(builder: (context, constrains) {
|
||||||
crossAxisCount: constrains.isXs
|
return SliverGrid(
|
||||||
? 2
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
: constrains.smAndDown
|
crossAxisCount: constrains.isXs
|
||||||
? 3
|
? 2
|
||||||
: constrains.mdAndDown
|
: constrains.smAndDown
|
||||||
? 4
|
? 3
|
||||||
: constrains.lgAndDown
|
: constrains.mdAndDown
|
||||||
? 5
|
? 4
|
||||||
: 6,
|
: constrains.lgAndDown
|
||||||
mainAxisSpacing: 10,
|
? 5
|
||||||
crossAxisSpacing: 10,
|
: 6,
|
||||||
childAspectRatio: constrains.isXs ? 1.3 : 1.5,
|
mainAxisSpacing: 10,
|
||||||
),
|
crossAxisSpacing: 10,
|
||||||
delegate: SliverChildListDelegate([
|
childAspectRatio: constrains.isXs ? 1.3 : 1.5,
|
||||||
SummaryCard(
|
|
||||||
title: summary.duration.inMinutes.toDouble(),
|
|
||||||
unit: "minutes",
|
|
||||||
description: 'Listened to music',
|
|
||||||
color: Colors.purple,
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SummaryCard(
|
delegate: SliverChildListDelegate([
|
||||||
title: summary.tracks.toDouble(),
|
SummaryCard(
|
||||||
unit: "songs",
|
title: summaryData.duration.inMinutes.toDouble(),
|
||||||
description: 'Streamed overall',
|
unit: "minutes",
|
||||||
color: Colors.lightBlue,
|
description: 'Listened to music',
|
||||||
onTap: () {
|
color: Colors.purple,
|
||||||
ServiceUtils.pushNamed(context, StatsStreamsPage.name);
|
onTap: () {
|
||||||
},
|
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
|
||||||
),
|
},
|
||||||
SummaryCard.unformatted(
|
),
|
||||||
title: usdFormatter.format(summary.fees.toDouble()),
|
SummaryCard(
|
||||||
unit: "",
|
title: summaryData.tracks.toDouble(),
|
||||||
description: 'Owed to artists\nthis month',
|
unit: "songs",
|
||||||
color: Colors.green,
|
description: 'Streamed overall',
|
||||||
onTap: () {
|
color: Colors.lightBlue,
|
||||||
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
|
onTap: () {
|
||||||
},
|
ServiceUtils.pushNamed(context, StatsStreamsPage.name);
|
||||||
),
|
},
|
||||||
SummaryCard(
|
),
|
||||||
title: summary.artists.toDouble(),
|
SummaryCard.unformatted(
|
||||||
unit: "artist's",
|
title: usdFormatter.format(summaryData.fees.toDouble()),
|
||||||
description: 'Music reached you',
|
unit: "",
|
||||||
color: Colors.yellow,
|
description: 'Owed to artists\nthis month',
|
||||||
onTap: () {
|
color: Colors.green,
|
||||||
ServiceUtils.pushNamed(context, StatsArtistsPage.name);
|
onTap: () {
|
||||||
},
|
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
|
||||||
),
|
},
|
||||||
SummaryCard(
|
),
|
||||||
title: summary.albums.toDouble(),
|
SummaryCard(
|
||||||
unit: "full albums",
|
title: summaryData.artists.toDouble(),
|
||||||
description: 'Got your love',
|
unit: "artist's",
|
||||||
color: Colors.pink,
|
description: 'Music reached you',
|
||||||
onTap: () {
|
color: Colors.yellow,
|
||||||
ServiceUtils.pushNamed(context, StatsAlbumsPage.name);
|
onTap: () {
|
||||||
},
|
ServiceUtils.pushNamed(context, StatsArtistsPage.name);
|
||||||
),
|
},
|
||||||
SummaryCard(
|
),
|
||||||
title: summary.playlists.toDouble(),
|
SummaryCard(
|
||||||
unit: "playlists",
|
title: summaryData.albums.toDouble(),
|
||||||
description: 'Were on repeat',
|
unit: "full albums",
|
||||||
color: Colors.teal,
|
description: 'Got your love',
|
||||||
onTap: () {
|
color: Colors.pink,
|
||||||
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name);
|
onTap: () {
|
||||||
},
|
ServiceUtils.pushNamed(context, StatsAlbumsPage.name);
|
||||||
),
|
},
|
||||||
]),
|
),
|
||||||
);
|
SummaryCard(
|
||||||
}),
|
title: summaryData.playlists.toDouble(),
|
||||||
|
unit: "playlists",
|
||||||
|
description: 'Were on repeat',
|
||||||
|
color: Colors.teal,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/formatters.dart';
|
import 'package:spotube/collections/formatters.dart';
|
||||||
import 'package:spotube/modules/stats/common/album_item.dart';
|
import 'package:spotube/modules/stats/common/album_item.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
import 'package:spotube/provider/history/top.dart';
|
||||||
@ -11,19 +12,24 @@ class TopAlbums extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
final albums = ref.watch(playbackHistoryTopProvider(historyDuration)
|
final albums = ref.watch(playbackHistoryTopProvider(historyDuration)
|
||||||
.select((value) => value.albums));
|
.select((value) => value.whenData((s) => s.albums)));
|
||||||
|
|
||||||
return SliverList.builder(
|
final albumsData = albums.asData?.value ?? [];
|
||||||
itemCount: albums.length,
|
|
||||||
itemBuilder: (context, index) {
|
return Skeletonizer(
|
||||||
final album = albums[index];
|
enabled: albums.isLoading,
|
||||||
return StatsAlbumItem(
|
child: SliverList.builder(
|
||||||
album: album.album,
|
itemCount: albumsData.length,
|
||||||
info: Text(
|
itemBuilder: (context, index) {
|
||||||
"${compactNumberFormatter.format(album.count)} plays",
|
final album = albumsData[index];
|
||||||
),
|
return StatsAlbumItem(
|
||||||
);
|
album: album.album,
|
||||||
},
|
info: Text(
|
||||||
|
"${compactNumberFormatter.format(album.count)} plays",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,14 @@ class TopArtists extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
final artists = ref.watch(playbackHistoryTopProvider(historyDuration)
|
final artists = ref.watch(playbackHistoryTopProvider(historyDuration)
|
||||||
.select((value) => value.artists));
|
.select((value) => value.whenData((s) => s.artists)));
|
||||||
|
|
||||||
|
final artistsData = artists.asData?.value ?? [];
|
||||||
|
|
||||||
return SliverList.builder(
|
return SliverList.builder(
|
||||||
itemCount: artists.length,
|
itemCount: artistsData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final artist = artists[index];
|
final artist = artistsData[index];
|
||||||
return StatsArtistItem(
|
return StatsArtistItem(
|
||||||
artist: artist.artist,
|
artist: artist.artist,
|
||||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||||
|
@ -12,13 +12,15 @@ class TopTracks extends HookConsumerWidget {
|
|||||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
final tracks = ref.watch(
|
final tracks = ref.watch(
|
||||||
playbackHistoryTopProvider(historyDuration)
|
playbackHistoryTopProvider(historyDuration)
|
||||||
.select((value) => value.tracks),
|
.select((value) => value.whenData((s) => s.tracks)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final tracksData = tracks.asData?.value ?? [];
|
||||||
|
|
||||||
return SliverList.builder(
|
return SliverList.builder(
|
||||||
itemCount: tracks.length,
|
itemCount: tracksData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final track = tracks[index];
|
final track = tracksData[index];
|
||||||
return StatsTrackItem(
|
return StatsTrackItem(
|
||||||
track: track.track,
|
track: track.track,
|
||||||
info: Text(
|
info: Text(
|
||||||
|
@ -139,14 +139,12 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final duration =
|
|
||||||
await audioPlayer.duration ??
|
|
||||||
Duration.zero;
|
|
||||||
final time = Duration(
|
final time = Duration(
|
||||||
seconds:
|
seconds:
|
||||||
lyricSlice.time.inSeconds - delay,
|
lyricSlice.time.inSeconds - delay,
|
||||||
);
|
);
|
||||||
if (time > duration || time.isNegative) {
|
if (time > audioPlayer.duration ||
|
||||||
|
time.isNegative) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
audioPlayer.seek(time);
|
audioPlayer.seek(time);
|
||||||
|
@ -12,10 +12,10 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final albums = ref.watch(
|
final albums = ref.watch(playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
.select((value) => value.whenData((s) => s.albums)));
|
||||||
.select((s) => s.albums),
|
|
||||||
);
|
final albumsData = albums.asData?.value ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
@ -24,9 +24,9 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
|||||||
title: Text("Albums"),
|
title: Text("Albums"),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: albums.length,
|
itemCount: albumsData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final album = albums[index];
|
final album = albumsData[index];
|
||||||
return StatsAlbumItem(
|
return StatsAlbumItem(
|
||||||
album: album.album,
|
album: album.album,
|
||||||
info: Text("${compactNumberFormatter.format(album.count)} plays"),
|
info: Text("${compactNumberFormatter.format(album.count)} plays"),
|
||||||
|
@ -14,9 +14,11 @@ class StatsArtistsPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final artists = ref.watch(
|
final artists = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
.select((s) => s.artists),
|
.select((s) => s.whenData((s) => s.artists)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final artistsData = artists.asData?.value ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
@ -24,9 +26,9 @@ class StatsArtistsPage extends HookConsumerWidget {
|
|||||||
title: Text("Artists"),
|
title: Text("Artists"),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: artists.length,
|
itemCount: artistsData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final artist = artists[index];
|
final artist = artistsData[index];
|
||||||
return StatsArtistItem(
|
return StatsArtistItem(
|
||||||
artist: artist.artist,
|
artist: artist.artist,
|
||||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||||
|
@ -18,9 +18,11 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final artists = ref.watch(
|
final artists = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.days30)
|
playbackHistoryTopProvider(HistoryDuration.days30)
|
||||||
.select((value) => value.artists),
|
.select((value) => value.whenData((s) => s.artists)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final artistsData = artists.asData?.value ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
@ -49,9 +51,9 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: artists.length,
|
itemCount: artistsData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final artist = artists[index];
|
final artist = artistsData[index];
|
||||||
return StatsArtistItem(
|
return StatsArtistItem(
|
||||||
artist: artist.artist,
|
artist: artist.artist,
|
||||||
info: Text(usdFormatter.format(artist.count * 0.005)),
|
info: Text(usdFormatter.format(artist.count * 0.005)),
|
||||||
|
@ -16,9 +16,11 @@ class StatsMinutesPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final topTracks = ref.watch(
|
final topTracks = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
.select((s) => s.tracks),
|
.select((s) => s.whenData((s) => s.tracks)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final topTracksData = topTracks.asData?.value ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
title: Text("Minutes listened"),
|
title: Text("Minutes listened"),
|
||||||
@ -27,9 +29,9 @@ class StatsMinutesPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: ListView.separated(
|
body: ListView.separated(
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
itemCount: topTracks.length,
|
itemCount: topTracksData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final (:track, :count) = topTracks[index];
|
final (:track, :count) = topTracksData[index];
|
||||||
|
|
||||||
return StatsTrackItem(
|
return StatsTrackItem(
|
||||||
track: track,
|
track: track,
|
||||||
|
@ -14,9 +14,11 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlists = ref.watch(
|
final playlists = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
.select((s) => s.playlists),
|
.select((s) => s.whenData((s) => s.playlists)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final playlistsData = playlists.asData?.value ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
@ -24,11 +26,11 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
|||||||
title: Text("Playlists"),
|
title: Text("Playlists"),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
itemCount: playlists.length,
|
itemCount: playlistsData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final playlist = playlists[index];
|
final playlist = playlistsData[index];
|
||||||
return StatsPlaylistItem(
|
return StatsPlaylistItem(
|
||||||
playlist: playlist.playlist.playlist,
|
playlist: playlist.playlist,
|
||||||
info:
|
info:
|
||||||
Text("${compactNumberFormatter.format(playlist.count)} plays"),
|
Text("${compactNumberFormatter.format(playlist.count)} plays"),
|
||||||
);
|
);
|
||||||
|
@ -16,9 +16,11 @@ class StatsStreamsPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final topTracks = ref.watch(
|
final topTracks = ref.watch(
|
||||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
.select((s) => s.tracks),
|
.select((s) => s.whenData((s) => s.tracks)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final topTracksData = topTracks.asData?.value ?? [];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
title: Text("Streamed songs"),
|
title: Text("Streamed songs"),
|
||||||
@ -27,9 +29,9 @@ class StatsStreamsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: ListView.separated(
|
body: ListView.separated(
|
||||||
separatorBuilder: (context, index) => const Gap(8),
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
itemCount: topTracks.length,
|
itemCount: topTracksData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final (:track, :count) = topTracks[index];
|
final (:track, :count) = topTracksData[index];
|
||||||
|
|
||||||
return StatsTrackItem(
|
return StatsTrackItem(
|
||||||
track: track,
|
track: track,
|
||||||
|
@ -45,8 +45,8 @@ class AudioPlayerStreamListeners {
|
|||||||
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
||||||
Discord get discord => ref.read(discordProvider);
|
Discord get discord => ref.read(discordProvider);
|
||||||
AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider);
|
AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider);
|
||||||
PlaybackHistoryNotifier get history =>
|
PlaybackHistoryActions get history =>
|
||||||
ref.read(playbackHistoryProvider.notifier);
|
ref.read(playbackHistoryActionsProvider);
|
||||||
|
|
||||||
Future<void> updatePalette() async {
|
Future<void> updatePalette() async {
|
||||||
final palette = ref.read(paletteProvider);
|
final palette = ref.read(paletteProvider);
|
||||||
|
@ -1,129 +1,68 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
|
||||||
|
|
||||||
class PlaybackHistoryState {
|
class PlaybackHistoryActions {
|
||||||
final List<PlaybackHistoryItem> items;
|
|
||||||
const PlaybackHistoryState({this.items = const []});
|
|
||||||
|
|
||||||
factory PlaybackHistoryState.fromJson(Map<String, dynamic> json) {
|
|
||||||
return PlaybackHistoryState(
|
|
||||||
items: json["items"]
|
|
||||||
?.map(
|
|
||||||
(json) => PlaybackHistoryItem.fromJson(json),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
.cast<PlaybackHistoryItem>() ??
|
|
||||||
<PlaybackHistoryItem>[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
"items": items.map((s) => s.toJson()).toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaybackHistoryState copyWith({
|
|
||||||
List<PlaybackHistoryItem>? items,
|
|
||||||
}) {
|
|
||||||
return PlaybackHistoryState(items: items ?? this.items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaybackHistoryNotifier
|
|
||||||
extends PersistedStateNotifier<PlaybackHistoryState> {
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
PlaybackHistoryNotifier(this.ref)
|
AppDatabase get _db => ref.read(databaseProvider);
|
||||||
: super(const PlaybackHistoryState(), "playback_history");
|
|
||||||
|
|
||||||
SpotifyApi get spotify => ref.read(spotifyProvider);
|
PlaybackHistoryActions(this.ref);
|
||||||
|
|
||||||
@override
|
Future<void> _batchInsertHistoryEntries(
|
||||||
FutureOr<PlaybackHistoryState> fromJson(Map<String, dynamic> json) =>
|
List<HistoryTableCompanion> entries) async {
|
||||||
PlaybackHistoryState.fromJson(json);
|
await _db.batch((batch) {
|
||||||
|
batch.insertAll(_db.historyTable, entries);
|
||||||
@override
|
});
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return state.toJson();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPlaylists(List<PlaylistSimple> playlists) {
|
Future<void> addPlaylists(List<PlaylistSimple> playlists) async {
|
||||||
state = state.copyWith(
|
await _batchInsertHistoryEntries([
|
||||||
items: [
|
for (final playlist in playlists)
|
||||||
...state.items,
|
HistoryTableCompanion.insert(
|
||||||
for (final playlist in playlists)
|
type: HistoryEntryType.playlist,
|
||||||
PlaybackHistoryItem.playlist(
|
itemId: playlist.id!,
|
||||||
date: DateTime.now(), playlist: playlist),
|
data: playlist.toJson(),
|
||||||
],
|
),
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAlbums(List<AlbumSimple> albums) {
|
Future<void> addAlbums(List<AlbumSimple> albums) async {
|
||||||
state = state.copyWith(
|
await _batchInsertHistoryEntries([
|
||||||
items: [
|
for (final albums in albums)
|
||||||
...state.items,
|
HistoryTableCompanion.insert(
|
||||||
for (final album in albums)
|
type: HistoryEntryType.album,
|
||||||
PlaybackHistoryItem.album(date: DateTime.now(), album: album),
|
itemId: albums.id!,
|
||||||
],
|
data: albums.toJson(),
|
||||||
);
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addTrack(Track track) async {
|
Future<void> addTracks(List<Track> tracks) async {
|
||||||
// For some reason Track's artists images are `null`
|
await _batchInsertHistoryEntries([
|
||||||
// so we need to fetch them from the API
|
for (final track in tracks)
|
||||||
final artists =
|
HistoryTableCompanion.insert(
|
||||||
await spotify.artists.list(track.artists!.map((e) => e.id!).toList());
|
type: HistoryEntryType.track,
|
||||||
|
itemId: track.id!,
|
||||||
track.artists = artists.toList();
|
data: track.toJson(),
|
||||||
|
),
|
||||||
state = state.copyWith(
|
]);
|
||||||
items: [
|
|
||||||
...state.items,
|
|
||||||
PlaybackHistoryItem.track(date: DateTime.now(), track: track),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
Future<void> addTrack(Track track) async {
|
||||||
state = state.copyWith(items: []);
|
await _db.into(_db.historyTable).insert(
|
||||||
|
HistoryTableCompanion.insert(
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
itemId: track.id!,
|
||||||
|
data: track.toJson(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear() async {
|
||||||
|
_db.delete(_db.historyTable).go();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final playbackHistoryProvider =
|
final playbackHistoryActionsProvider =
|
||||||
StateNotifierProvider<PlaybackHistoryNotifier, PlaybackHistoryState>(
|
Provider((ref) => PlaybackHistoryActions(ref));
|
||||||
(ref) => PlaybackHistoryNotifier(ref),
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef PlaybackHistoryGrouped = ({
|
|
||||||
List<PlaybackHistoryTrack> tracks,
|
|
||||||
List<PlaybackHistoryAlbum> albums,
|
|
||||||
List<PlaybackHistoryPlaylist> playlists,
|
|
||||||
});
|
|
||||||
|
|
||||||
final playbackHistoryGroupedProvider = Provider<PlaybackHistoryGrouped>((ref) {
|
|
||||||
final history = ref.watch(playbackHistoryProvider);
|
|
||||||
final tracks = history.items
|
|
||||||
.whereType<PlaybackHistoryTrack>()
|
|
||||||
.sorted((a, b) => b.date.compareTo(a.date))
|
|
||||||
.toList();
|
|
||||||
final albums = history.items
|
|
||||||
.whereType<PlaybackHistoryAlbum>()
|
|
||||||
.sorted((a, b) => b.date.compareTo(a.date))
|
|
||||||
.toList();
|
|
||||||
final playlists = history.items
|
|
||||||
.whereType<PlaybackHistoryPlaylist>()
|
|
||||||
.sorted((a, b) => b.date.compareTo(a.date))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return (
|
|
||||||
tracks: tracks,
|
|
||||||
albums: albums,
|
|
||||||
playlists: playlists,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
@ -1,40 +1,55 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
|
||||||
final recentlyPlayedItems = Provider((ref) {
|
class RecentlyPlayedItemNotifier extends AsyncNotifier<List<HistoryTableData>> {
|
||||||
return ref.watch(
|
@override
|
||||||
playbackHistoryProvider.select(
|
build() async {
|
||||||
(s) => s.items
|
final database = ref.watch(databaseProvider);
|
||||||
.toSet()
|
|
||||||
// unique items
|
final uniqueItemIds =
|
||||||
.whereIndexed(
|
await (database.selectOnly(database.historyTable, distinct: true)
|
||||||
(index, item) =>
|
..addColumns([database.historyTable.itemId])
|
||||||
index ==
|
..where(
|
||||||
s.items.lastIndexWhere(
|
database.historyTable.type.isIn([
|
||||||
(e) => switch ((e, item)) {
|
HistoryEntryType.playlist.name,
|
||||||
(
|
HistoryEntryType.album.name,
|
||||||
PlaybackHistoryPlaylist(:final playlist),
|
]),
|
||||||
PlaybackHistoryPlaylist(playlist: final playlist2)
|
)
|
||||||
) =>
|
..limit(10))
|
||||||
playlist.id == playlist2.id,
|
.map((row) => row.read(database.historyTable.itemId))
|
||||||
(
|
.get()
|
||||||
PlaybackHistoryAlbum(:final album),
|
.then((value) => value.whereNotNull().toList());
|
||||||
PlaybackHistoryAlbum(album: final album2)
|
|
||||||
) =>
|
final query = database.select(database.historyTable)
|
||||||
album.id == album2.id,
|
..where(
|
||||||
_ => false,
|
(tbl) =>
|
||||||
},
|
tbl.type.isIn([
|
||||||
),
|
HistoryEntryType.playlist.name,
|
||||||
)
|
HistoryEntryType.album.name,
|
||||||
.where(
|
]) &
|
||||||
(s) => s is PlaybackHistoryPlaylist || s is PlaybackHistoryAlbum,
|
tbl.itemId.isIn(uniqueItemIds),
|
||||||
)
|
)
|
||||||
.take(10)
|
..orderBy([
|
||||||
.sortedBy((s) => s.date)
|
(tbl) => OrderingTerm(
|
||||||
.reversed
|
expression: tbl.createdAt,
|
||||||
.toList(),
|
mode: OrderingMode.desc,
|
||||||
),
|
),
|
||||||
);
|
]);
|
||||||
});
|
|
||||||
|
final subscription = query.watch().listen((event) {
|
||||||
|
state = AsyncData(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(() => subscription.cancel());
|
||||||
|
|
||||||
|
return await query.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final recentlyPlayedItems =
|
||||||
|
AsyncNotifierProvider<RecentlyPlayedItemNotifier, List<HistoryTableData>>(
|
||||||
|
() => RecentlyPlayedItemNotifier(),
|
||||||
|
);
|
||||||
|
@ -1,62 +1,197 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/extensions/json1.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/history/top.dart';
|
|
||||||
|
|
||||||
final playbackHistorySummaryProvider = Provider((ref) {
|
class PlaybackHistorySummary {
|
||||||
final (:tracks, :albums, :playlists) =
|
final Duration duration;
|
||||||
ref.watch(playbackHistoryGroupedProvider);
|
final int tracks;
|
||||||
|
final int artists;
|
||||||
|
final double fees;
|
||||||
|
final int albums;
|
||||||
|
final int playlists;
|
||||||
|
|
||||||
final totalDurationListened = tracks.fold(
|
const PlaybackHistorySummary({
|
||||||
Duration.zero,
|
required this.duration,
|
||||||
(previousValue, element) => previousValue + element.track.duration!,
|
required this.tracks,
|
||||||
);
|
required this.artists,
|
||||||
|
required this.fees,
|
||||||
|
required this.albums,
|
||||||
|
required this.playlists,
|
||||||
|
});
|
||||||
|
|
||||||
final totalTracksListened = tracks
|
PlaybackHistorySummary copyWith({
|
||||||
.whereIndexed(
|
Duration? duration,
|
||||||
(i, track) =>
|
int? tracks,
|
||||||
i == tracks.lastIndexWhere((e) => e.track.id == track.track.id),
|
int? artists,
|
||||||
)
|
double? fees,
|
||||||
.length;
|
int? albums,
|
||||||
|
int? playlists,
|
||||||
|
}) {
|
||||||
|
return PlaybackHistorySummary(
|
||||||
|
duration: duration ?? this.duration,
|
||||||
|
tracks: tracks ?? this.tracks,
|
||||||
|
artists: artists ?? this.artists,
|
||||||
|
fees: fees ?? this.fees,
|
||||||
|
albums: albums ?? this.albums,
|
||||||
|
playlists: playlists ?? this.playlists,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final artists =
|
class PlaybackHistorySummaryNotifier
|
||||||
tracks.map((e) => e.track.artists).expand((e) => e ?? []).toList();
|
extends AsyncNotifier<PlaybackHistorySummary> {
|
||||||
|
@override
|
||||||
|
build() async {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
|
||||||
final totalArtistsListened = artists
|
final uniqItemIdCountingCol =
|
||||||
.whereIndexed(
|
database.historyTable.itemId.count(distinct: true);
|
||||||
(i, artist) => i == artists.lastIndexWhere((e) => e.id == artist.id),
|
final itemIdCountingCol = database.historyTable.itemId.count();
|
||||||
)
|
final durationSumJsonColumn =
|
||||||
.length;
|
database.historyTable.data.jsonExtract<int>(r"$.duration_ms").sum();
|
||||||
|
final artistCountingCol =
|
||||||
|
database.historyTable.data.jsonExtract<String>(r"$.artists");
|
||||||
|
|
||||||
final totalAlbumsListened = albums
|
final totalTracksListenedQuery = (database.selectOnly(database.historyTable)
|
||||||
.whereIndexed(
|
..addColumns([uniqItemIdCountingCol])
|
||||||
(i, album) =>
|
..where(
|
||||||
i == albums.lastIndexWhere((e) => e.album.id == album.album.id),
|
database.historyTable.type.equals(HistoryEntryType.track.name)))
|
||||||
)
|
.map((row) => row.read(uniqItemIdCountingCol));
|
||||||
.length;
|
|
||||||
|
|
||||||
final totalPlaylistsListened = playlists
|
final totalDurationListenedQuery = (database
|
||||||
.whereIndexed(
|
.selectOnly(database.historyTable)
|
||||||
(i, playlist) =>
|
..addColumns([durationSumJsonColumn])
|
||||||
i ==
|
..where(
|
||||||
playlists
|
database.historyTable.type.equals(HistoryEntryType.track.name)))
|
||||||
.lastIndexWhere((e) => e.playlist.id == playlist.playlist.id),
|
.map(
|
||||||
)
|
(row) => Duration(milliseconds: row.read(durationSumJsonColumn) ?? 0),
|
||||||
.length;
|
);
|
||||||
|
|
||||||
final tracksThisMonth = ref.watch(
|
final totalArtistsListenedQuery =
|
||||||
playbackHistoryTopProvider(HistoryDuration.days30).select((s) => s.tracks),
|
(database.selectOnly(database.historyTable)
|
||||||
);
|
..addColumns([artistCountingCol])
|
||||||
|
..where(
|
||||||
|
database.historyTable.type.equals(HistoryEntryType.track.name),
|
||||||
|
))
|
||||||
|
.map(
|
||||||
|
(row) {
|
||||||
|
final data = jsonDecode(row.read(artistCountingCol)!) as List;
|
||||||
|
return data.map((e) => e['id'] as String).cast<String>().toList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final streams = tracksThisMonth.fold(0, (acc, el) => acc + el.count);
|
final totalAlbumsListenedQuery = (database.selectOnly(database.historyTable)
|
||||||
|
..addColumns([uniqItemIdCountingCol])
|
||||||
|
..where(
|
||||||
|
database.historyTable.type.equals(HistoryEntryType.album.name)))
|
||||||
|
.map((row) => row.read(uniqItemIdCountingCol));
|
||||||
|
|
||||||
return (
|
final totalPlaylistsListenedQuery =
|
||||||
duration: totalDurationListened,
|
(database.selectOnly(database.historyTable)
|
||||||
tracks: totalTracksListened,
|
..addColumns([uniqItemIdCountingCol])
|
||||||
artists: totalArtistsListened,
|
..where(
|
||||||
fees: streams * 0.005, // Spotify pays $0.003 to $0.005
|
database.historyTable.type
|
||||||
albums: totalAlbumsListened,
|
.equals(HistoryEntryType.playlist.name),
|
||||||
playlists: totalPlaylistsListened,
|
))
|
||||||
);
|
.map((row) => row.read(uniqItemIdCountingCol));
|
||||||
});
|
|
||||||
|
final oldestDate = DateTime.now().copyWith(day: 1, hour: 0, minute: 0);
|
||||||
|
final newestDate = DateTime.now().copyWith(day: 30, hour: 23, minute: 59);
|
||||||
|
final totalTracksListenedThisMonthQuery =
|
||||||
|
(database.selectOnly(database.historyTable)
|
||||||
|
..addColumns([itemIdCountingCol])
|
||||||
|
..where(
|
||||||
|
database.historyTable.type.equals(
|
||||||
|
HistoryEntryType.track.name,
|
||||||
|
) &
|
||||||
|
database.historyTable.createdAt
|
||||||
|
.isBetweenValues(oldestDate, newestDate),
|
||||||
|
))
|
||||||
|
.map((row) => row.read(itemIdCountingCol));
|
||||||
|
|
||||||
|
final subscriptions = <StreamSubscription>[
|
||||||
|
totalTracksListenedQuery.watchSingle().listen((event) {
|
||||||
|
if (event == null || state.asData == null) return;
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
tracks: event,
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
totalDurationListenedQuery.watchSingle().listen((event) {
|
||||||
|
if (state.asData == null) return;
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
duration: event,
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
totalArtistsListenedQuery.watch().listen((event) {
|
||||||
|
if (state.asData == null) return;
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
artists: event.expand((e) => e).toSet().length,
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
totalAlbumsListenedQuery.watchSingle().listen((event) {
|
||||||
|
if (event == null || state.asData == null) return;
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
albums: event,
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
totalPlaylistsListenedQuery.watchSingle().listen((event) {
|
||||||
|
if (event == null || state.asData == null) return;
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
playlists: event,
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
totalTracksListenedThisMonthQuery.watchSingle().listen((event) {
|
||||||
|
if (event == null || state.asData == null) return;
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
fees: event * 0.005,
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
for (final subscription in subscriptions) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return database.transaction(() async {
|
||||||
|
final totalTracksListened =
|
||||||
|
await totalTracksListenedQuery.getSingle() ?? 0;
|
||||||
|
|
||||||
|
final totalDurationListened =
|
||||||
|
await totalDurationListenedQuery.getSingle();
|
||||||
|
|
||||||
|
final totalArtistsListened = await totalArtistsListenedQuery
|
||||||
|
.get()
|
||||||
|
.then((value) => value.expand((e) => e).toSet().length);
|
||||||
|
|
||||||
|
final totalAlbumsListened =
|
||||||
|
await totalAlbumsListenedQuery.getSingle() ?? 0;
|
||||||
|
|
||||||
|
final totalPlaylistsListened =
|
||||||
|
await totalPlaylistsListenedQuery.getSingle() ?? 0;
|
||||||
|
|
||||||
|
final totalTracksListenedThisMonth =
|
||||||
|
await totalTracksListenedThisMonthQuery.getSingle() ?? 0;
|
||||||
|
|
||||||
|
return PlaybackHistorySummary(
|
||||||
|
duration: totalDurationListened,
|
||||||
|
tracks: totalTracksListened,
|
||||||
|
artists: totalArtistsListened,
|
||||||
|
fees: totalTracksListenedThisMonth * 0.005,
|
||||||
|
albums: totalAlbumsListened,
|
||||||
|
playlists: totalPlaylistsListened,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final playbackHistorySummaryProvider = AsyncNotifierProvider<
|
||||||
|
PlaybackHistorySummaryNotifier, PlaybackHistorySummary>(
|
||||||
|
() => PlaybackHistorySummaryNotifier(),
|
||||||
|
);
|
||||||
|
@ -1,95 +1,212 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/history/state.dart';
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
|
||||||
final playbackHistoryTopDurationProvider =
|
final playbackHistoryTopDurationProvider =
|
||||||
StateProvider((ref) => HistoryDuration.days30);
|
StateProvider((ref) => HistoryDuration.days30);
|
||||||
|
|
||||||
final playbackHistoryTopProvider =
|
typedef PlaybackHistoryTrack = ({int count, Track track});
|
||||||
Provider.family((ref, HistoryDuration durationState) {
|
typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album});
|
||||||
final grouped = ref.watch(playbackHistoryGroupedProvider);
|
typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist});
|
||||||
|
typedef PlaybackHistoryArtist = ({int count, Artist artist});
|
||||||
|
|
||||||
final duration = switch (durationState) {
|
class PlaybackHistoryTopState {
|
||||||
HistoryDuration.allTime => const Duration(days: 365 * 2003),
|
final List<PlaybackHistoryTrack> tracks;
|
||||||
HistoryDuration.days7 => const Duration(days: 7),
|
final List<PlaybackHistoryAlbum> albums;
|
||||||
HistoryDuration.days30 => const Duration(days: 30),
|
final List<PlaybackHistoryPlaylist> playlists;
|
||||||
HistoryDuration.months6 => const Duration(days: 30 * 6),
|
final List<PlaybackHistoryArtist> artists;
|
||||||
HistoryDuration.year => const Duration(days: 365),
|
|
||||||
HistoryDuration.years2 => const Duration(days: 365 * 2),
|
|
||||||
};
|
|
||||||
final tracks = grouped.tracks
|
|
||||||
.where(
|
|
||||||
(item) => item.date.isAfter(
|
|
||||||
DateTime.now().subtract(duration),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final albums = grouped.albums
|
|
||||||
.where(
|
|
||||||
(item) => item.date.isAfter(
|
|
||||||
DateTime.now().subtract(duration),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final playlists = grouped.playlists
|
const PlaybackHistoryTopState({
|
||||||
.where(
|
required this.tracks,
|
||||||
(item) => item.date.isAfter(
|
required this.albums,
|
||||||
DateTime.now().subtract(duration),
|
required this.playlists,
|
||||||
),
|
required this.artists,
|
||||||
)
|
});
|
||||||
.toList();
|
|
||||||
|
|
||||||
final tracksWithCount = groupBy(
|
PlaybackHistoryTopState copyWith({
|
||||||
tracks,
|
List<PlaybackHistoryTrack>? tracks,
|
||||||
(track) => track.track.id!,
|
List<PlaybackHistoryAlbum>? albums,
|
||||||
)
|
List<PlaybackHistoryPlaylist>? playlists,
|
||||||
.entries
|
List<PlaybackHistoryArtist>? artists,
|
||||||
.map((entry) {
|
}) {
|
||||||
return (count: entry.value.length, track: entry.value.first.track);
|
return PlaybackHistoryTopState(
|
||||||
})
|
tracks: tracks ?? this.tracks,
|
||||||
.sorted((a, b) => b.count.compareTo(a.count))
|
albums: albums ?? this.albums,
|
||||||
.toList();
|
playlists: playlists ?? this.playlists,
|
||||||
|
artists: artists ?? this.artists,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final albumsWithTrackAlbums = [
|
class PlaybackHistoryTopNotifier
|
||||||
for (final historicAlbum in albums) historicAlbum.album,
|
extends FamilyAsyncNotifier<PlaybackHistoryTopState, HistoryDuration> {
|
||||||
for (final track in tracks) track.track.album!
|
@override
|
||||||
];
|
build(arg) async {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
|
||||||
final albumsWithCount = groupBy(albumsWithTrackAlbums, (album) => album.id!)
|
final duration = switch (arg) {
|
||||||
.entries
|
HistoryDuration.allTime => const Duration(days: 365 * 2003),
|
||||||
.map((entry) {
|
HistoryDuration.days7 => const Duration(days: 7),
|
||||||
return (count: entry.value.length, album: entry.value.first);
|
HistoryDuration.days30 => const Duration(days: 30),
|
||||||
})
|
HistoryDuration.months6 => const Duration(days: 30 * 6),
|
||||||
.sorted((a, b) => b.count.compareTo(a.count))
|
HistoryDuration.year => const Duration(days: 365),
|
||||||
.toList();
|
HistoryDuration.years2 => const Duration(days: 365 * 2),
|
||||||
|
};
|
||||||
|
|
||||||
final artists =
|
final tracksQuery = (database.select(database.historyTable)
|
||||||
tracks.map((track) => track.track.artists).expand((e) => e ?? <Artist>[]);
|
..where(
|
||||||
|
(tbl) =>
|
||||||
|
tbl.type.equalsValue(HistoryEntryType.track) &
|
||||||
|
tbl.createdAt.isBiggerOrEqualValue(
|
||||||
|
DateTime.now().subtract(duration),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
final artistsWithCount = groupBy(artists, (artist) => artist.id!)
|
final albumsQuery = database.select(database.historyTable)
|
||||||
.entries
|
..where(
|
||||||
.map((entry) {
|
(tbl) =>
|
||||||
return (count: entry.value.length, artist: entry.value.first);
|
tbl.type.equalsValue(HistoryEntryType.album) &
|
||||||
})
|
tbl.createdAt.isBiggerOrEqualValue(
|
||||||
.sorted((a, b) => b.count.compareTo(a.count))
|
DateTime.now().subtract(duration),
|
||||||
.toList();
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final playlistsWithCount =
|
final playlistsQuery = database.select(database.historyTable)
|
||||||
groupBy(playlists, (playlist) => playlist.playlist.id!)
|
..where(
|
||||||
.entries
|
(tbl) =>
|
||||||
.map((entry) {
|
tbl.type.equalsValue(HistoryEntryType.playlist) &
|
||||||
return (count: entry.value.length, playlist: entry.value.first);
|
tbl.createdAt.isBiggerOrEqualValue(
|
||||||
})
|
DateTime.now().subtract(duration),
|
||||||
.sorted((a, b) => b.count.compareTo(a.count))
|
),
|
||||||
.toList();
|
);
|
||||||
|
|
||||||
return (
|
final subscriptions = <StreamSubscription>[
|
||||||
tracks: tracksWithCount,
|
tracksQuery.watch().listen((event) {
|
||||||
albums: albumsWithCount,
|
if (state.asData == null) return;
|
||||||
artists: artistsWithCount,
|
final artists = event
|
||||||
playlists: playlistsWithCount,
|
.map((track) => track.track!.artists)
|
||||||
);
|
.expand((e) => e ?? <Artist>[]);
|
||||||
});
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
tracks: getTracksWithCount(event),
|
||||||
|
artists: getArtistsWithCount(artists),
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
albumsQuery.watch().listen((event) async {
|
||||||
|
if (state.asData == null) return;
|
||||||
|
final tracks = await tracksQuery.get();
|
||||||
|
|
||||||
|
final albumsWithTrackAlbums = [
|
||||||
|
for (final historicAlbum in event) historicAlbum.album!,
|
||||||
|
for (final track in tracks) track.track!.album!
|
||||||
|
];
|
||||||
|
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
albums: getAlbumsWithCount(albumsWithTrackAlbums),
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
playlistsQuery.watch().listen((event) {
|
||||||
|
if (state.asData == null) return;
|
||||||
|
state = AsyncData(state.asData!.value.copyWith(
|
||||||
|
playlists: getPlaylistsWithCount(event),
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
for (final subscription in subscriptions) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return database.transaction(() async {
|
||||||
|
final tracks = await tracksQuery.get();
|
||||||
|
final albums = await albumsQuery.get();
|
||||||
|
final playlists = await playlistsQuery.get();
|
||||||
|
|
||||||
|
final tracksWithCount = getTracksWithCount(tracks);
|
||||||
|
|
||||||
|
final albumsWithTrackAlbums = [
|
||||||
|
for (final historicAlbum in albums) historicAlbum.album!,
|
||||||
|
for (final track in tracks) track.track!.album!
|
||||||
|
];
|
||||||
|
|
||||||
|
final albumsWithCount = getAlbumsWithCount(albumsWithTrackAlbums);
|
||||||
|
|
||||||
|
final artists = tracks
|
||||||
|
.map((track) => track.track!.artists)
|
||||||
|
.expand((e) => e ?? <Artist>[]);
|
||||||
|
|
||||||
|
final artistsWithCount = getArtistsWithCount(artists);
|
||||||
|
|
||||||
|
final playlistsWithCount = getPlaylistsWithCount(playlists);
|
||||||
|
|
||||||
|
return PlaybackHistoryTopState(
|
||||||
|
tracks: tracksWithCount,
|
||||||
|
albums: albumsWithCount,
|
||||||
|
artists: artistsWithCount,
|
||||||
|
playlists: playlistsWithCount,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PlaybackHistoryTrack> getTracksWithCount(List<HistoryTableData> tracks) {
|
||||||
|
return groupBy(
|
||||||
|
tracks,
|
||||||
|
(track) => track.track!.id!,
|
||||||
|
)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (count: entry.value.length, track: entry.value.first.track!);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PlaybackHistoryAlbum> getAlbumsWithCount(
|
||||||
|
List<AlbumSimple> albumsWithTrackAlbums,
|
||||||
|
) {
|
||||||
|
return groupBy(albumsWithTrackAlbums, (album) => album.id!)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (count: entry.value.length, album: entry.value.first);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PlaybackHistoryArtist> getArtistsWithCount(Iterable<Artist> artists) {
|
||||||
|
return groupBy(artists, (artist) => artist.id!)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (count: entry.value.length, artist: entry.value.first);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PlaybackHistoryPlaylist> getPlaylistsWithCount(
|
||||||
|
List<HistoryTableData> playlists,
|
||||||
|
) {
|
||||||
|
return groupBy(playlists, (playlist) => playlist.playlist!.id!)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (
|
||||||
|
count: entry.value.length,
|
||||||
|
playlist: entry.value.first.playlist!,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final playbackHistoryTopProvider = AsyncNotifierProviderFamily<
|
||||||
|
PlaybackHistoryTopNotifier,
|
||||||
|
PlaybackHistoryTopState,
|
||||||
|
HistoryDuration>(PlaybackHistoryTopNotifier.new);
|
||||||
|
@ -40,8 +40,8 @@ class ServerConnectRoutes {
|
|||||||
|
|
||||||
AudioPlayerNotifier get audioPlayerNotifier =>
|
AudioPlayerNotifier get audioPlayerNotifier =>
|
||||||
ref.read(audioPlayerProvider.notifier);
|
ref.read(audioPlayerProvider.notifier);
|
||||||
PlaybackHistoryNotifier get historyNotifier =>
|
PlaybackHistoryActions get historyNotifier =>
|
||||||
ref.read(playbackHistoryProvider.notifier);
|
ref.read(playbackHistoryActionsProvider);
|
||||||
Stream<String> get connectClientStream =>
|
Stream<String> get connectClientStream =>
|
||||||
_connectClientStreamController.stream;
|
_connectClientStreamController.stream;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user