From 1cfd377c298e98f460a082e1196c441e71291079 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 11:01:40 +0600 Subject: [PATCH] refactor: synced lyric cache to use drift db --- .../sections/header/header_buttons.dart | 4 +- lib/main.dart | 5 - lib/models/database/database.dart | 4 + lib/models/database/database.g.dart | 323 ++++++++++++++++++ lib/models/database/tables/lyrics.dart | 8 + .../database/typeconverters/subtitle.dart | 13 + lib/pages/root/root_app.dart | 9 - lib/provider/spotify/lyrics/synced.dart | 38 ++- lib/provider/spotify/playlist/liked.dart | 18 +- lib/provider/spotify/spotify.dart | 5 +- lib/provider/spotify/utils/json_cast.dart | 21 ++ lib/provider/spotify/utils/persistence.dart | 2 +- lib/utils/persisted_change_notifier.dart | 55 --- lib/utils/persisted_state_notifier.dart | 164 --------- 14 files changed, 401 insertions(+), 268 deletions(-) create mode 100644 lib/models/database/tables/lyrics.dart create mode 100644 lib/models/database/typeconverters/subtitle.dart create mode 100644 lib/provider/spotify/utils/json_cast.dart delete mode 100644 lib/utils/persisted_change_notifier.dart delete mode 100644 lib/utils/persisted_state_notifier.dart diff --git a/lib/components/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart index e9fbf6bb..54e0f0cf 100644 --- a/lib/components/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/tracks_view/sections/header/header_buttons.dart @@ -131,7 +131,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget { ); } } finally { - isLoading.value = false; + if (context.mounted) { + isLoading.value = false; + } } } diff --git a/lib/main.dart b/lib/main.dart index 9b92a21d..69c89062 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 main(List rawArgs) async { Hive.init(hiveCacheDir); - await PersistedStateNotifier.initializeBoxes( - path: hiveCacheDir, - ); - if (kIsDesktop) { await localNotifier.setup(appName: "Spotube"); await WindowManagerTools.initialize(); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 9b47aaab..609d6771 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -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 { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index bb7d2fb6..1e585fa8 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3709,6 +3709,218 @@ class HistoryTableCompanion extends UpdateCompanion { } } +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 id = GeneratedColumn( + '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 trackId = GeneratedColumn( + 'track_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _dataMeta = const VerificationMeta('data'); + @override + late final GeneratedColumnWithTypeConverter data = + GeneratedColumn('data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($LyricsTableTable.$converterdata); + @override + List 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 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 get $primaryKey => {id}; + @override + LyricsTableData map(Map 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 $converterdata = + SubtitleTypeConverter(); +} + +class LyricsTableData extends DataClass implements Insertable { + final int id; + final String trackId; + final SubtitleSimple data; + const LyricsTableData( + {required this.id, required this.trackId, required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + { + map['data'] = + Variable($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 json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LyricsTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + data: serializer.fromJson(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'data': serializer.toJson(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 { + final Value id; + final Value trackId; + final Value 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 custom({ + Expression? id, + Expression? trackId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (data != null) 'data': data, + }); + } + + LyricsTableCompanion copyWith( + {Value? id, Value? trackId, Value? data}) { + return LyricsTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (data.present) { + map['data'] = + Variable($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 id, + required String trackId, + required SubtitleSimple data, +}); +typedef $$LyricsTableTableUpdateCompanionBuilder = LyricsTableCompanion + Function({ + Value id, + Value trackId, + Value 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 id = const Value.absent(), + Value trackId = const Value.absent(), + Value data = const Value.absent(), + }) => + LyricsTableCompanion( + id: id, + trackId: trackId, + data: data, + ), + getInsertCompanionBuilder: ({ + Value 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 get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + 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 get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings 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); } diff --git a/lib/models/database/tables/lyrics.dart b/lib/models/database/tables/lyrics.dart new file mode 100644 index 00000000..7c4c7f8f --- /dev/null +++ b/lib/models/database/tables/lyrics.dart @@ -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())(); +} diff --git a/lib/models/database/typeconverters/subtitle.dart b/lib/models/database/typeconverters/subtitle.dart new file mode 100644 index 00000000..25fa4ad5 --- /dev/null +++ b/lib/models/database/typeconverters/subtitle.dart @@ -0,0 +1,13 @@ +part of '../database.dart'; + +class SubtitleTypeConverter extends TypeConverter { + @override + SubtitleSimple fromSql(String fromDb) { + return SubtitleSimple.fromJson(jsonDecode(fromDb)); + } + + @override + String toSql(SubtitleSimple value) { + return jsonEncode(value.toJson()); + } +} diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 322a8731..402a7cf0 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -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 = [ diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index ef83a1a1..bcf2a162 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -1,11 +1,6 @@ part of '../spotify.dart'; -class SyncedLyricsNotifier extends FamilyAsyncNotifier - with Persistence { - SyncedLyricsNotifier() { - load(); - } - +class SyncedLyricsNotifier extends FamilyAsyncNotifier { Track get _track => arg!; Future getSpotifyLyrics(String? token) async { @@ -128,12 +123,25 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier @override FutureOr 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 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 fromJson(Map json) => - SubtitleSimple.fromJson(json.castKeyDeep()); - - @override - Map toJson(SubtitleSimple data) => data.toJson(); } final syncedLyricsDelayProvider = StateProvider((ref) => 0); diff --git a/lib/provider/spotify/playlist/liked.dart b/lib/provider/spotify/playlist/liked.dart index 52463d3d..27c3e2b6 100644 --- a/lib/provider/spotify/playlist/liked.dart +++ b/lib/provider/spotify/playlist/liked.dart @@ -1,10 +1,6 @@ part of '../spotify.dart'; -class LikedTracksNotifier extends AsyncNotifier> with Persistence { - LikedTracksNotifier() { - load(); - } - +class LikedTracksNotifier extends AsyncNotifier> { @override FutureOr> build() async { final spotify = ref.watch(spotifyProvider); @@ -29,18 +25,6 @@ class LikedTracksNotifier extends AsyncNotifier> with Persistence { } }); } - - @override - FutureOr> fromJson(Map json) { - return (json['tracks'] as List).map((e) => Track.fromJson(e)).toList(); - } - - @override - Map toJson(List data) { - return { - 'tracks': data.map((e) => e.toJson()).toList(), - }; - } } final likedTracksProvider = diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index e592e93b..63a8ed38 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -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'; diff --git a/lib/provider/spotify/utils/json_cast.dart b/lib/provider/spotify/utils/json_cast.dart new file mode 100644 index 00000000..30700971 --- /dev/null +++ b/lib/provider/spotify/utils/json_cast.dart @@ -0,0 +1,21 @@ +Map castNestedJson(Map map) { + return Map.castFrom( + 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); + }), + ); +} diff --git a/lib/provider/spotify/utils/persistence.dart b/lib/provider/spotify/utils/persistence.dart index 14d3c940..57f41dec 100644 --- a/lib/provider/spotify/utils/persistence.dart +++ b/lib/provider/spotify/utils/persistence.dart @@ -16,7 +16,7 @@ mixin Persistence on BuildlessAsyncNotifier { (json is List && json.isNotEmpty)) { state = AsyncData( await fromJson( - PersistedStateNotifier.castNestedJson(json), + castNestedJson(json), ), ); } diff --git a/lib/utils/persisted_change_notifier.dart b/lib/utils/persisted_change_notifier.dart deleted file mode 100644 index d48cb67a..00000000 --- a/lib/utils/persisted_change_notifier.dart +++ /dev/null @@ -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>({}, (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 loadFromLocal(Map map); - - FutureOr> toMap(); - - Future 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); - } - } - } -} diff --git a/lib/utils/persisted_state_notifier.dart b/lib/utils/persisted_state_notifier.dart deleted file mode 100644 index 450bb664..00000000 --- a/lib/utils/persisted_state_notifier.dart +++ /dev/null @@ -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 extends StateNotifier { - final String cacheKey; - final bool encrypted; - - FutureOr onInit() {} - - PersistedStateNotifier( - super.state, - this.cacheKey, { - this.encrypted = false, - }) { - _load().then((_) => onInit()); - } - - static late LazyBox _box; - static late LazyBox _encryptedBox; - - static Future 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 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 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 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 _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 castNestedJson(Map map) { - return Map.castFrom( - 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 fromJson(Map json); - Map toJson(); - - @override - set state(T value) { - if (state == value) return; - super.state = value; - save(); - } -}