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 {
|
} 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/logger/logger.dart';
|
||||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||||
import 'package:spotube/themes/theme.dart';
|
import 'package:spotube/themes/theme.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@ -85,10 +84,6 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
|
|
||||||
Hive.init(hiveCacheDir);
|
Hive.init(hiveCacheDir);
|
||||||
|
|
||||||
await PersistedStateNotifier.initializeBoxes(
|
|
||||||
path: hiveCacheDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (kIsDesktop) {
|
if (kIsDesktop) {
|
||||||
await localNotifier.setup(appName: "Spotube");
|
await localNotifier.setup(appName: "Spotube");
|
||||||
await WindowManagerTools.initialize();
|
await WindowManagerTools.initialize();
|
||||||
|
@ -9,6 +9,7 @@ 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' hide Playlist;
|
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/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';
|
||||||
@ -28,12 +29,14 @@ 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 'tables/history.dart';
|
||||||
|
part 'tables/lyrics.dart';
|
||||||
|
|
||||||
part 'typeconverters/color.dart';
|
part 'typeconverters/color.dart';
|
||||||
part 'typeconverters/locale.dart';
|
part 'typeconverters/locale.dart';
|
||||||
part 'typeconverters/string_list.dart';
|
part 'typeconverters/string_list.dart';
|
||||||
part 'typeconverters/encrypted_text.dart';
|
part 'typeconverters/encrypted_text.dart';
|
||||||
part 'typeconverters/map.dart';
|
part 'typeconverters/map.dart';
|
||||||
|
part 'typeconverters/subtitle.dart';
|
||||||
|
|
||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
tables: [
|
tables: [
|
||||||
@ -47,6 +50,7 @@ part 'typeconverters/map.dart';
|
|||||||
PlaylistTable,
|
PlaylistTable,
|
||||||
PlaylistMediaTable,
|
PlaylistMediaTable,
|
||||||
HistoryTable,
|
HistoryTable,
|
||||||
|
LyricsTable,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
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 {
|
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);
|
||||||
@ -3728,6 +3940,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
late final $PlaylistMediaTableTable playlistMediaTable =
|
late final $PlaylistMediaTableTable playlistMediaTable =
|
||||||
$PlaylistMediaTableTable(this);
|
$PlaylistMediaTableTable(this);
|
||||||
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
||||||
|
late final $LyricsTableTable lyricsTable = $LyricsTableTable(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',
|
||||||
@ -3747,6 +3960,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
playlistTable,
|
playlistTable,
|
||||||
playlistMediaTable,
|
playlistMediaTable,
|
||||||
historyTable,
|
historyTable,
|
||||||
|
lyricsTable,
|
||||||
uniqueBlacklist,
|
uniqueBlacklist,
|
||||||
uniqTrackMatch
|
uniqTrackMatch
|
||||||
];
|
];
|
||||||
@ -5492,6 +5706,113 @@ class $$HistoryTableTableOrderingComposer
|
|||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
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 {
|
class _$AppDatabaseManager {
|
||||||
final _$AppDatabase _db;
|
final _$AppDatabase _db;
|
||||||
_$AppDatabaseManager(this._db);
|
_$AppDatabaseManager(this._db);
|
||||||
@ -5515,4 +5836,6 @@ class _$AppDatabaseManager {
|
|||||||
$$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable);
|
$$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable);
|
||||||
$$HistoryTableTableTableManager get historyTable =>
|
$$HistoryTableTableTableManager get historyTable =>
|
||||||
$$HistoryTableTableTableManager(_db, _db.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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/modules/player/player_queue.dart';
|
import 'package:spotube/modules/player/player_queue.dart';
|
||||||
import 'package:spotube/components/dialogs/replace_downloaded_dialog.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/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/server/routes/connect.dart';
|
import 'package:spotube/provider/server/routes/connect.dart';
|
||||||
import 'package:spotube/services/connectivity_adapter.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/platform.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -41,13 +39,6 @@ class RootApp extends HookConsumerWidget {
|
|||||||
useEffect(() {
|
useEffect(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
ServiceUtils.checkForUpdates(context, ref);
|
ServiceUtils.checkForUpdates(context, ref);
|
||||||
|
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
|
|
||||||
context.mounted) {
|
|
||||||
await PersistedStateNotifier.showNoEncryptionDialog(context);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final subscriptions = [
|
final subscriptions = [
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
part of '../spotify.dart';
|
part of '../spotify.dart';
|
||||||
|
|
||||||
class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?>
|
class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
|
||||||
with Persistence<SubtitleSimple> {
|
|
||||||
SyncedLyricsNotifier() {
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
Track get _track => arg!;
|
Track get _track => arg!;
|
||||||
|
|
||||||
Future<SubtitleSimple> getSpotifyLyrics(String? token) async {
|
Future<SubtitleSimple> getSpotifyLyrics(String? token) async {
|
||||||
@ -128,12 +123,25 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?>
|
|||||||
@override
|
@override
|
||||||
FutureOr<SubtitleSimple> build(track) async {
|
FutureOr<SubtitleSimple> build(track) async {
|
||||||
try {
|
try {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
|
|
||||||
if (track == null) {
|
if (track == null) {
|
||||||
throw "No track currently";
|
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();
|
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) {
|
if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) {
|
||||||
lyrics = await getLRCLibLyrics();
|
lyrics = await getLRCLibLyrics();
|
||||||
@ -143,19 +151,21 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?>
|
|||||||
throw Exception("Unable to find lyrics");
|
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;
|
return lyrics;
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
AppLogger.reportError(e, stackTrace);
|
AppLogger.reportError(e, stackTrace);
|
||||||
rethrow;
|
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);
|
final syncedLyricsDelayProvider = StateProvider<int>((ref) => 0);
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
part of '../spotify.dart';
|
part of '../spotify.dart';
|
||||||
|
|
||||||
class LikedTracksNotifier extends AsyncNotifier<List<Track>> with Persistence {
|
class LikedTracksNotifier extends AsyncNotifier<List<Track>> {
|
||||||
LikedTracksNotifier() {
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<List<Track>> build() async {
|
FutureOr<List<Track>> build() async {
|
||||||
final spotify = ref.watch(spotifyProvider);
|
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 =
|
final likedTracksProvider =
|
||||||
|
@ -2,6 +2,9 @@ library spotify;
|
|||||||
|
|
||||||
import 'dart:async';
|
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:spotube/services/logger/logger.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@ -15,7 +18,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
// ignore: depend_on_referenced_packages, implementation_imports
|
// ignore: depend_on_referenced_packages, implementation_imports
|
||||||
import 'package:riverpod/src/async_notifier.dart';
|
import 'package:riverpod/src/async_notifier.dart';
|
||||||
import 'package:spotube/extensions/album_simple.dart';
|
import 'package:spotube/extensions/album_simple.dart';
|
||||||
import 'package:spotube/extensions/map.dart';
|
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/extensions/track.dart';
|
||||||
import 'package:spotube/models/lyrics.dart';
|
import 'package:spotube/models/lyrics.dart';
|
||||||
import 'package:spotube/models/spotify/recommendation_seeds.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/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/dio/dio.dart';
|
import 'package:spotube/services/dio/dio.dart';
|
||||||
import 'package:spotube/services/wikipedia/wikipedia.dart';
|
import 'package:spotube/services/wikipedia/wikipedia.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
|
||||||
|
|
||||||
import 'package:wikipedia_api/wikipedia_api.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)) {
|
(json is List && json.isNotEmpty)) {
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
await fromJson(
|
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