mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: synced lyric cache to use drift db
This commit is contained in:
parent
08ac29c979
commit
1cfd377c29
@ -131,7 +131,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
if (context.mounted) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,6 @@ import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||
import 'package:spotube/themes/theme.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@ -85,10 +84,6 @@ Future<void> main(List<String> rawArgs) async {
|
||||
|
||||
Hive.init(hiveCacheDir);
|
||||
|
||||
await PersistedStateNotifier.initializeBoxes(
|
||||
path: hiveCacheDir,
|
||||
);
|
||||
|
||||
if (kIsDesktop) {
|
||||
await localNotifier.setup(appName: "Spotube");
|
||||
await WindowManagerTools.initialize();
|
||||
|
@ -9,6 +9,7 @@ import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotify/spotify.dart' hide Playlist;
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
@ -28,12 +29,14 @@ part 'tables/skip_segment.dart';
|
||||
part 'tables/source_match.dart';
|
||||
part 'tables/audio_player_state.dart';
|
||||
part 'tables/history.dart';
|
||||
part 'tables/lyrics.dart';
|
||||
|
||||
part 'typeconverters/color.dart';
|
||||
part 'typeconverters/locale.dart';
|
||||
part 'typeconverters/string_list.dart';
|
||||
part 'typeconverters/encrypted_text.dart';
|
||||
part 'typeconverters/map.dart';
|
||||
part 'typeconverters/subtitle.dart';
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
@ -47,6 +50,7 @@ part 'typeconverters/map.dart';
|
||||
PlaylistTable,
|
||||
PlaylistMediaTable,
|
||||
HistoryTable,
|
||||
LyricsTable,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
|
@ -3709,6 +3709,218 @@ class HistoryTableCompanion extends UpdateCompanion<HistoryTableData> {
|
||||
}
|
||||
}
|
||||
|
||||
class $LyricsTableTable extends LyricsTable
|
||||
with TableInfo<$LyricsTableTable, LyricsTableData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$LyricsTableTable(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 _trackIdMeta =
|
||||
const VerificationMeta('trackId');
|
||||
@override
|
||||
late final GeneratedColumn<String> trackId = GeneratedColumn<String>(
|
||||
'track_id', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const VerificationMeta _dataMeta = const VerificationMeta('data');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SubtitleSimple, String> data =
|
||||
GeneratedColumn<String>('data', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||
.withConverter<SubtitleSimple>($LyricsTableTable.$converterdata);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, trackId, data];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'lyrics_table';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<LyricsTableData> 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('track_id')) {
|
||||
context.handle(_trackIdMeta,
|
||||
trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_trackIdMeta);
|
||||
}
|
||||
context.handle(_dataMeta, const VerificationResult.success());
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
LyricsTableData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return LyricsTableData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
trackId: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}track_id'])!,
|
||||
data: $LyricsTableTable.$converterdata.fromSql(attachedDatabase
|
||||
.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}data'])!),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$LyricsTableTable createAlias(String alias) {
|
||||
return $LyricsTableTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static TypeConverter<SubtitleSimple, String> $converterdata =
|
||||
SubtitleTypeConverter();
|
||||
}
|
||||
|
||||
class LyricsTableData extends DataClass implements Insertable<LyricsTableData> {
|
||||
final int id;
|
||||
final String trackId;
|
||||
final SubtitleSimple data;
|
||||
const LyricsTableData(
|
||||
{required this.id, required this.trackId, required this.data});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['track_id'] = Variable<String>(trackId);
|
||||
{
|
||||
map['data'] =
|
||||
Variable<String>($LyricsTableTable.$converterdata.toSql(data));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
LyricsTableCompanion toCompanion(bool nullToAbsent) {
|
||||
return LyricsTableCompanion(
|
||||
id: Value(id),
|
||||
trackId: Value(trackId),
|
||||
data: Value(data),
|
||||
);
|
||||
}
|
||||
|
||||
factory LyricsTableData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return LyricsTableData(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
trackId: serializer.fromJson<String>(json['trackId']),
|
||||
data: serializer.fromJson<SubtitleSimple>(json['data']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'trackId': serializer.toJson<String>(trackId),
|
||||
'data': serializer.toJson<SubtitleSimple>(data),
|
||||
};
|
||||
}
|
||||
|
||||
LyricsTableData copyWith({int? id, String? trackId, SubtitleSimple? data}) =>
|
||||
LyricsTableData(
|
||||
id: id ?? this.id,
|
||||
trackId: trackId ?? this.trackId,
|
||||
data: data ?? this.data,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('LyricsTableData(')
|
||||
..write('id: $id, ')
|
||||
..write('trackId: $trackId, ')
|
||||
..write('data: $data')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, trackId, data);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is LyricsTableData &&
|
||||
other.id == this.id &&
|
||||
other.trackId == this.trackId &&
|
||||
other.data == this.data);
|
||||
}
|
||||
|
||||
class LyricsTableCompanion extends UpdateCompanion<LyricsTableData> {
|
||||
final Value<int> id;
|
||||
final Value<String> trackId;
|
||||
final Value<SubtitleSimple> data;
|
||||
const LyricsTableCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.trackId = const Value.absent(),
|
||||
this.data = const Value.absent(),
|
||||
});
|
||||
LyricsTableCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String trackId,
|
||||
required SubtitleSimple data,
|
||||
}) : trackId = Value(trackId),
|
||||
data = Value(data);
|
||||
static Insertable<LyricsTableData> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? trackId,
|
||||
Expression<String>? data,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (trackId != null) 'track_id': trackId,
|
||||
if (data != null) 'data': data,
|
||||
});
|
||||
}
|
||||
|
||||
LyricsTableCompanion copyWith(
|
||||
{Value<int>? id, Value<String>? trackId, Value<SubtitleSimple>? data}) {
|
||||
return LyricsTableCompanion(
|
||||
id: id ?? this.id,
|
||||
trackId: trackId ?? this.trackId,
|
||||
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 (trackId.present) {
|
||||
map['track_id'] = Variable<String>(trackId.value);
|
||||
}
|
||||
if (data.present) {
|
||||
map['data'] =
|
||||
Variable<String>($LyricsTableTable.$converterdata.toSql(data.value));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('LyricsTableCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('trackId: $trackId, ')
|
||||
..write('data: $data')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
_$AppDatabase(QueryExecutor e) : super(e);
|
||||
_$AppDatabaseManager get managers => _$AppDatabaseManager(this);
|
||||
@ -3728,6 +3940,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
late final $PlaylistMediaTableTable playlistMediaTable =
|
||||
$PlaylistMediaTableTable(this);
|
||||
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
||||
late final $LyricsTableTable lyricsTable = $LyricsTableTable(this);
|
||||
late final Index uniqueBlacklist = Index('unique_blacklist',
|
||||
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
||||
late final Index uniqTrackMatch = Index('uniq_track_match',
|
||||
@ -3747,6 +3960,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
playlistTable,
|
||||
playlistMediaTable,
|
||||
historyTable,
|
||||
lyricsTable,
|
||||
uniqueBlacklist,
|
||||
uniqTrackMatch
|
||||
];
|
||||
@ -5492,6 +5706,113 @@ class $$HistoryTableTableOrderingComposer
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
}
|
||||
|
||||
typedef $$LyricsTableTableInsertCompanionBuilder = LyricsTableCompanion
|
||||
Function({
|
||||
Value<int> id,
|
||||
required String trackId,
|
||||
required SubtitleSimple data,
|
||||
});
|
||||
typedef $$LyricsTableTableUpdateCompanionBuilder = LyricsTableCompanion
|
||||
Function({
|
||||
Value<int> id,
|
||||
Value<String> trackId,
|
||||
Value<SubtitleSimple> data,
|
||||
});
|
||||
|
||||
class $$LyricsTableTableTableManager extends RootTableManager<
|
||||
_$AppDatabase,
|
||||
$LyricsTableTable,
|
||||
LyricsTableData,
|
||||
$$LyricsTableTableFilterComposer,
|
||||
$$LyricsTableTableOrderingComposer,
|
||||
$$LyricsTableTableProcessedTableManager,
|
||||
$$LyricsTableTableInsertCompanionBuilder,
|
||||
$$LyricsTableTableUpdateCompanionBuilder> {
|
||||
$$LyricsTableTableTableManager(_$AppDatabase db, $LyricsTableTable table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
filteringComposer:
|
||||
$$LyricsTableTableFilterComposer(ComposerState(db, table)),
|
||||
orderingComposer:
|
||||
$$LyricsTableTableOrderingComposer(ComposerState(db, table)),
|
||||
getChildManagerBuilder: (p) =>
|
||||
$$LyricsTableTableProcessedTableManager(p),
|
||||
getUpdateCompanionBuilder: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<String> trackId = const Value.absent(),
|
||||
Value<SubtitleSimple> data = const Value.absent(),
|
||||
}) =>
|
||||
LyricsTableCompanion(
|
||||
id: id,
|
||||
trackId: trackId,
|
||||
data: data,
|
||||
),
|
||||
getInsertCompanionBuilder: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
required String trackId,
|
||||
required SubtitleSimple data,
|
||||
}) =>
|
||||
LyricsTableCompanion.insert(
|
||||
id: id,
|
||||
trackId: trackId,
|
||||
data: data,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
class $$LyricsTableTableProcessedTableManager extends ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$LyricsTableTable,
|
||||
LyricsTableData,
|
||||
$$LyricsTableTableFilterComposer,
|
||||
$$LyricsTableTableOrderingComposer,
|
||||
$$LyricsTableTableProcessedTableManager,
|
||||
$$LyricsTableTableInsertCompanionBuilder,
|
||||
$$LyricsTableTableUpdateCompanionBuilder> {
|
||||
$$LyricsTableTableProcessedTableManager(super.$state);
|
||||
}
|
||||
|
||||
class $$LyricsTableTableFilterComposer
|
||||
extends FilterComposer<_$AppDatabase, $LyricsTableTable> {
|
||||
$$LyricsTableTableFilterComposer(super.$state);
|
||||
ColumnFilters<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnFilters<String> get trackId => $state.composableBuilder(
|
||||
column: $state.table.trackId,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnWithTypeConverterFilters<SubtitleSimple, SubtitleSimple, String>
|
||||
get data => $state.composableBuilder(
|
||||
column: $state.table.data,
|
||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||
column,
|
||||
joinBuilders: joinBuilders));
|
||||
}
|
||||
|
||||
class $$LyricsTableTableOrderingComposer
|
||||
extends OrderingComposer<_$AppDatabase, $LyricsTableTable> {
|
||||
$$LyricsTableTableOrderingComposer(super.$state);
|
||||
ColumnOrderings<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<String> get trackId => $state.composableBuilder(
|
||||
column: $state.table.trackId,
|
||||
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 {
|
||||
final _$AppDatabase _db;
|
||||
_$AppDatabaseManager(this._db);
|
||||
@ -5515,4 +5836,6 @@ class _$AppDatabaseManager {
|
||||
$$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable);
|
||||
$$HistoryTableTableTableManager get historyTable =>
|
||||
$$HistoryTableTableTableManager(_db, _db.historyTable);
|
||||
$$LyricsTableTableTableManager get lyricsTable =>
|
||||
$$LyricsTableTableTableManager(_db, _db.lyricsTable);
|
||||
}
|
||||
|
8
lib/models/database/tables/lyrics.dart
Normal file
8
lib/models/database/tables/lyrics.dart
Normal file
@ -0,0 +1,8 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class LyricsTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get trackId => text()();
|
||||
TextColumn get data => text().map(SubtitleTypeConverter())();
|
||||
}
|
13
lib/models/database/typeconverters/subtitle.dart
Normal file
13
lib/models/database/typeconverters/subtitle.dart
Normal file
@ -0,0 +1,13 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class SubtitleTypeConverter extends TypeConverter<SubtitleSimple, String> {
|
||||
@override
|
||||
SubtitleSimple fromSql(String fromDb) {
|
||||
return SubtitleSimple.fromJson(jsonDecode(fromDb));
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SubtitleSimple value) {
|
||||
return jsonEncode(value.toJson());
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/modules/player/player_queue.dart';
|
||||
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
|
||||
@ -19,7 +18,6 @@ import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/server/routes/connect.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
@ -41,13 +39,6 @@ class RootApp extends HookConsumerWidget {
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
ServiceUtils.checkForUpdates(context, ref);
|
||||
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
|
||||
context.mounted) {
|
||||
await PersistedStateNotifier.showNoEncryptionDialog(context);
|
||||
}
|
||||
});
|
||||
|
||||
final subscriptions = [
|
||||
|
@ -1,11 +1,6 @@
|
||||
part of '../spotify.dart';
|
||||
|
||||
class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?>
|
||||
with Persistence<SubtitleSimple> {
|
||||
SyncedLyricsNotifier() {
|
||||
load();
|
||||
}
|
||||
|
||||
class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
|
||||
Track get _track => arg!;
|
||||
|
||||
Future<SubtitleSimple> getSpotifyLyrics(String? token) async {
|
||||
@ -128,12 +123,25 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?>
|
||||
@override
|
||||
FutureOr<SubtitleSimple> build(track) async {
|
||||
try {
|
||||
final database = ref.watch(databaseProvider);
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
|
||||
if (track == null) {
|
||||
throw "No track currently";
|
||||
}
|
||||
|
||||
final cachedLyrics = await (database.select(database.lyricsTable)
|
||||
..where((tbl) => tbl.trackId.equals(track.id!)))
|
||||
.map((row) => row.data)
|
||||
.getSingleOrNull();
|
||||
|
||||
SubtitleSimple? lyrics = cachedLyrics;
|
||||
|
||||
final token = await spotify.getCredentials();
|
||||
SubtitleSimple lyrics = await getSpotifyLyrics(token.accessToken);
|
||||
|
||||
if (lyrics == null || lyrics.lyrics.isEmpty) {
|
||||
lyrics = await getSpotifyLyrics(token.accessToken);
|
||||
}
|
||||
|
||||
if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) {
|
||||
lyrics = await getLRCLibLyrics();
|
||||
@ -143,19 +151,21 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?>
|
||||
throw Exception("Unable to find lyrics");
|
||||
}
|
||||
|
||||
if (cachedLyrics == null || cachedLyrics.lyrics.isEmpty) {
|
||||
await database.into(database.lyricsTable).insertOnConflictUpdate(
|
||||
LyricsTableCompanion.insert(
|
||||
trackId: track.id!,
|
||||
data: lyrics,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return lyrics;
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.reportError(e, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<SubtitleSimple> fromJson(Map<String, dynamic> json) =>
|
||||
SubtitleSimple.fromJson(json.castKeyDeep<String>());
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(SubtitleSimple data) => data.toJson();
|
||||
}
|
||||
|
||||
final syncedLyricsDelayProvider = StateProvider<int>((ref) => 0);
|
||||
|
@ -1,10 +1,6 @@
|
||||
part of '../spotify.dart';
|
||||
|
||||
class LikedTracksNotifier extends AsyncNotifier<List<Track>> with Persistence {
|
||||
LikedTracksNotifier() {
|
||||
load();
|
||||
}
|
||||
|
||||
class LikedTracksNotifier extends AsyncNotifier<List<Track>> {
|
||||
@override
|
||||
FutureOr<List<Track>> build() async {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
@ -29,18 +25,6 @@ class LikedTracksNotifier extends AsyncNotifier<List<Track>> with Persistence {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<List<Track>> fromJson(Map<String, dynamic> json) {
|
||||
return (json['tracks'] as List).map((e) => Track.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(List<Track> data) {
|
||||
return {
|
||||
'tracks': data.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final likedTracksProvider =
|
||||
|
@ -2,6 +2,9 @@ library spotify;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/spotify/utils/json_cast.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
@ -15,7 +18,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
// ignore: depend_on_referenced_packages, implementation_imports
|
||||
import 'package:riverpod/src/async_notifier.dart';
|
||||
import 'package:spotube/extensions/album_simple.dart';
|
||||
import 'package:spotube/extensions/map.dart';
|
||||
import 'package:spotube/extensions/track.dart';
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||
@ -25,7 +27,6 @@ import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/dio/dio.dart';
|
||||
import 'package:spotube/services/wikipedia/wikipedia.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
|
||||
import 'package:wikipedia_api/wikipedia_api.dart';
|
||||
|
||||
|
21
lib/provider/spotify/utils/json_cast.dart
Normal file
21
lib/provider/spotify/utils/json_cast.dart
Normal file
@ -0,0 +1,21 @@
|
||||
Map<String, dynamic> castNestedJson(Map map) {
|
||||
return Map.castFrom<dynamic, dynamic, String, dynamic>(
|
||||
map.map((key, value) {
|
||||
if (value is Map) {
|
||||
return MapEntry(
|
||||
key,
|
||||
castNestedJson(value),
|
||||
);
|
||||
} else if (value is Iterable) {
|
||||
return MapEntry(
|
||||
key,
|
||||
value.map((e) {
|
||||
if (e is Map) return castNestedJson(e);
|
||||
return e;
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
}),
|
||||
);
|
||||
}
|
@ -16,7 +16,7 @@ mixin Persistence<T> on BuildlessAsyncNotifier<T> {
|
||||
(json is List && json.isNotEmpty)) {
|
||||
state = AsyncData(
|
||||
await fromJson(
|
||||
PersistedStateNotifier.castNestedJson(json),
|
||||
castNestedJson(json),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class PersistedChangeNotifier extends ChangeNotifier {
|
||||
late SharedPreferences _localStorage;
|
||||
PersistedChangeNotifier() {
|
||||
SharedPreferences.getInstance().then((value) => _localStorage = value).then(
|
||||
(_) async {
|
||||
final persistedMap = (await toMap())
|
||||
.entries
|
||||
.toList()
|
||||
.fold<Map<String, dynamic>>({}, (acc, entry) {
|
||||
if (entry.value != null) {
|
||||
if (entry.value is bool) {
|
||||
acc[entry.key] = _localStorage.getBool(entry.key);
|
||||
} else if (entry.value is int) {
|
||||
acc[entry.key] = _localStorage.getInt(entry.key);
|
||||
} else if (entry.value is double) {
|
||||
acc[entry.key] = _localStorage.getDouble(entry.key);
|
||||
} else if (entry.value is String) {
|
||||
acc[entry.key] = _localStorage.getString(entry.key);
|
||||
}
|
||||
} else {
|
||||
acc[entry.key] = _localStorage.get(entry.key);
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
await loadFromLocal(persistedMap);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FutureOr<void> loadFromLocal(Map<String, dynamic> map);
|
||||
|
||||
FutureOr<Map<String, dynamic>> toMap();
|
||||
|
||||
Future<void> updatePersistence({bool clearNullEntries = false}) async {
|
||||
for (final entry in (await toMap()).entries) {
|
||||
if (entry.value is bool) {
|
||||
await _localStorage.setBool(entry.key, entry.value);
|
||||
} else if (entry.value is int) {
|
||||
await _localStorage.setInt(entry.key, entry.value);
|
||||
} else if (entry.value is double) {
|
||||
await _localStorage.setDouble(entry.key, entry.value);
|
||||
} else if (entry.value is String) {
|
||||
await _localStorage.setString(entry.key, entry.value);
|
||||
} else if (entry.value == null && clearNullEntries) {
|
||||
_localStorage.remove(entry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
||||
const secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
);
|
||||
|
||||
const kKeyBoxName = "spotube_box_name";
|
||||
const kNoEncryptionWarningShownKey = "showedNoEncryptionWarning";
|
||||
const kIsUsingEncryption = "isUsingEncryption";
|
||||
String getBoxKey(String boxName) => "spotube_box_$boxName";
|
||||
|
||||
abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||
final String cacheKey;
|
||||
final bool encrypted;
|
||||
|
||||
FutureOr<void> onInit() {}
|
||||
|
||||
PersistedStateNotifier(
|
||||
super.state,
|
||||
this.cacheKey, {
|
||||
this.encrypted = false,
|
||||
}) {
|
||||
_load().then((_) => onInit());
|
||||
}
|
||||
|
||||
static late LazyBox _box;
|
||||
static late LazyBox _encryptedBox;
|
||||
|
||||
static Future<void> showNoEncryptionDialog(BuildContext context) async {
|
||||
final localStorage = await SharedPreferences.getInstance();
|
||||
final wasShownAlready =
|
||||
localStorage.getBool(kNoEncryptionWarningShownKey) == true;
|
||||
|
||||
if (wasShownAlready || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n.failed_to_encrypt,
|
||||
message: context.l10n.encryption_failed_warning,
|
||||
cancelText: null,
|
||||
);
|
||||
await localStorage.setBool(kNoEncryptionWarningShownKey, true);
|
||||
}
|
||||
|
||||
static Future<String?> read(String key) async {
|
||||
final localStorage = await SharedPreferences.getInstance();
|
||||
if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) {
|
||||
return localStorage.getString(key);
|
||||
}
|
||||
|
||||
try {
|
||||
await localStorage.setBool(kIsUsingEncryption, true);
|
||||
return await secureStorage.read(key: key);
|
||||
} catch (e) {
|
||||
await localStorage.setBool(kIsUsingEncryption, false);
|
||||
return localStorage.getString(key);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> write(String key, String value) async {
|
||||
final localStorage = await SharedPreferences.getInstance();
|
||||
if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) {
|
||||
await localStorage.setString(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await localStorage.setBool(kIsUsingEncryption, true);
|
||||
await secureStorage.write(key: key, value: value);
|
||||
} catch (e) {
|
||||
await localStorage.setBool(kIsUsingEncryption, false);
|
||||
await localStorage.setString(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> initializeBoxes({required String? path}) async {
|
||||
String? boxName = await read(kKeyBoxName);
|
||||
|
||||
if (boxName == null) {
|
||||
boxName = "spotube-${PrimitiveUtils.uuid.v4()}";
|
||||
await write(kKeyBoxName, boxName);
|
||||
}
|
||||
|
||||
String? encryptionKey = await read(getBoxKey(boxName));
|
||||
|
||||
if (encryptionKey == null) {
|
||||
encryptionKey = base64Url.encode(Hive.generateSecureKey());
|
||||
await write(getBoxKey(boxName), encryptionKey);
|
||||
}
|
||||
|
||||
_encryptedBox = await Hive.openLazyBox(
|
||||
boxName,
|
||||
encryptionCipher: HiveAesCipher(base64Url.decode(encryptionKey)),
|
||||
);
|
||||
|
||||
_box = await Hive.openLazyBox(
|
||||
"spotube_cache",
|
||||
path: path,
|
||||
);
|
||||
}
|
||||
|
||||
LazyBox get box => encrypted ? _encryptedBox : _box;
|
||||
|
||||
Future<void> _load() async {
|
||||
final json = await box.get(cacheKey);
|
||||
|
||||
if (json != null ||
|
||||
(json is Map && json.entries.isNotEmpty) ||
|
||||
(json is List && json.isNotEmpty)) {
|
||||
state = await fromJson(castNestedJson(json));
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, dynamic> castNestedJson(Map map) {
|
||||
return Map.castFrom<dynamic, dynamic, String, dynamic>(
|
||||
map.map((key, value) {
|
||||
if (value is Map) {
|
||||
return MapEntry(
|
||||
key,
|
||||
castNestedJson(value),
|
||||
);
|
||||
} else if (value is Iterable) {
|
||||
return MapEntry(
|
||||
key,
|
||||
value.map((e) {
|
||||
if (e is Map) return castNestedJson(e);
|
||||
return e;
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void save() async {
|
||||
await box.put(cacheKey, toJson());
|
||||
}
|
||||
|
||||
FutureOr<T> fromJson(Map<String, dynamic> json);
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
set state(T value) {
|
||||
if (state == value) return;
|
||||
super.state = value;
|
||||
save();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user