🔀 Merge database system from spotube
This commit is contained in:
19
lib/services/color.dart
Normal file
19
lib/services/color.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class RhythmColor extends Color {
|
||||
final String name;
|
||||
|
||||
const RhythmColor(super.color, {required this.name});
|
||||
|
||||
const RhythmColor.from(super.value, {required this.name});
|
||||
|
||||
factory RhythmColor.fromString(String string) {
|
||||
final slices = string.split(':');
|
||||
return RhythmColor(int.parse(slices.last), name: slices.first);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$name:$value';
|
||||
}
|
||||
}
|
80
lib/services/database/database.dart
Executable file
80
lib/services/database/database.dart
Executable file
@ -0,0 +1,80 @@
|
||||
library database;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rhythm_box/services/color.dart';
|
||||
import 'package:rhythm_box/services/lyrics.dart';
|
||||
import 'package:spotify/spotify.dart' hide Playlist;
|
||||
import 'package:rhythm_box/services/kv_store/encrypted_kv_store.dart';
|
||||
import 'package:rhythm_box/services/kv_store/kv_store.dart';
|
||||
import 'package:rhythm_box/services/sourced_track/enums.dart';
|
||||
import 'package:flutter/material.dart' hide Table, Key, View;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
part 'tables/authentication.dart';
|
||||
part 'tables/blacklist.dart';
|
||||
part 'tables/preferences.dart';
|
||||
part 'tables/scrobbler.dart';
|
||||
part 'tables/skip_segment.dart';
|
||||
part 'tables/source_match.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: [
|
||||
AuthenticationTable,
|
||||
BlacklistTable,
|
||||
PreferencesTable,
|
||||
ScrobblerTable,
|
||||
SkipSegmentTable,
|
||||
SourceMatchTable,
|
||||
HistoryTable,
|
||||
LyricsTable,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
// the LazyDatabase util lets us find the right location for the file async.
|
||||
return LazyDatabase(() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
// for your app.
|
||||
final dbFolder = await getApplicationSupportDirectory();
|
||||
final file = File(join(dbFolder.path, 'db.sqlite'));
|
||||
|
||||
// Also work around limitations on old Android versions
|
||||
if (Platform.isAndroid) {
|
||||
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
}
|
||||
|
||||
// Make sqlite3 pick a more suitable location for temporary files - the
|
||||
// one from the system may be inaccessible due to sandboxing.
|
||||
final cacheBase = (await getTemporaryDirectory()).path;
|
||||
// We can't access /tmp on Android, which sqlite3 would try by default.
|
||||
// Explicitly tell it about the correct temporary directory.
|
||||
sqlite3.tempDirectory = cacheBase;
|
||||
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
4744
lib/services/database/database.g.dart
Normal file
4744
lib/services/database/database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
8
lib/services/database/tables/authentication.dart
Executable file
8
lib/services/database/tables/authentication.dart
Executable file
@ -0,0 +1,8 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class AuthenticationTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get cookie => text().map(EncryptedTextConverter())();
|
||||
TextColumn get accessToken => text().map(EncryptedTextConverter())();
|
||||
DateTimeColumn get expiration => dateTime()();
|
||||
}
|
18
lib/services/database/tables/blacklist.dart
Executable file
18
lib/services/database/tables/blacklist.dart
Executable file
@ -0,0 +1,18 @@
|
||||
part of '../database.dart';
|
||||
|
||||
enum BlacklistedType {
|
||||
artist,
|
||||
track;
|
||||
}
|
||||
|
||||
@TableIndex(
|
||||
name: "unique_blacklist",
|
||||
unique: true,
|
||||
columns: {#elementType, #elementId},
|
||||
)
|
||||
class BlacklistTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get elementType => textEnum<BlacklistedType>()();
|
||||
TextColumn get elementId => text()();
|
||||
}
|
25
lib/services/database/tables/history.dart
Executable file
25
lib/services/database/tables/history.dart
Executable file
@ -0,0 +1,25 @@
|
||||
part of '../database.dart';
|
||||
|
||||
enum HistoryEntryType {
|
||||
playlist,
|
||||
album,
|
||||
track,
|
||||
}
|
||||
|
||||
class HistoryTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
TextColumn get type => textEnum<HistoryEntryType>()();
|
||||
TextColumn get itemId => text()();
|
||||
TextColumn get data =>
|
||||
text().map(const MapTypeConverter<String, dynamic>())();
|
||||
}
|
||||
|
||||
extension HistoryItemParseExtension on HistoryTableData {
|
||||
PlaylistSimple? get playlist =>
|
||||
type == HistoryEntryType.playlist ? PlaylistSimple.fromJson(data) : null;
|
||||
AlbumSimple? get album =>
|
||||
type == HistoryEntryType.album ? AlbumSimple.fromJson(data) : null;
|
||||
Track? get track =>
|
||||
type == HistoryEntryType.track ? Track.fromJson(data) : null;
|
||||
}
|
8
lib/services/database/tables/lyrics.dart
Executable file
8
lib/services/database/tables/lyrics.dart
Executable 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())();
|
||||
}
|
125
lib/services/database/tables/preferences.dart
Executable file
125
lib/services/database/tables/preferences.dart
Executable file
@ -0,0 +1,125 @@
|
||||
part of '../database.dart';
|
||||
|
||||
enum LayoutMode {
|
||||
compact,
|
||||
extended,
|
||||
adaptive,
|
||||
}
|
||||
|
||||
enum CloseBehavior {
|
||||
minimizeToTray,
|
||||
close,
|
||||
}
|
||||
|
||||
enum AudioSource {
|
||||
youtube,
|
||||
piped,
|
||||
jiosaavn;
|
||||
|
||||
String get label => name[0].toUpperCase() + name.substring(1);
|
||||
}
|
||||
|
||||
enum MusicCodec {
|
||||
m4a._('M4a (Best for downloaded music)'),
|
||||
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
|
||||
|
||||
final String label;
|
||||
const MusicCodec._(this.label);
|
||||
}
|
||||
|
||||
enum SearchMode {
|
||||
youtube._('YouTube'),
|
||||
youtubeMusic._('YouTube Music');
|
||||
|
||||
final String label;
|
||||
|
||||
const SearchMode._(this.label);
|
||||
|
||||
factory SearchMode.fromString(String key) {
|
||||
return SearchMode.values.firstWhere((e) => e.name == key);
|
||||
}
|
||||
}
|
||||
|
||||
class PreferencesTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get audioQuality => textEnum<SourceQualities>()
|
||||
.withDefault(Constant(SourceQualities.high.name))();
|
||||
BoolColumn get albumColorSync =>
|
||||
boolean().withDefault(const Constant(true))();
|
||||
BoolColumn get amoledDarkTheme =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get checkUpdate => boolean().withDefault(const Constant(true))();
|
||||
BoolColumn get normalizeAudio =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get showSystemTrayIcon =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get systemTitleBar =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get skipNonMusic => boolean().withDefault(const Constant(false))();
|
||||
TextColumn get closeBehavior => textEnum<CloseBehavior>()
|
||||
.withDefault(Constant(CloseBehavior.close.name))();
|
||||
TextColumn get accentColorScheme => text()
|
||||
.withDefault(const Constant('Blue:0xFF2196F3'))
|
||||
.map(const RhythmColorConverter())();
|
||||
TextColumn get layoutMode =>
|
||||
textEnum<LayoutMode>().withDefault(Constant(LayoutMode.adaptive.name))();
|
||||
TextColumn get locale => text()
|
||||
.withDefault(
|
||||
const Constant('{"languageCode":"system","countryCode":"system"}'),
|
||||
)
|
||||
.map(const LocaleConverter())();
|
||||
TextColumn get market =>
|
||||
textEnum<Market>().withDefault(Constant(Market.US.name))();
|
||||
TextColumn get searchMode =>
|
||||
textEnum<SearchMode>().withDefault(Constant(SearchMode.youtube.name))();
|
||||
TextColumn get downloadLocation => text().withDefault(const Constant(''))();
|
||||
TextColumn get localLibraryLocation =>
|
||||
text().withDefault(const Constant('')).map(const StringListConverter())();
|
||||
TextColumn get pipedInstance =>
|
||||
text().withDefault(const Constant('https://pipedapi.kavin.rocks'))();
|
||||
TextColumn get themeMode =>
|
||||
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
|
||||
TextColumn get audioSource =>
|
||||
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
|
||||
TextColumn get streamMusicCodec =>
|
||||
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))();
|
||||
TextColumn get downloadMusicCodec =>
|
||||
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.m4a.name))();
|
||||
BoolColumn get discordPresence =>
|
||||
boolean().withDefault(const Constant(true))();
|
||||
BoolColumn get endlessPlayback =>
|
||||
boolean().withDefault(const Constant(true))();
|
||||
BoolColumn get enableConnect =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
// Default values as PreferencesTableData
|
||||
static PreferencesTableData defaults() {
|
||||
return PreferencesTableData(
|
||||
id: 0,
|
||||
audioQuality: SourceQualities.high,
|
||||
albumColorSync: true,
|
||||
amoledDarkTheme: false,
|
||||
checkUpdate: true,
|
||||
normalizeAudio: false,
|
||||
showSystemTrayIcon: false,
|
||||
systemTitleBar: false,
|
||||
skipNonMusic: false,
|
||||
closeBehavior: CloseBehavior.close,
|
||||
accentColorScheme: RhythmColor(Colors.blue.value, name: 'Blue'),
|
||||
layoutMode: LayoutMode.adaptive,
|
||||
locale: const Locale('system', 'system'),
|
||||
market: Market.US,
|
||||
searchMode: SearchMode.youtube,
|
||||
downloadLocation: '',
|
||||
localLibraryLocation: [],
|
||||
pipedInstance: 'https://pipedapi.kavin.rocks',
|
||||
themeMode: ThemeMode.system,
|
||||
audioSource: AudioSource.youtube,
|
||||
streamMusicCodec: SourceCodecs.weba,
|
||||
downloadMusicCodec: SourceCodecs.m4a,
|
||||
discordPresence: true,
|
||||
endlessPlayback: true,
|
||||
enableConnect: false,
|
||||
);
|
||||
}
|
||||
}
|
8
lib/services/database/tables/scrobbler.dart
Executable file
8
lib/services/database/tables/scrobbler.dart
Executable file
@ -0,0 +1,8 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class ScrobblerTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
TextColumn get username => text()();
|
||||
TextColumn get passwordHash => text().map(EncryptedTextConverter())();
|
||||
}
|
9
lib/services/database/tables/skip_segment.dart
Executable file
9
lib/services/database/tables/skip_segment.dart
Executable file
@ -0,0 +1,9 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class SkipSegmentTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get start => integer()();
|
||||
IntColumn get end => integer()();
|
||||
TextColumn get trackId => text()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
25
lib/services/database/tables/source_match.dart
Executable file
25
lib/services/database/tables/source_match.dart
Executable file
@ -0,0 +1,25 @@
|
||||
part of '../database.dart';
|
||||
|
||||
enum SourceType {
|
||||
youtube._("YouTube"),
|
||||
youtubeMusic._("YouTube Music"),
|
||||
jiosaavn._("JioSaavn");
|
||||
|
||||
final String label;
|
||||
|
||||
const SourceType._(this.label);
|
||||
}
|
||||
|
||||
@TableIndex(
|
||||
name: "uniq_track_match",
|
||||
columns: {#trackId, #sourceId, #sourceType},
|
||||
unique: true,
|
||||
)
|
||||
class SourceMatchTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get trackId => text()();
|
||||
TextColumn get sourceId => text()();
|
||||
TextColumn get sourceType =>
|
||||
textEnum<SourceType>().withDefault(Constant(SourceType.youtube.name))();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
29
lib/services/database/typeconverters/color.dart
Executable file
29
lib/services/database/typeconverters/color.dart
Executable file
@ -0,0 +1,29 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class ColorConverter extends TypeConverter<Color, int> {
|
||||
const ColorConverter();
|
||||
|
||||
@override
|
||||
Color fromSql(int fromDb) {
|
||||
return Color(fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
int toSql(Color value) {
|
||||
return value.value;
|
||||
}
|
||||
}
|
||||
|
||||
class RhythmColorConverter extends TypeConverter<RhythmColor, String> {
|
||||
const RhythmColorConverter();
|
||||
|
||||
@override
|
||||
RhythmColor fromSql(String fromDb) {
|
||||
return RhythmColor.fromString(fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(RhythmColor value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
44
lib/services/database/typeconverters/encrypted_text.dart
Executable file
44
lib/services/database/typeconverters/encrypted_text.dart
Executable file
@ -0,0 +1,44 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class DecryptedText {
|
||||
final String value;
|
||||
const DecryptedText(this.value);
|
||||
|
||||
static Encrypter? _encrypter;
|
||||
|
||||
factory DecryptedText.decrypted(String value) {
|
||||
_encrypter ??= Encrypter(
|
||||
Salsa20(
|
||||
Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync),
|
||||
),
|
||||
);
|
||||
|
||||
return DecryptedText(
|
||||
_encrypter!.decrypt(
|
||||
Encrypted.fromBase64(value),
|
||||
iv: KVStoreService.ivKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String encrypt() {
|
||||
_encrypter ??= Encrypter(
|
||||
Salsa20(
|
||||
Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync),
|
||||
),
|
||||
);
|
||||
return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64;
|
||||
}
|
||||
}
|
||||
|
||||
class EncryptedTextConverter extends TypeConverter<DecryptedText, String> {
|
||||
@override
|
||||
DecryptedText fromSql(String fromDb) {
|
||||
return DecryptedText.decrypted(fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(DecryptedText value) {
|
||||
return value.encrypt();
|
||||
}
|
||||
}
|
19
lib/services/database/typeconverters/locale.dart
Executable file
19
lib/services/database/typeconverters/locale.dart
Executable file
@ -0,0 +1,19 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class LocaleConverter extends TypeConverter<Locale, String> {
|
||||
const LocaleConverter();
|
||||
|
||||
@override
|
||||
Locale fromSql(String fromDb) {
|
||||
final rawMap = jsonDecode(fromDb) as Map<String, dynamic>;
|
||||
return Locale(rawMap["languageCode"], rawMap["countryCode"]);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(Locale value) {
|
||||
return jsonEncode({
|
||||
"languageCode": value.languageCode,
|
||||
"countryCode": value.countryCode,
|
||||
});
|
||||
}
|
||||
}
|
15
lib/services/database/typeconverters/map.dart
Executable file
15
lib/services/database/typeconverters/map.dart
Executable file
@ -0,0 +1,15 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class MapTypeConverter<K, V> extends TypeConverter<Map<K, V>, String> {
|
||||
const MapTypeConverter();
|
||||
|
||||
@override
|
||||
fromSql(String fromDb) {
|
||||
return json.decode(fromDb) as Map<K, V>;
|
||||
}
|
||||
|
||||
@override
|
||||
toSql(value) {
|
||||
return json.encode(value);
|
||||
}
|
||||
}
|
15
lib/services/database/typeconverters/string_list.dart
Executable file
15
lib/services/database/typeconverters/string_list.dart
Executable file
@ -0,0 +1,15 @@
|
||||
part of '../database.dart';
|
||||
|
||||
class StringListConverter extends TypeConverter<List<String>, String> {
|
||||
const StringListConverter();
|
||||
|
||||
@override
|
||||
List<String> fromSql(String fromDb) {
|
||||
return fromDb.split(",").where((e) => e.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(List<String> value) {
|
||||
return value.join(",");
|
||||
}
|
||||
}
|
13
lib/services/database/typeconverters/subtitle.dart
Executable file
13
lib/services/database/typeconverters/subtitle.dart
Executable 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());
|
||||
}
|
||||
}
|
61
lib/services/kv_store/encrypted_kv_store.dart
Executable file
61
lib/services/kv_store/encrypted_kv_store.dart
Executable file
@ -0,0 +1,61 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:rhythm_box/platform.dart';
|
||||
import 'package:rhythm_box/services/kv_store/kv_store.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
abstract class EncryptedKvStoreService {
|
||||
static const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
);
|
||||
|
||||
static FlutterSecureStorage get storage => _storage;
|
||||
|
||||
static String? _encryptionKeySync;
|
||||
|
||||
static Future<void> initialize() async {
|
||||
_encryptionKeySync = await encryptionKey;
|
||||
}
|
||||
|
||||
static String get encryptionKeySync => _encryptionKeySync!;
|
||||
|
||||
static bool get isUnsupportedPlatform =>
|
||||
PlatformInfo.isMacOS ||
|
||||
PlatformInfo.isIOS ||
|
||||
(PlatformInfo.isLinux && !PlatformInfo.isInFlatpak);
|
||||
|
||||
static Future<String> get encryptionKey async {
|
||||
if (isUnsupportedPlatform) {
|
||||
return KVStoreService.encryptionKey;
|
||||
}
|
||||
try {
|
||||
final value = await _storage.read(key: 'encryption');
|
||||
final key = const Uuid().v4();
|
||||
|
||||
if (value == null) {
|
||||
await setEncryptionKey(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch (e) {
|
||||
return KVStoreService.encryptionKey;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setEncryptionKey(String key) async {
|
||||
if (isUnsupportedPlatform) {
|
||||
await KVStoreService.setEncryptionKey(key);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _storage.write(key: 'encryption', value: key);
|
||||
} catch (e) {
|
||||
await KVStoreService.setEncryptionKey(key);
|
||||
} finally {
|
||||
_encryptionKeySync = key;
|
||||
}
|
||||
}
|
||||
}
|
90
lib/services/kv_store/kv_store.dart
Executable file
90
lib/services/kv_store/kv_store.dart
Executable file
@ -0,0 +1,90 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:rhythm_box/services/wm_tools.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
abstract class KVStoreService {
|
||||
static SharedPreferences? _sharedPreferences;
|
||||
static SharedPreferences get sharedPreferences => _sharedPreferences!;
|
||||
|
||||
static Future<void> initialize() async {
|
||||
_sharedPreferences = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
static bool get doneGettingStarted =>
|
||||
sharedPreferences.getBool('doneGettingStarted') ?? false;
|
||||
static Future<void> setDoneGettingStarted(bool value) async =>
|
||||
await sharedPreferences.setBool('doneGettingStarted', value);
|
||||
|
||||
static bool get askedForBatteryOptimization =>
|
||||
sharedPreferences.getBool('askedForBatteryOptimization') ?? false;
|
||||
static Future<void> setAskedForBatteryOptimization(bool value) async =>
|
||||
await sharedPreferences.setBool('askedForBatteryOptimization', value);
|
||||
|
||||
static List<String> get recentSearches =>
|
||||
sharedPreferences.getStringList('recentSearches') ?? [];
|
||||
|
||||
static Future<void> setRecentSearches(List<String> value) async =>
|
||||
await sharedPreferences.setStringList('recentSearches', value);
|
||||
|
||||
static WindowSize? get windowSize {
|
||||
final raw = sharedPreferences.getString('windowSize');
|
||||
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
return WindowSize.fromJson(jsonDecode(raw));
|
||||
}
|
||||
|
||||
static Future<void> setWindowSize(WindowSize value) async =>
|
||||
await sharedPreferences.setString(
|
||||
'windowSize',
|
||||
jsonEncode(
|
||||
value.toJson(),
|
||||
),
|
||||
);
|
||||
|
||||
static String get encryptionKey {
|
||||
final value = sharedPreferences.getString('encryption');
|
||||
|
||||
final key = const Uuid().v4();
|
||||
if (value == null) {
|
||||
setEncryptionKey(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static Future<void> setEncryptionKey(String key) async {
|
||||
await sharedPreferences.setString('encryption', key);
|
||||
}
|
||||
|
||||
static IV get ivKey {
|
||||
final iv = sharedPreferences.getString('iv');
|
||||
final value = IV.fromSecureRandom(8);
|
||||
|
||||
if (iv == null) {
|
||||
setIVKey(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return IV.fromBase64(iv);
|
||||
}
|
||||
|
||||
static Future<void> setIVKey(IV iv) async {
|
||||
await sharedPreferences.setString('iv', iv.base64);
|
||||
}
|
||||
|
||||
static double get volume => sharedPreferences.getDouble('volume') ?? 1.0;
|
||||
static Future<void> setVolume(double value) async =>
|
||||
await sharedPreferences.setDouble('volume', value);
|
||||
|
||||
static bool get hasMigratedToDrift =>
|
||||
sharedPreferences.getBool('hasMigratedToDrift') ?? false;
|
||||
static Future<void> setHasMigratedToDrift(bool value) async =>
|
||||
await sharedPreferences.setBool('hasMigratedToDrift', value);
|
||||
}
|
72
lib/services/lyrics.dart
Executable file
72
lib/services/lyrics.dart
Executable file
@ -0,0 +1,72 @@
|
||||
import 'package:lrc/lrc.dart';
|
||||
|
||||
class SubtitleSimple {
|
||||
Uri uri;
|
||||
String name;
|
||||
List<LyricSlice> lyrics;
|
||||
int rating;
|
||||
String provider;
|
||||
|
||||
SubtitleSimple({
|
||||
required this.uri,
|
||||
required this.name,
|
||||
required this.lyrics,
|
||||
required this.rating,
|
||||
required this.provider,
|
||||
});
|
||||
|
||||
factory SubtitleSimple.fromJson(Map<String, dynamic> json) {
|
||||
return SubtitleSimple(
|
||||
uri: Uri.parse(json['uri'] as String),
|
||||
name: json['name'] as String,
|
||||
lyrics: (json['lyrics'] as List<dynamic>)
|
||||
.map((e) => LyricSlice.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
rating: json['rating'] as int,
|
||||
provider: json['provider'] as String? ?? 'unknown',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uri': uri.toString(),
|
||||
'name': name,
|
||||
'lyrics': lyrics.map((e) => e.toJson()).toList(),
|
||||
'rating': rating,
|
||||
'provider': provider,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class LyricSlice {
|
||||
Duration time;
|
||||
String text;
|
||||
|
||||
LyricSlice({required this.time, required this.text});
|
||||
|
||||
factory LyricSlice.fromLrcLine(LrcLine line) {
|
||||
return LyricSlice(
|
||||
time: line.timestamp,
|
||||
text: line.lyrics.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
factory LyricSlice.fromJson(Map<String, dynamic> json) {
|
||||
return LyricSlice(
|
||||
time: Duration(milliseconds: json['time']),
|
||||
text: json['text'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'time': time.inMilliseconds,
|
||||
'text': text,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LyricsSlice({time: $time, text: $text})';
|
||||
}
|
||||
}
|
3
lib/services/song_link/song_link.g.dart
Executable file → Normal file
3
lib/services/song_link/song_link.g.dart
Executable file → Normal file
@ -6,7 +6,8 @@ part of 'song_link.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SongLinkImpl _$$SongLinkImplFromJson(Map json) => _$SongLinkImpl(
|
||||
_$SongLinkImpl _$$SongLinkImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SongLinkImpl(
|
||||
displayName: json['displayName'] as String,
|
||||
linkId: json['linkId'] as String,
|
||||
platform: json['platform'] as String,
|
||||
|
4
lib/services/sourced_track/models/source_info.g.dart
Executable file → Normal file
4
lib/services/sourced_track/models/source_info.g.dart
Executable file → Normal file
@ -6,13 +6,13 @@ part of 'source_info.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo(
|
||||
SourceInfo _$SourceInfoFromJson(Map<String, dynamic> json) => SourceInfo(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
artist: json['artist'] as String,
|
||||
thumbnail: json['thumbnail'] as String,
|
||||
pageUrl: json['pageUrl'] as String,
|
||||
duration: Duration(microseconds: json['duration'] as int),
|
||||
duration: Duration(microseconds: (json['duration'] as num).toInt()),
|
||||
artistUrl: json['artistUrl'] as String,
|
||||
album: json['album'] as String?,
|
||||
);
|
||||
|
15
lib/services/sourced_track/models/source_map.g.dart
Executable file → Normal file
15
lib/services/sourced_track/models/source_map.g.dart
Executable file → Normal file
@ -6,7 +6,8 @@ part of 'source_map.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SourceQualityMap _$SourceQualityMapFromJson(Map json) => SourceQualityMap(
|
||||
SourceQualityMap _$SourceQualityMapFromJson(Map<String, dynamic> json) =>
|
||||
SourceQualityMap(
|
||||
high: json['high'] as String,
|
||||
medium: json['medium'] as String,
|
||||
low: json['low'] as String,
|
||||
@ -19,18 +20,16 @@ Map<String, dynamic> _$SourceQualityMapToJson(SourceQualityMap instance) =>
|
||||
'low': instance.low,
|
||||
};
|
||||
|
||||
SourceMap _$SourceMapFromJson(Map json) => SourceMap(
|
||||
SourceMap _$SourceMapFromJson(Map<String, dynamic> json) => SourceMap(
|
||||
weba: json['weba'] == null
|
||||
? null
|
||||
: SourceQualityMap.fromJson(
|
||||
Map<String, dynamic>.from(json['weba'] as Map)),
|
||||
: SourceQualityMap.fromJson(json['weba'] as Map<String, dynamic>),
|
||||
m4a: json['m4a'] == null
|
||||
? null
|
||||
: SourceQualityMap.fromJson(
|
||||
Map<String, dynamic>.from(json['m4a'] as Map)),
|
||||
: SourceQualityMap.fromJson(json['m4a'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SourceMapToJson(SourceMap instance) => <String, dynamic>{
|
||||
'weba': instance.weba?.toJson(),
|
||||
'm4a': instance.m4a?.toJson(),
|
||||
'weba': instance.weba,
|
||||
'm4a': instance.m4a,
|
||||
};
|
||||
|
89
lib/services/wm_tools.dart
Executable file
89
lib/services/wm_tools.dart
Executable file
@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rhythm_box/platform.dart';
|
||||
import 'package:rhythm_box/services/kv_store/kv_store.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class WindowSize {
|
||||
final double height;
|
||||
final double width;
|
||||
final bool maximized;
|
||||
|
||||
WindowSize({
|
||||
required this.height,
|
||||
required this.width,
|
||||
required this.maximized,
|
||||
});
|
||||
|
||||
factory WindowSize.fromJson(Map<String, dynamic> json) => WindowSize(
|
||||
height: json['height'],
|
||||
width: json['width'],
|
||||
maximized: json['maximized'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'height': height,
|
||||
'width': width,
|
||||
'maximized': maximized,
|
||||
};
|
||||
}
|
||||
|
||||
class WindowManagerTools with WidgetsBindingObserver {
|
||||
static WindowManagerTools? _instance;
|
||||
static WindowManagerTools get instance => _instance!;
|
||||
|
||||
WindowManagerTools._();
|
||||
|
||||
static Future<void> initialize() async {
|
||||
await windowManager.ensureInitialized();
|
||||
_instance = WindowManagerTools._();
|
||||
WidgetsBinding.instance.addObserver(instance);
|
||||
|
||||
await windowManager.waitUntilReadyToShow(
|
||||
const WindowOptions(
|
||||
title: 'RhythmBox',
|
||||
backgroundColor: Colors.transparent,
|
||||
minimumSize: Size(300, 700),
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
center: true,
|
||||
),
|
||||
() async {
|
||||
final savedSize = KVStoreService.windowSize;
|
||||
await windowManager.setResizable(true);
|
||||
if (savedSize?.maximized == true &&
|
||||
!(await windowManager.isMaximized())) {
|
||||
await windowManager.maximize();
|
||||
} else if (savedSize != null) {
|
||||
await windowManager.setSize(Size(savedSize.width, savedSize.height));
|
||||
}
|
||||
|
||||
await windowManager.focus();
|
||||
await windowManager.show();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Size? _prevSize;
|
||||
|
||||
@override
|
||||
void didChangeMetrics() async {
|
||||
super.didChangeMetrics();
|
||||
if (PlatformInfo.isMobile) return;
|
||||
final size = await windowManager.getSize();
|
||||
final windowSameDimension =
|
||||
_prevSize?.width == size.width && _prevSize?.height == size.height;
|
||||
|
||||
if (windowSameDimension || _prevSize == null) {
|
||||
_prevSize = size;
|
||||
return;
|
||||
}
|
||||
final isMaximized = await windowManager.isMaximized();
|
||||
await KVStoreService.setWindowSize(
|
||||
WindowSize(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
maximized: isMaximized,
|
||||
),
|
||||
);
|
||||
_prevSize = size;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user