From 3fb003ea60d90a8f8da7eb177bc747d4eb219199 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Jun 2024 00:29:09 +0600 Subject: [PATCH 01/24] refactor(preferences): use Drift sql db for preferences --- .../sections/body/track_view_options.dart | 2 +- .../configurators/use_close_behavior.dart | 3 +- lib/models/database/database.dart | 54 + lib/models/database/database.g.dart | 1707 +++++++++++++++++ lib/models/database/tables/preferences.dart | 125 ++ lib/models/database/typeconverters/color.dart | 29 + .../database/typeconverters/locale.dart | 19 + .../database/typeconverters/string_list.dart | 15 + lib/modules/player/sibling_tracks_sheet.dart | 3 +- lib/modules/root/bottom_player.dart | 3 +- lib/modules/root/sidebar.dart | 3 +- lib/modules/root/spotube_navigation_bar.dart | 3 +- .../getting_started/sections/playback.dart | 2 +- .../getting_started/sections/region.dart | 4 +- .../playlist_generate/playlist_generate.dart | 2 +- lib/pages/settings/sections/appearance.dart | 2 +- lib/pages/settings/sections/desktop.dart | 3 +- .../settings/sections/language_region.dart | 2 +- lib/pages/settings/sections/playback.dart | 3 +- lib/provider/database/database.dart | 4 + .../proxy_playlist_provider.dart | 2 +- .../proxy_playlist/skip_segments.dart | 3 +- lib/provider/server/routes/playback.dart | 1 - lib/provider/spotify/album/releases.dart | 4 +- lib/provider/spotify/artist/albums.dart | 4 +- lib/provider/spotify/artist/top_tracks.dart | 3 +- lib/provider/spotify/category/categories.dart | 3 +- lib/provider/spotify/category/playlists.dart | 4 +- lib/provider/spotify/playlist/generate.dart | 2 +- lib/provider/spotify/search/search.dart | 4 +- lib/provider/spotify/views/home.dart | 2 +- lib/provider/spotify/views/home_section.dart | 2 +- lib/provider/spotify/views/view.dart | 2 +- .../user_preferences_provider.dart | 189 +- .../user_preferences_state.dart | 142 -- .../user_preferences_state.freezed.dart | 751 -------- .../user_preferences_state.g.dart | 388 ---- .../sourced_track/models/video_info.dart | 3 +- lib/services/sourced_track/sourced_track.dart | 3 +- lib/services/sourced_track/sources/piped.dart | 3 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 21 + pubspec.lock | 48 + pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 48 files changed, 2187 insertions(+), 1400 deletions(-) create mode 100644 lib/models/database/database.dart create mode 100644 lib/models/database/database.g.dart create mode 100644 lib/models/database/tables/preferences.dart create mode 100644 lib/models/database/typeconverters/color.dart create mode 100644 lib/models/database/typeconverters/locale.dart create mode 100644 lib/models/database/typeconverters/string_list.dart create mode 100644 lib/provider/database/database.dart delete mode 100644 lib/provider/user_preferences/user_preferences_state.dart delete mode 100644 lib/provider/user_preferences/user_preferences_state.freezed.dart delete mode 100644 lib/provider/user_preferences/user_preferences_state.g.dart diff --git a/lib/components/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart index f004b10a..1accba34 100644 --- a/lib/components/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -8,11 +8,11 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/components/tracks_view/track_view_provider.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; class TrackViewBodyOptions extends HookConsumerWidget { const TrackViewBodyOptions({super.key}); diff --git a/lib/hooks/configurators/use_close_behavior.dart b/lib/hooks/configurators/use_close_behavior.dart index 3df6a528..2bdc65ef 100644 --- a/lib/hooks/configurators/use_close_behavior.dart +++ b/lib/hooks/configurators/use_close_behavior.dart @@ -2,8 +2,9 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/hooks/configurators/use_window_listener.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:local_notifier/local_notifier.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart new file mode 100644 index 00000000..7d8fe088 --- /dev/null +++ b/lib/models/database/database.dart @@ -0,0 +1,54 @@ +library database; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:flutter/material.dart' hide Table; +import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +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/preferences.dart'; +part 'typeconverters/color.dart'; +part 'typeconverters/locale.dart'; +part 'typeconverters/string_list.dart'; + +@DriftDatabase(tables: [PreferencesTable]) +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 getApplicationDocumentsDirectory(); + 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); + }); +} diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart new file mode 100644 index 00000000..1516b266 --- /dev/null +++ b/lib/models/database/database.g.dart @@ -0,0 +1,1707 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $PreferencesTableTable extends PreferencesTable + with TableInfo<$PreferencesTableTable, PreferencesTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PreferencesTableTable(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 _audioQualityMeta = + const VerificationMeta('audioQuality'); + @override + late final GeneratedColumnWithTypeConverter + audioQuality = GeneratedColumn( + 'audio_quality', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceQualities.high.name)) + .withConverter( + $PreferencesTableTable.$converteraudioQuality); + static const VerificationMeta _albumColorSyncMeta = + const VerificationMeta('albumColorSync'); + @override + late final GeneratedColumn albumColorSync = GeneratedColumn( + 'album_color_sync', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("album_color_sync" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _amoledDarkThemeMeta = + const VerificationMeta('amoledDarkTheme'); + @override + late final GeneratedColumn amoledDarkTheme = GeneratedColumn( + 'amoled_dark_theme', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("amoled_dark_theme" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _checkUpdateMeta = + const VerificationMeta('checkUpdate'); + @override + late final GeneratedColumn checkUpdate = GeneratedColumn( + 'check_update', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("check_update" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _normalizeAudioMeta = + const VerificationMeta('normalizeAudio'); + @override + late final GeneratedColumn normalizeAudio = GeneratedColumn( + 'normalize_audio', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("normalize_audio" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _showSystemTrayIconMeta = + const VerificationMeta('showSystemTrayIcon'); + @override + late final GeneratedColumn showSystemTrayIcon = GeneratedColumn( + 'show_system_tray_icon', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_system_tray_icon" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _systemTitleBarMeta = + const VerificationMeta('systemTitleBar'); + @override + late final GeneratedColumn systemTitleBar = GeneratedColumn( + 'system_title_bar', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("system_title_bar" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _skipNonMusicMeta = + const VerificationMeta('skipNonMusic'); + @override + late final GeneratedColumn skipNonMusic = GeneratedColumn( + 'skip_non_music', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("skip_non_music" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _closeBehaviorMeta = + const VerificationMeta('closeBehavior'); + @override + late final GeneratedColumnWithTypeConverter + closeBehavior = GeneratedColumn( + 'close_behavior', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(CloseBehavior.close.name)) + .withConverter( + $PreferencesTableTable.$convertercloseBehavior); + static const VerificationMeta _accentColorSchemeMeta = + const VerificationMeta('accentColorScheme'); + @override + late final GeneratedColumnWithTypeConverter + accentColorScheme = GeneratedColumn( + 'accent_color_scheme', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("Blue:0xFF2196F3")) + .withConverter( + $PreferencesTableTable.$converteraccentColorScheme); + static const VerificationMeta _layoutModeMeta = + const VerificationMeta('layoutMode'); + @override + late final GeneratedColumnWithTypeConverter layoutMode = + GeneratedColumn('layout_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(LayoutMode.adaptive.name)) + .withConverter( + $PreferencesTableTable.$converterlayoutMode); + static const VerificationMeta _localeMeta = const VerificationMeta('locale'); + @override + late final GeneratedColumnWithTypeConverter locale = + GeneratedColumn('locale', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant( + '{"languageCode":"system","countryCode":"system"}')) + .withConverter($PreferencesTableTable.$converterlocale); + static const VerificationMeta _marketMeta = const VerificationMeta('market'); + @override + late final GeneratedColumnWithTypeConverter market = + GeneratedColumn('market', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(Market.US.name)) + .withConverter($PreferencesTableTable.$convertermarket); + static const VerificationMeta _searchModeMeta = + const VerificationMeta('searchMode'); + @override + late final GeneratedColumnWithTypeConverter searchMode = + GeneratedColumn('search_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SearchMode.youtube.name)) + .withConverter( + $PreferencesTableTable.$convertersearchMode); + static const VerificationMeta _downloadLocationMeta = + const VerificationMeta('downloadLocation'); + @override + late final GeneratedColumn downloadLocation = GeneratedColumn( + 'download_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")); + static const VerificationMeta _localLibraryLocationMeta = + const VerificationMeta('localLibraryLocation'); + @override + late final GeneratedColumnWithTypeConverter, String> + localLibraryLocation = GeneratedColumn( + 'local_library_location', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("")) + .withConverter>( + $PreferencesTableTable.$converterlocalLibraryLocation); + static const VerificationMeta _pipedInstanceMeta = + const VerificationMeta('pipedInstance'); + @override + late final GeneratedColumn pipedInstance = GeneratedColumn( + 'piped_instance', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant("https://pipedapi.kavin.rocks")); + static const VerificationMeta _themeModeMeta = + const VerificationMeta('themeMode'); + @override + late final GeneratedColumnWithTypeConverter themeMode = + GeneratedColumn('theme_mode', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(ThemeMode.system.name)) + .withConverter($PreferencesTableTable.$converterthemeMode); + static const VerificationMeta _audioSourceMeta = + const VerificationMeta('audioSource'); + @override + late final GeneratedColumnWithTypeConverter audioSource = + GeneratedColumn('audio_source', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(AudioSource.youtube.name)) + .withConverter( + $PreferencesTableTable.$converteraudioSource); + static const VerificationMeta _streamMusicCodecMeta = + const VerificationMeta('streamMusicCodec'); + @override + late final GeneratedColumnWithTypeConverter + streamMusicCodec = GeneratedColumn( + 'stream_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.weba.name)) + .withConverter( + $PreferencesTableTable.$converterstreamMusicCodec); + static const VerificationMeta _downloadMusicCodecMeta = + const VerificationMeta('downloadMusicCodec'); + @override + late final GeneratedColumnWithTypeConverter + downloadMusicCodec = GeneratedColumn( + 'download_music_codec', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceCodecs.m4a.name)) + .withConverter( + $PreferencesTableTable.$converterdownloadMusicCodec); + static const VerificationMeta _discordPresenceMeta = + const VerificationMeta('discordPresence'); + @override + late final GeneratedColumn discordPresence = GeneratedColumn( + 'discord_presence', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("discord_presence" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _endlessPlaybackMeta = + const VerificationMeta('endlessPlayback'); + @override + late final GeneratedColumn endlessPlayback = GeneratedColumn( + 'endless_playback', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("endless_playback" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _enableConnectMeta = + const VerificationMeta('enableConnect'); + @override + late final GeneratedColumn enableConnect = GeneratedColumn( + 'enable_connect', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enable_connect" IN (0, 1))'), + defaultValue: const Constant(false)); + @override + List get $columns => [ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'preferences_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)); + } + context.handle(_audioQualityMeta, const VerificationResult.success()); + if (data.containsKey('album_color_sync')) { + context.handle( + _albumColorSyncMeta, + albumColorSync.isAcceptableOrUnknown( + data['album_color_sync']!, _albumColorSyncMeta)); + } + if (data.containsKey('amoled_dark_theme')) { + context.handle( + _amoledDarkThemeMeta, + amoledDarkTheme.isAcceptableOrUnknown( + data['amoled_dark_theme']!, _amoledDarkThemeMeta)); + } + if (data.containsKey('check_update')) { + context.handle( + _checkUpdateMeta, + checkUpdate.isAcceptableOrUnknown( + data['check_update']!, _checkUpdateMeta)); + } + if (data.containsKey('normalize_audio')) { + context.handle( + _normalizeAudioMeta, + normalizeAudio.isAcceptableOrUnknown( + data['normalize_audio']!, _normalizeAudioMeta)); + } + if (data.containsKey('show_system_tray_icon')) { + context.handle( + _showSystemTrayIconMeta, + showSystemTrayIcon.isAcceptableOrUnknown( + data['show_system_tray_icon']!, _showSystemTrayIconMeta)); + } + if (data.containsKey('system_title_bar')) { + context.handle( + _systemTitleBarMeta, + systemTitleBar.isAcceptableOrUnknown( + data['system_title_bar']!, _systemTitleBarMeta)); + } + if (data.containsKey('skip_non_music')) { + context.handle( + _skipNonMusicMeta, + skipNonMusic.isAcceptableOrUnknown( + data['skip_non_music']!, _skipNonMusicMeta)); + } + context.handle(_closeBehaviorMeta, const VerificationResult.success()); + context.handle(_accentColorSchemeMeta, const VerificationResult.success()); + context.handle(_layoutModeMeta, const VerificationResult.success()); + context.handle(_localeMeta, const VerificationResult.success()); + context.handle(_marketMeta, const VerificationResult.success()); + context.handle(_searchModeMeta, const VerificationResult.success()); + if (data.containsKey('download_location')) { + context.handle( + _downloadLocationMeta, + downloadLocation.isAcceptableOrUnknown( + data['download_location']!, _downloadLocationMeta)); + } + context.handle( + _localLibraryLocationMeta, const VerificationResult.success()); + if (data.containsKey('piped_instance')) { + context.handle( + _pipedInstanceMeta, + pipedInstance.isAcceptableOrUnknown( + data['piped_instance']!, _pipedInstanceMeta)); + } + context.handle(_themeModeMeta, const VerificationResult.success()); + context.handle(_audioSourceMeta, const VerificationResult.success()); + context.handle(_streamMusicCodecMeta, const VerificationResult.success()); + context.handle(_downloadMusicCodecMeta, const VerificationResult.success()); + if (data.containsKey('discord_presence')) { + context.handle( + _discordPresenceMeta, + discordPresence.isAcceptableOrUnknown( + data['discord_presence']!, _discordPresenceMeta)); + } + if (data.containsKey('endless_playback')) { + context.handle( + _endlessPlaybackMeta, + endlessPlayback.isAcceptableOrUnknown( + data['endless_playback']!, _endlessPlaybackMeta)); + } + if (data.containsKey('enable_connect')) { + context.handle( + _enableConnectMeta, + enableConnect.isAcceptableOrUnknown( + data['enable_connect']!, _enableConnectMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PreferencesTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PreferencesTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + audioQuality: $PreferencesTableTable.$converteraudioQuality.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}audio_quality'])!), + albumColorSync: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}album_color_sync'])!, + amoledDarkTheme: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}amoled_dark_theme'])!, + checkUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}check_update'])!, + normalizeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}normalize_audio'])!, + showSystemTrayIcon: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}show_system_tray_icon'])!, + systemTitleBar: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}system_title_bar'])!, + skipNonMusic: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}skip_non_music'])!, + closeBehavior: $PreferencesTableTable.$convertercloseBehavior.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}close_behavior'])!), + accentColorScheme: $PreferencesTableTable.$converteraccentColorScheme + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}accent_color_scheme'])!), + layoutMode: $PreferencesTableTable.$converterlayoutMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}layout_mode'])!), + locale: $PreferencesTableTable.$converterlocale.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}locale'])!), + market: $PreferencesTableTable.$convertermarket.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}market'])!), + searchMode: $PreferencesTableTable.$convertersearchMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}search_mode'])!), + downloadLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_location'])!, + localLibraryLocation: $PreferencesTableTable + .$converterlocalLibraryLocation + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}local_library_location'])!), + pipedInstance: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}piped_instance'])!, + themeMode: $PreferencesTableTable.$converterthemeMode.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}theme_mode'])!), + audioSource: $PreferencesTableTable.$converteraudioSource.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}audio_source'])!), + streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}stream_music_codec'])!), + downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}download_music_codec'])!), + discordPresence: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}discord_presence'])!, + endlessPlayback: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!, + enableConnect: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}enable_connect'])!, + ); + } + + @override + $PreferencesTableTable createAlias(String alias) { + return $PreferencesTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converteraudioQuality = + const EnumNameConverter(SourceQualities.values); + static JsonTypeConverter2 + $convertercloseBehavior = + const EnumNameConverter(CloseBehavior.values); + static TypeConverter $converteraccentColorScheme = + const SpotubeColorConverter(); + static JsonTypeConverter2 $converterlayoutMode = + const EnumNameConverter(LayoutMode.values); + static TypeConverter $converterlocale = + const LocaleConverter(); + static JsonTypeConverter2 $convertermarket = + const EnumNameConverter(Market.values); + static JsonTypeConverter2 $convertersearchMode = + const EnumNameConverter(SearchMode.values); + static TypeConverter, String> $converterlocalLibraryLocation = + const StringListConverter(); + static JsonTypeConverter2 $converterthemeMode = + const EnumNameConverter(ThemeMode.values); + static JsonTypeConverter2 $converteraudioSource = + const EnumNameConverter(AudioSource.values); + static JsonTypeConverter2 + $converterstreamMusicCodec = + const EnumNameConverter(SourceCodecs.values); + static JsonTypeConverter2 + $converterdownloadMusicCodec = + const EnumNameConverter(SourceCodecs.values); +} + +class PreferencesTableData extends DataClass + implements Insertable { + final int id; + final SourceQualities audioQuality; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool systemTitleBar; + final bool skipNonMusic; + final CloseBehavior closeBehavior; + final SpotubeColor accentColorScheme; + final LayoutMode layoutMode; + final Locale locale; + final Market market; + final SearchMode searchMode; + final String downloadLocation; + final List localLibraryLocation; + final String pipedInstance; + final ThemeMode themeMode; + final AudioSource audioSource; + final SourceCodecs streamMusicCodec; + final SourceCodecs downloadMusicCodec; + final bool discordPresence; + final bool endlessPlayback; + final bool enableConnect; + const PreferencesTableData( + {required this.id, + required this.audioQuality, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.systemTitleBar, + required this.skipNonMusic, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.market, + required this.searchMode, + required this.downloadLocation, + required this.localLibraryLocation, + required this.pipedInstance, + required this.themeMode, + required this.audioSource, + required this.streamMusicCodec, + required this.downloadMusicCodec, + required this.discordPresence, + required this.endlessPlayback, + required this.enableConnect}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + { + map['audio_quality'] = Variable( + $PreferencesTableTable.$converteraudioQuality.toSql(audioQuality)); + } + map['album_color_sync'] = Variable(albumColorSync); + map['amoled_dark_theme'] = Variable(amoledDarkTheme); + map['check_update'] = Variable(checkUpdate); + map['normalize_audio'] = Variable(normalizeAudio); + map['show_system_tray_icon'] = Variable(showSystemTrayIcon); + map['system_title_bar'] = Variable(systemTitleBar); + map['skip_non_music'] = Variable(skipNonMusic); + { + map['close_behavior'] = Variable( + $PreferencesTableTable.$convertercloseBehavior.toSql(closeBehavior)); + } + { + map['accent_color_scheme'] = Variable($PreferencesTableTable + .$converteraccentColorScheme + .toSql(accentColorScheme)); + } + { + map['layout_mode'] = Variable( + $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode)); + } + { + map['locale'] = Variable( + $PreferencesTableTable.$converterlocale.toSql(locale)); + } + { + map['market'] = Variable( + $PreferencesTableTable.$convertermarket.toSql(market)); + } + { + map['search_mode'] = Variable( + $PreferencesTableTable.$convertersearchMode.toSql(searchMode)); + } + map['download_location'] = Variable(downloadLocation); + { + map['local_library_location'] = Variable($PreferencesTableTable + .$converterlocalLibraryLocation + .toSql(localLibraryLocation)); + } + map['piped_instance'] = Variable(pipedInstance); + { + map['theme_mode'] = Variable( + $PreferencesTableTable.$converterthemeMode.toSql(themeMode)); + } + { + map['audio_source'] = Variable( + $PreferencesTableTable.$converteraudioSource.toSql(audioSource)); + } + { + map['stream_music_codec'] = Variable($PreferencesTableTable + .$converterstreamMusicCodec + .toSql(streamMusicCodec)); + } + { + map['download_music_codec'] = Variable($PreferencesTableTable + .$converterdownloadMusicCodec + .toSql(downloadMusicCodec)); + } + map['discord_presence'] = Variable(discordPresence); + map['endless_playback'] = Variable(endlessPlayback); + map['enable_connect'] = Variable(enableConnect); + return map; + } + + PreferencesTableCompanion toCompanion(bool nullToAbsent) { + return PreferencesTableCompanion( + id: Value(id), + audioQuality: Value(audioQuality), + albumColorSync: Value(albumColorSync), + amoledDarkTheme: Value(amoledDarkTheme), + checkUpdate: Value(checkUpdate), + normalizeAudio: Value(normalizeAudio), + showSystemTrayIcon: Value(showSystemTrayIcon), + systemTitleBar: Value(systemTitleBar), + skipNonMusic: Value(skipNonMusic), + closeBehavior: Value(closeBehavior), + accentColorScheme: Value(accentColorScheme), + layoutMode: Value(layoutMode), + locale: Value(locale), + market: Value(market), + searchMode: Value(searchMode), + downloadLocation: Value(downloadLocation), + localLibraryLocation: Value(localLibraryLocation), + pipedInstance: Value(pipedInstance), + themeMode: Value(themeMode), + audioSource: Value(audioSource), + streamMusicCodec: Value(streamMusicCodec), + downloadMusicCodec: Value(downloadMusicCodec), + discordPresence: Value(discordPresence), + endlessPlayback: Value(endlessPlayback), + enableConnect: Value(enableConnect), + ); + } + + factory PreferencesTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PreferencesTableData( + id: serializer.fromJson(json['id']), + audioQuality: $PreferencesTableTable.$converteraudioQuality + .fromJson(serializer.fromJson(json['audioQuality'])), + albumColorSync: serializer.fromJson(json['albumColorSync']), + amoledDarkTheme: serializer.fromJson(json['amoledDarkTheme']), + checkUpdate: serializer.fromJson(json['checkUpdate']), + normalizeAudio: serializer.fromJson(json['normalizeAudio']), + showSystemTrayIcon: serializer.fromJson(json['showSystemTrayIcon']), + systemTitleBar: serializer.fromJson(json['systemTitleBar']), + skipNonMusic: serializer.fromJson(json['skipNonMusic']), + closeBehavior: $PreferencesTableTable.$convertercloseBehavior + .fromJson(serializer.fromJson(json['closeBehavior'])), + accentColorScheme: + serializer.fromJson(json['accentColorScheme']), + layoutMode: $PreferencesTableTable.$converterlayoutMode + .fromJson(serializer.fromJson(json['layoutMode'])), + locale: serializer.fromJson(json['locale']), + market: $PreferencesTableTable.$convertermarket + .fromJson(serializer.fromJson(json['market'])), + searchMode: $PreferencesTableTable.$convertersearchMode + .fromJson(serializer.fromJson(json['searchMode'])), + downloadLocation: serializer.fromJson(json['downloadLocation']), + localLibraryLocation: + serializer.fromJson>(json['localLibraryLocation']), + pipedInstance: serializer.fromJson(json['pipedInstance']), + themeMode: $PreferencesTableTable.$converterthemeMode + .fromJson(serializer.fromJson(json['themeMode'])), + audioSource: $PreferencesTableTable.$converteraudioSource + .fromJson(serializer.fromJson(json['audioSource'])), + streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec + .fromJson(serializer.fromJson(json['streamMusicCodec'])), + downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec + .fromJson(serializer.fromJson(json['downloadMusicCodec'])), + discordPresence: serializer.fromJson(json['discordPresence']), + endlessPlayback: serializer.fromJson(json['endlessPlayback']), + enableConnect: serializer.fromJson(json['enableConnect']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'audioQuality': serializer.toJson( + $PreferencesTableTable.$converteraudioQuality.toJson(audioQuality)), + 'albumColorSync': serializer.toJson(albumColorSync), + 'amoledDarkTheme': serializer.toJson(amoledDarkTheme), + 'checkUpdate': serializer.toJson(checkUpdate), + 'normalizeAudio': serializer.toJson(normalizeAudio), + 'showSystemTrayIcon': serializer.toJson(showSystemTrayIcon), + 'systemTitleBar': serializer.toJson(systemTitleBar), + 'skipNonMusic': serializer.toJson(skipNonMusic), + 'closeBehavior': serializer.toJson( + $PreferencesTableTable.$convertercloseBehavior.toJson(closeBehavior)), + 'accentColorScheme': serializer.toJson(accentColorScheme), + 'layoutMode': serializer.toJson( + $PreferencesTableTable.$converterlayoutMode.toJson(layoutMode)), + 'locale': serializer.toJson(locale), + 'market': serializer.toJson( + $PreferencesTableTable.$convertermarket.toJson(market)), + 'searchMode': serializer.toJson( + $PreferencesTableTable.$convertersearchMode.toJson(searchMode)), + 'downloadLocation': serializer.toJson(downloadLocation), + 'localLibraryLocation': + serializer.toJson>(localLibraryLocation), + 'pipedInstance': serializer.toJson(pipedInstance), + 'themeMode': serializer.toJson( + $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), + 'audioSource': serializer.toJson( + $PreferencesTableTable.$converteraudioSource.toJson(audioSource)), + 'streamMusicCodec': serializer.toJson($PreferencesTableTable + .$converterstreamMusicCodec + .toJson(streamMusicCodec)), + 'downloadMusicCodec': serializer.toJson($PreferencesTableTable + .$converterdownloadMusicCodec + .toJson(downloadMusicCodec)), + 'discordPresence': serializer.toJson(discordPresence), + 'endlessPlayback': serializer.toJson(endlessPlayback), + 'enableConnect': serializer.toJson(enableConnect), + }; + } + + PreferencesTableData copyWith( + {int? id, + SourceQualities? audioQuality, + bool? albumColorSync, + bool? amoledDarkTheme, + bool? checkUpdate, + bool? normalizeAudio, + bool? showSystemTrayIcon, + bool? systemTitleBar, + bool? skipNonMusic, + CloseBehavior? closeBehavior, + SpotubeColor? accentColorScheme, + LayoutMode? layoutMode, + Locale? locale, + Market? market, + SearchMode? searchMode, + String? downloadLocation, + List? localLibraryLocation, + String? pipedInstance, + ThemeMode? themeMode, + AudioSource? audioSource, + SourceCodecs? streamMusicCodec, + SourceCodecs? downloadMusicCodec, + bool? discordPresence, + bool? endlessPlayback, + bool? enableConnect}) => + PreferencesTableData( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + ); + @override + String toString() { + return (StringBuffer('PreferencesTableData(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + systemTitleBar, + skipNonMusic, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + market, + searchMode, + downloadLocation, + localLibraryLocation, + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PreferencesTableData && + other.id == this.id && + other.audioQuality == this.audioQuality && + other.albumColorSync == this.albumColorSync && + other.amoledDarkTheme == this.amoledDarkTheme && + other.checkUpdate == this.checkUpdate && + other.normalizeAudio == this.normalizeAudio && + other.showSystemTrayIcon == this.showSystemTrayIcon && + other.systemTitleBar == this.systemTitleBar && + other.skipNonMusic == this.skipNonMusic && + other.closeBehavior == this.closeBehavior && + other.accentColorScheme == this.accentColorScheme && + other.layoutMode == this.layoutMode && + other.locale == this.locale && + other.market == this.market && + other.searchMode == this.searchMode && + other.downloadLocation == this.downloadLocation && + other.localLibraryLocation == this.localLibraryLocation && + other.pipedInstance == this.pipedInstance && + other.themeMode == this.themeMode && + other.audioSource == this.audioSource && + other.streamMusicCodec == this.streamMusicCodec && + other.downloadMusicCodec == this.downloadMusicCodec && + other.discordPresence == this.discordPresence && + other.endlessPlayback == this.endlessPlayback && + other.enableConnect == this.enableConnect); +} + +class PreferencesTableCompanion extends UpdateCompanion { + final Value id; + final Value audioQuality; + final Value albumColorSync; + final Value amoledDarkTheme; + final Value checkUpdate; + final Value normalizeAudio; + final Value showSystemTrayIcon; + final Value systemTitleBar; + final Value skipNonMusic; + final Value closeBehavior; + final Value accentColorScheme; + final Value layoutMode; + final Value locale; + final Value market; + final Value searchMode; + final Value downloadLocation; + final Value> localLibraryLocation; + final Value pipedInstance; + final Value themeMode; + final Value audioSource; + final Value streamMusicCodec; + final Value downloadMusicCodec; + final Value discordPresence; + final Value endlessPlayback; + final Value enableConnect; + const PreferencesTableCompanion({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + }); + PreferencesTableCompanion.insert({ + this.id = const Value.absent(), + this.audioQuality = const Value.absent(), + this.albumColorSync = const Value.absent(), + this.amoledDarkTheme = const Value.absent(), + this.checkUpdate = const Value.absent(), + this.normalizeAudio = const Value.absent(), + this.showSystemTrayIcon = const Value.absent(), + this.systemTitleBar = const Value.absent(), + this.skipNonMusic = const Value.absent(), + this.closeBehavior = const Value.absent(), + this.accentColorScheme = const Value.absent(), + this.layoutMode = const Value.absent(), + this.locale = const Value.absent(), + this.market = const Value.absent(), + this.searchMode = const Value.absent(), + this.downloadLocation = const Value.absent(), + this.localLibraryLocation = const Value.absent(), + this.pipedInstance = const Value.absent(), + this.themeMode = const Value.absent(), + this.audioSource = const Value.absent(), + this.streamMusicCodec = const Value.absent(), + this.downloadMusicCodec = const Value.absent(), + this.discordPresence = const Value.absent(), + this.endlessPlayback = const Value.absent(), + this.enableConnect = const Value.absent(), + }); + static Insertable custom({ + Expression? id, + Expression? audioQuality, + Expression? albumColorSync, + Expression? amoledDarkTheme, + Expression? checkUpdate, + Expression? normalizeAudio, + Expression? showSystemTrayIcon, + Expression? systemTitleBar, + Expression? skipNonMusic, + Expression? closeBehavior, + Expression? accentColorScheme, + Expression? layoutMode, + Expression? locale, + Expression? market, + Expression? searchMode, + Expression? downloadLocation, + Expression? localLibraryLocation, + Expression? pipedInstance, + Expression? themeMode, + Expression? audioSource, + Expression? streamMusicCodec, + Expression? downloadMusicCodec, + Expression? discordPresence, + Expression? endlessPlayback, + Expression? enableConnect, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (audioQuality != null) 'audio_quality': audioQuality, + if (albumColorSync != null) 'album_color_sync': albumColorSync, + if (amoledDarkTheme != null) 'amoled_dark_theme': amoledDarkTheme, + if (checkUpdate != null) 'check_update': checkUpdate, + if (normalizeAudio != null) 'normalize_audio': normalizeAudio, + if (showSystemTrayIcon != null) + 'show_system_tray_icon': showSystemTrayIcon, + if (systemTitleBar != null) 'system_title_bar': systemTitleBar, + if (skipNonMusic != null) 'skip_non_music': skipNonMusic, + if (closeBehavior != null) 'close_behavior': closeBehavior, + if (accentColorScheme != null) 'accent_color_scheme': accentColorScheme, + if (layoutMode != null) 'layout_mode': layoutMode, + if (locale != null) 'locale': locale, + if (market != null) 'market': market, + if (searchMode != null) 'search_mode': searchMode, + if (downloadLocation != null) 'download_location': downloadLocation, + if (localLibraryLocation != null) + 'local_library_location': localLibraryLocation, + if (pipedInstance != null) 'piped_instance': pipedInstance, + if (themeMode != null) 'theme_mode': themeMode, + if (audioSource != null) 'audio_source': audioSource, + if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, + if (downloadMusicCodec != null) + 'download_music_codec': downloadMusicCodec, + if (discordPresence != null) 'discord_presence': discordPresence, + if (endlessPlayback != null) 'endless_playback': endlessPlayback, + if (enableConnect != null) 'enable_connect': enableConnect, + }); + } + + PreferencesTableCompanion copyWith( + {Value? id, + Value? audioQuality, + Value? albumColorSync, + Value? amoledDarkTheme, + Value? checkUpdate, + Value? normalizeAudio, + Value? showSystemTrayIcon, + Value? systemTitleBar, + Value? skipNonMusic, + Value? closeBehavior, + Value? accentColorScheme, + Value? layoutMode, + Value? locale, + Value? market, + Value? searchMode, + Value? downloadLocation, + Value>? localLibraryLocation, + Value? pipedInstance, + Value? themeMode, + Value? audioSource, + Value? streamMusicCodec, + Value? downloadMusicCodec, + Value? discordPresence, + Value? endlessPlayback, + Value? enableConnect}) { + return PreferencesTableCompanion( + id: id ?? this.id, + audioQuality: audioQuality ?? this.audioQuality, + albumColorSync: albumColorSync ?? this.albumColorSync, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + checkUpdate: checkUpdate ?? this.checkUpdate, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, + skipNonMusic: skipNonMusic ?? this.skipNonMusic, + closeBehavior: closeBehavior ?? this.closeBehavior, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + layoutMode: layoutMode ?? this.layoutMode, + locale: locale ?? this.locale, + market: market ?? this.market, + searchMode: searchMode ?? this.searchMode, + downloadLocation: downloadLocation ?? this.downloadLocation, + localLibraryLocation: localLibraryLocation ?? this.localLibraryLocation, + pipedInstance: pipedInstance ?? this.pipedInstance, + themeMode: themeMode ?? this.themeMode, + audioSource: audioSource ?? this.audioSource, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + discordPresence: discordPresence ?? this.discordPresence, + endlessPlayback: endlessPlayback ?? this.endlessPlayback, + enableConnect: enableConnect ?? this.enableConnect, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (audioQuality.present) { + map['audio_quality'] = Variable($PreferencesTableTable + .$converteraudioQuality + .toSql(audioQuality.value)); + } + if (albumColorSync.present) { + map['album_color_sync'] = Variable(albumColorSync.value); + } + if (amoledDarkTheme.present) { + map['amoled_dark_theme'] = Variable(amoledDarkTheme.value); + } + if (checkUpdate.present) { + map['check_update'] = Variable(checkUpdate.value); + } + if (normalizeAudio.present) { + map['normalize_audio'] = Variable(normalizeAudio.value); + } + if (showSystemTrayIcon.present) { + map['show_system_tray_icon'] = Variable(showSystemTrayIcon.value); + } + if (systemTitleBar.present) { + map['system_title_bar'] = Variable(systemTitleBar.value); + } + if (skipNonMusic.present) { + map['skip_non_music'] = Variable(skipNonMusic.value); + } + if (closeBehavior.present) { + map['close_behavior'] = Variable($PreferencesTableTable + .$convertercloseBehavior + .toSql(closeBehavior.value)); + } + if (accentColorScheme.present) { + map['accent_color_scheme'] = Variable($PreferencesTableTable + .$converteraccentColorScheme + .toSql(accentColorScheme.value)); + } + if (layoutMode.present) { + map['layout_mode'] = Variable( + $PreferencesTableTable.$converterlayoutMode.toSql(layoutMode.value)); + } + if (locale.present) { + map['locale'] = Variable( + $PreferencesTableTable.$converterlocale.toSql(locale.value)); + } + if (market.present) { + map['market'] = Variable( + $PreferencesTableTable.$convertermarket.toSql(market.value)); + } + if (searchMode.present) { + map['search_mode'] = Variable( + $PreferencesTableTable.$convertersearchMode.toSql(searchMode.value)); + } + if (downloadLocation.present) { + map['download_location'] = Variable(downloadLocation.value); + } + if (localLibraryLocation.present) { + map['local_library_location'] = Variable($PreferencesTableTable + .$converterlocalLibraryLocation + .toSql(localLibraryLocation.value)); + } + if (pipedInstance.present) { + map['piped_instance'] = Variable(pipedInstance.value); + } + if (themeMode.present) { + map['theme_mode'] = Variable( + $PreferencesTableTable.$converterthemeMode.toSql(themeMode.value)); + } + if (audioSource.present) { + map['audio_source'] = Variable($PreferencesTableTable + .$converteraudioSource + .toSql(audioSource.value)); + } + if (streamMusicCodec.present) { + map['stream_music_codec'] = Variable($PreferencesTableTable + .$converterstreamMusicCodec + .toSql(streamMusicCodec.value)); + } + if (downloadMusicCodec.present) { + map['download_music_codec'] = Variable($PreferencesTableTable + .$converterdownloadMusicCodec + .toSql(downloadMusicCodec.value)); + } + if (discordPresence.present) { + map['discord_presence'] = Variable(discordPresence.value); + } + if (endlessPlayback.present) { + map['endless_playback'] = Variable(endlessPlayback.value); + } + if (enableConnect.present) { + map['enable_connect'] = Variable(enableConnect.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PreferencesTableCompanion(') + ..write('id: $id, ') + ..write('audioQuality: $audioQuality, ') + ..write('albumColorSync: $albumColorSync, ') + ..write('amoledDarkTheme: $amoledDarkTheme, ') + ..write('checkUpdate: $checkUpdate, ') + ..write('normalizeAudio: $normalizeAudio, ') + ..write('showSystemTrayIcon: $showSystemTrayIcon, ') + ..write('systemTitleBar: $systemTitleBar, ') + ..write('skipNonMusic: $skipNonMusic, ') + ..write('closeBehavior: $closeBehavior, ') + ..write('accentColorScheme: $accentColorScheme, ') + ..write('layoutMode: $layoutMode, ') + ..write('locale: $locale, ') + ..write('market: $market, ') + ..write('searchMode: $searchMode, ') + ..write('downloadLocation: $downloadLocation, ') + ..write('localLibraryLocation: $localLibraryLocation, ') + ..write('pipedInstance: $pipedInstance, ') + ..write('themeMode: $themeMode, ') + ..write('audioSource: $audioSource, ') + ..write('streamMusicCodec: $streamMusicCodec, ') + ..write('downloadMusicCodec: $downloadMusicCodec, ') + ..write('discordPresence: $discordPresence, ') + ..write('endlessPlayback: $endlessPlayback, ') + ..write('enableConnect: $enableConnect') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + _$AppDatabaseManager get managers => _$AppDatabaseManager(this); + late final $PreferencesTableTable preferencesTable = + $PreferencesTableTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [preferencesTable]; +} + +typedef $$PreferencesTableTableInsertCompanionBuilder + = PreferencesTableCompanion Function({ + Value id, + Value audioQuality, + Value albumColorSync, + Value amoledDarkTheme, + Value checkUpdate, + Value normalizeAudio, + Value showSystemTrayIcon, + Value systemTitleBar, + Value skipNonMusic, + Value closeBehavior, + Value accentColorScheme, + Value layoutMode, + Value locale, + Value market, + Value searchMode, + Value downloadLocation, + Value> localLibraryLocation, + Value pipedInstance, + Value themeMode, + Value audioSource, + Value streamMusicCodec, + Value downloadMusicCodec, + Value discordPresence, + Value endlessPlayback, + Value enableConnect, +}); +typedef $$PreferencesTableTableUpdateCompanionBuilder + = PreferencesTableCompanion Function({ + Value id, + Value audioQuality, + Value albumColorSync, + Value amoledDarkTheme, + Value checkUpdate, + Value normalizeAudio, + Value showSystemTrayIcon, + Value systemTitleBar, + Value skipNonMusic, + Value closeBehavior, + Value accentColorScheme, + Value layoutMode, + Value locale, + Value market, + Value searchMode, + Value downloadLocation, + Value> localLibraryLocation, + Value pipedInstance, + Value themeMode, + Value audioSource, + Value streamMusicCodec, + Value downloadMusicCodec, + Value discordPresence, + Value endlessPlayback, + Value enableConnect, +}); + +class $$PreferencesTableTableTableManager extends RootTableManager< + _$AppDatabase, + $PreferencesTableTable, + PreferencesTableData, + $$PreferencesTableTableFilterComposer, + $$PreferencesTableTableOrderingComposer, + $$PreferencesTableTableProcessedTableManager, + $$PreferencesTableTableInsertCompanionBuilder, + $$PreferencesTableTableUpdateCompanionBuilder> { + $$PreferencesTableTableTableManager( + _$AppDatabase db, $PreferencesTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$PreferencesTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$PreferencesTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$PreferencesTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value audioQuality = const Value.absent(), + Value albumColorSync = const Value.absent(), + Value amoledDarkTheme = const Value.absent(), + Value checkUpdate = const Value.absent(), + Value normalizeAudio = const Value.absent(), + Value showSystemTrayIcon = const Value.absent(), + Value systemTitleBar = const Value.absent(), + Value skipNonMusic = const Value.absent(), + Value closeBehavior = const Value.absent(), + Value accentColorScheme = const Value.absent(), + Value layoutMode = const Value.absent(), + Value locale = const Value.absent(), + Value market = const Value.absent(), + Value searchMode = const Value.absent(), + Value downloadLocation = const Value.absent(), + Value> localLibraryLocation = const Value.absent(), + Value pipedInstance = const Value.absent(), + Value themeMode = const Value.absent(), + Value audioSource = const Value.absent(), + Value streamMusicCodec = const Value.absent(), + Value downloadMusicCodec = const Value.absent(), + Value discordPresence = const Value.absent(), + Value endlessPlayback = const Value.absent(), + Value enableConnect = const Value.absent(), + }) => + PreferencesTableCompanion( + id: id, + audioQuality: audioQuality, + albumColorSync: albumColorSync, + amoledDarkTheme: amoledDarkTheme, + checkUpdate: checkUpdate, + normalizeAudio: normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon, + systemTitleBar: systemTitleBar, + skipNonMusic: skipNonMusic, + closeBehavior: closeBehavior, + accentColorScheme: accentColorScheme, + layoutMode: layoutMode, + locale: locale, + market: market, + searchMode: searchMode, + downloadLocation: downloadLocation, + localLibraryLocation: localLibraryLocation, + pipedInstance: pipedInstance, + themeMode: themeMode, + audioSource: audioSource, + streamMusicCodec: streamMusicCodec, + downloadMusicCodec: downloadMusicCodec, + discordPresence: discordPresence, + endlessPlayback: endlessPlayback, + enableConnect: enableConnect, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + Value audioQuality = const Value.absent(), + Value albumColorSync = const Value.absent(), + Value amoledDarkTheme = const Value.absent(), + Value checkUpdate = const Value.absent(), + Value normalizeAudio = const Value.absent(), + Value showSystemTrayIcon = const Value.absent(), + Value systemTitleBar = const Value.absent(), + Value skipNonMusic = const Value.absent(), + Value closeBehavior = const Value.absent(), + Value accentColorScheme = const Value.absent(), + Value layoutMode = const Value.absent(), + Value locale = const Value.absent(), + Value market = const Value.absent(), + Value searchMode = const Value.absent(), + Value downloadLocation = const Value.absent(), + Value> localLibraryLocation = const Value.absent(), + Value pipedInstance = const Value.absent(), + Value themeMode = const Value.absent(), + Value audioSource = const Value.absent(), + Value streamMusicCodec = const Value.absent(), + Value downloadMusicCodec = const Value.absent(), + Value discordPresence = const Value.absent(), + Value endlessPlayback = const Value.absent(), + Value enableConnect = const Value.absent(), + }) => + PreferencesTableCompanion.insert( + id: id, + audioQuality: audioQuality, + albumColorSync: albumColorSync, + amoledDarkTheme: amoledDarkTheme, + checkUpdate: checkUpdate, + normalizeAudio: normalizeAudio, + showSystemTrayIcon: showSystemTrayIcon, + systemTitleBar: systemTitleBar, + skipNonMusic: skipNonMusic, + closeBehavior: closeBehavior, + accentColorScheme: accentColorScheme, + layoutMode: layoutMode, + locale: locale, + market: market, + searchMode: searchMode, + downloadLocation: downloadLocation, + localLibraryLocation: localLibraryLocation, + pipedInstance: pipedInstance, + themeMode: themeMode, + audioSource: audioSource, + streamMusicCodec: streamMusicCodec, + downloadMusicCodec: downloadMusicCodec, + discordPresence: discordPresence, + endlessPlayback: endlessPlayback, + enableConnect: enableConnect, + ), + )); +} + +class $$PreferencesTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $PreferencesTableTable, + PreferencesTableData, + $$PreferencesTableTableFilterComposer, + $$PreferencesTableTableOrderingComposer, + $$PreferencesTableTableProcessedTableManager, + $$PreferencesTableTableInsertCompanionBuilder, + $$PreferencesTableTableUpdateCompanionBuilder> { + $$PreferencesTableTableProcessedTableManager(super.$state); +} + +class $$PreferencesTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $PreferencesTableTable> { + $$PreferencesTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get audioQuality => $state.composableBuilder( + column: $state.table.audioQuality, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get albumColorSync => $state.composableBuilder( + column: $state.table.albumColorSync, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get amoledDarkTheme => $state.composableBuilder( + column: $state.table.amoledDarkTheme, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get checkUpdate => $state.composableBuilder( + column: $state.table.checkUpdate, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get normalizeAudio => $state.composableBuilder( + column: $state.table.normalizeAudio, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get showSystemTrayIcon => $state.composableBuilder( + column: $state.table.showSystemTrayIcon, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get systemTitleBar => $state.composableBuilder( + column: $state.table.systemTitleBar, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get skipNonMusic => $state.composableBuilder( + column: $state.table.skipNonMusic, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get closeBehavior => $state.composableBuilder( + column: $state.table.closeBehavior, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get accentColorScheme => $state.composableBuilder( + column: $state.table.accentColorScheme, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get layoutMode => $state.composableBuilder( + column: $state.table.layoutMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters get locale => + $state.composableBuilder( + column: $state.table.locale, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters get market => + $state.composableBuilder( + column: $state.table.market, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get searchMode => $state.composableBuilder( + column: $state.table.searchMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get downloadLocation => $state.composableBuilder( + column: $state.table.downloadLocation, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters, List, String> + get localLibraryLocation => $state.composableBuilder( + column: $state.table.localLibraryLocation, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get pipedInstance => $state.composableBuilder( + column: $state.table.pipedInstance, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters get themeMode => + $state.composableBuilder( + column: $state.table.themeMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get audioSource => $state.composableBuilder( + column: $state.table.audioSource, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get streamMusicCodec => $state.composableBuilder( + column: $state.table.streamMusicCodec, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get downloadMusicCodec => $state.composableBuilder( + column: $state.table.downloadMusicCodec, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get discordPresence => $state.composableBuilder( + column: $state.table.discordPresence, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get endlessPlayback => $state.composableBuilder( + column: $state.table.endlessPlayback, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get enableConnect => $state.composableBuilder( + column: $state.table.enableConnect, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$PreferencesTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $PreferencesTableTable> { + $$PreferencesTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get audioQuality => $state.composableBuilder( + column: $state.table.audioQuality, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get albumColorSync => $state.composableBuilder( + column: $state.table.albumColorSync, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get amoledDarkTheme => $state.composableBuilder( + column: $state.table.amoledDarkTheme, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get checkUpdate => $state.composableBuilder( + column: $state.table.checkUpdate, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get normalizeAudio => $state.composableBuilder( + column: $state.table.normalizeAudio, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get showSystemTrayIcon => $state.composableBuilder( + column: $state.table.showSystemTrayIcon, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get systemTitleBar => $state.composableBuilder( + column: $state.table.systemTitleBar, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get skipNonMusic => $state.composableBuilder( + column: $state.table.skipNonMusic, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get closeBehavior => $state.composableBuilder( + column: $state.table.closeBehavior, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get accentColorScheme => $state.composableBuilder( + column: $state.table.accentColorScheme, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get layoutMode => $state.composableBuilder( + column: $state.table.layoutMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get locale => $state.composableBuilder( + column: $state.table.locale, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get market => $state.composableBuilder( + column: $state.table.market, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get searchMode => $state.composableBuilder( + column: $state.table.searchMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get downloadLocation => $state.composableBuilder( + column: $state.table.downloadLocation, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get localLibraryLocation => $state.composableBuilder( + column: $state.table.localLibraryLocation, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get pipedInstance => $state.composableBuilder( + column: $state.table.pipedInstance, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get themeMode => $state.composableBuilder( + column: $state.table.themeMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get audioSource => $state.composableBuilder( + column: $state.table.audioSource, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get streamMusicCodec => $state.composableBuilder( + column: $state.table.streamMusicCodec, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get downloadMusicCodec => $state.composableBuilder( + column: $state.table.downloadMusicCodec, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get discordPresence => $state.composableBuilder( + column: $state.table.discordPresence, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get endlessPlayback => $state.composableBuilder( + column: $state.table.endlessPlayback, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get enableConnect => $state.composableBuilder( + column: $state.table.enableConnect, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +class _$AppDatabaseManager { + final _$AppDatabase _db; + _$AppDatabaseManager(this._db); + $$PreferencesTableTableTableManager get preferencesTable => + $$PreferencesTableTableTableManager(_db, _db.preferencesTable); +} diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart new file mode 100644 index 00000000..ae4ec1e8 --- /dev/null +++ b/lib/models/database/tables/preferences.dart @@ -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() + .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() + .withDefault(Constant(CloseBehavior.close.name))(); + TextColumn get accentColorScheme => text() + .withDefault(const Constant("Blue:0xFF2196F3")) + .map(const SpotubeColorConverter())(); + TextColumn get layoutMode => + textEnum().withDefault(Constant(LayoutMode.adaptive.name))(); + TextColumn get locale => text() + .withDefault( + const Constant('{"languageCode":"system","countryCode":"system"}'), + ) + .map(const LocaleConverter())(); + TextColumn get market => + textEnum().withDefault(Constant(Market.US.name))(); + TextColumn get searchMode => + textEnum().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().withDefault(Constant(ThemeMode.system.name))(); + TextColumn get audioSource => + textEnum().withDefault(Constant(AudioSource.youtube.name))(); + TextColumn get streamMusicCodec => + textEnum().withDefault(Constant(SourceCodecs.weba.name))(); + TextColumn get downloadMusicCodec => + textEnum().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: SpotubeColor(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, + ); + } +} diff --git a/lib/models/database/typeconverters/color.dart b/lib/models/database/typeconverters/color.dart new file mode 100644 index 00000000..70c27374 --- /dev/null +++ b/lib/models/database/typeconverters/color.dart @@ -0,0 +1,29 @@ +part of '../database.dart'; + +class ColorConverter extends TypeConverter { + const ColorConverter(); + + @override + Color fromSql(int fromDb) { + return Color(fromDb); + } + + @override + int toSql(Color value) { + return value.value; + } +} + +class SpotubeColorConverter extends TypeConverter { + const SpotubeColorConverter(); + + @override + SpotubeColor fromSql(String fromDb) { + return SpotubeColor.fromString(fromDb); + } + + @override + String toSql(SpotubeColor value) { + return value.toString(); + } +} diff --git a/lib/models/database/typeconverters/locale.dart b/lib/models/database/typeconverters/locale.dart new file mode 100644 index 00000000..c460088e --- /dev/null +++ b/lib/models/database/typeconverters/locale.dart @@ -0,0 +1,19 @@ +part of '../database.dart'; + +class LocaleConverter extends TypeConverter { + const LocaleConverter(); + + @override + Locale fromSql(String fromDb) { + final rawMap = jsonDecode(fromDb) as Map; + return Locale(rawMap["languageCode"], rawMap["countryCode"]); + } + + @override + String toSql(Locale value) { + return jsonEncode({ + "languageCode": value.languageCode, + "countryCode": value.countryCode, + }); + } +} diff --git a/lib/models/database/typeconverters/string_list.dart b/lib/models/database/typeconverters/string_list.dart new file mode 100644 index 00000000..5c30a997 --- /dev/null +++ b/lib/models/database/typeconverters/string_list.dart @@ -0,0 +1,15 @@ +part of '../database.dart'; + +class StringListConverter extends TypeConverter, String> { + const StringListConverter(); + + @override + List fromSql(String fromDb) { + return fromDb.split(","); + } + + @override + String toSql(List value) { + return value.join(","); + } +} diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 14731907..a6136e62 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -14,10 +14,11 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index 2ab4b14a..14784176 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_overlay.dart'; import 'package:spotube/modules/player/player_track_details.dart'; @@ -20,7 +21,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index 79d229ef..592a3d90 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -9,6 +9,7 @@ import 'package:sidebarx/sidebarx.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -23,7 +24,7 @@ import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:window_manager/window_manager.dart'; diff --git a/lib/modules/root/spotube_navigation_bar.dart b/lib/modules/root/spotube_navigation_bar.dart index e16ad1a8..c624a40c 100644 --- a/lib/modules/root/spotube_navigation_bar.dart +++ b/lib/modules/root/spotube_navigation_bar.dart @@ -10,9 +10,10 @@ import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/utils/service_utils.dart'; final navigationPanelHeight = StateProvider((ref) => 50); diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index fab51d06..e7087afd 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -4,11 +4,11 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/string.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; final audioSourceToIconMap = { AudioSource.youtube: const Icon( diff --git a/lib/pages/getting_started/sections/region.dart b/lib/pages/getting_started/sections/region.dart index 0a80fba2..9e31a273 100644 --- a/lib/pages/getting_started/sections/region.dart +++ b/lib/pages/getting_started/sections/region.dart @@ -55,14 +55,14 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget { ), const Gap(16), DropdownMenu( - initialSelection: preferences.recommendationMarket, + initialSelection: preferences.market, onSelected: (value) { if (value == null) return; ref .read(userPreferencesProvider.notifier) .setRecommendationMarket(value); }, - hintText: preferences.recommendationMarket.name, + hintText: preferences.market.name, label: Text(context.l10n.market_place_region), inputDecorationTheme: const InputDecorationTheme(isDense: true), diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index c73c0b08..b62013c5 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -39,7 +39,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget { final genresCollection = ref.watch(categoryGenresProvider); final limit = useValueNotifier(10); - final market = useValueNotifier(preferences.recommendationMarket); + final market = useValueNotifier(preferences.market); final genres = useState>([]); final artists = useState>([]); diff --git a/lib/pages/settings/sections/appearance.dart b/lib/pages/settings/sections/appearance.dart index 67ed282b..f97add42 100644 --- a/lib/pages/settings/sections/appearance.dart +++ b/lib/pages/settings/sections/appearance.dart @@ -3,12 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; class SettingsAppearanceSection extends HookConsumerWidget { final bool isGettingStarted; diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index 5fbbd8b0..88f0ae6d 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/utils/platform.dart'; class SettingsDesktopSection extends HookConsumerWidget { diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart index c9776fd6..18c2d088 100644 --- a/lib/pages/settings/sections/language_region.dart +++ b/lib/pages/settings/sections/language_region.dart @@ -57,7 +57,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget { secondary: const Icon(SpotubeIcons.shoppingBag), title: Text(context.l10n.market_place_region), subtitle: Text(context.l10n.recommendation_country), - value: preferences.recommendationMarket, + value: preferences.market, onChanged: (value) { if (value == null) return; preferencesNotifier.setRecommendationMarket(value); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 0d37d990..6273c557 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -6,12 +6,13 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/piped_instances_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/services/sourced_track/enums.dart'; class SettingsPlaybackSection extends HookConsumerWidget { diff --git a/lib/provider/database/database.dart b/lib/provider/database/database.dart new file mode 100644 index 00000000..95976e56 --- /dev/null +++ b/lib/provider/database/database.dart @@ -0,0 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/database/database.dart'; + +final databaseProvider = Provider((ref) => AppDatabase()); diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index c8eb3657..d52073da 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -14,7 +14,7 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/provider/discord_provider.dart'; diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/proxy_playlist/skip_segments.dart index 12d066ac..461ac24e 100644 --- a/lib/provider/proxy_playlist/skip_segments.dart +++ b/lib/provider/proxy_playlist/skip_segments.dart @@ -1,10 +1,11 @@ +import 'package:spotube/models/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/skip_segment.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/services/dio/dio.dart'; class SourcedSegments { diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index dd9d6c3b..679f58b1 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -7,7 +7,6 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/logger/logger.dart'; class ServerPlaybackRoutes { diff --git a/lib/provider/spotify/album/releases.dart b/lib/provider/spotify/album/releases.dart index cacddbdf..43d2e474 100644 --- a/lib/provider/spotify/album/releases.dart +++ b/lib/provider/spotify/album/releases.dart @@ -30,7 +30,7 @@ class AlbumReleasesNotifier @override fetch(int offset, int limit) async { - final market = ref.read(userPreferencesProvider).recommendationMarket; + final market = ref.read(userPreferencesProvider).market; final albums = await spotify.browse .newReleases(country: market) @@ -43,7 +43,7 @@ class AlbumReleasesNotifier build() async { ref.watch(spotifyProvider); ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); ref.watch(allFollowedArtistsProvider); diff --git a/lib/provider/spotify/artist/albums.dart b/lib/provider/spotify/artist/albums.dart index 16bd8768..32aa38a6 100644 --- a/lib/provider/spotify/artist/albums.dart +++ b/lib/provider/spotify/artist/albums.dart @@ -30,7 +30,7 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< @override fetch(arg, offset, limit) async { - final market = ref.read(userPreferencesProvider).recommendationMarket; + final market = ref.read(userPreferencesProvider).market; final albums = await spotify.artists .albums(arg, country: market) .getPage(limit, offset); @@ -44,7 +44,7 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.watch(spotifyProvider); ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final albums = await fetch(arg, 0, 20); return ArtistAlbumsState( diff --git a/lib/provider/spotify/artist/top_tracks.dart b/lib/provider/spotify/artist/top_tracks.dart index fa40d646..a2862c3d 100644 --- a/lib/provider/spotify/artist/top_tracks.dart +++ b/lib/provider/spotify/artist/top_tracks.dart @@ -6,8 +6,7 @@ final artistTopTracksProvider = ref.cacheFor(); final spotify = ref.watch(spotifyProvider); - final market = ref - .watch(userPreferencesProvider.select((s) => s.recommendationMarket)); + final market = ref.watch(userPreferencesProvider.select((s) => s.market)); final tracks = await spotify.artists.topTracks(artistId, market); return tracks.toList(); diff --git a/lib/provider/spotify/category/categories.dart b/lib/provider/spotify/category/categories.dart index 7652215c..6237b64c 100644 --- a/lib/provider/spotify/category/categories.dart +++ b/lib/provider/spotify/category/categories.dart @@ -3,8 +3,7 @@ part of '../spotify.dart'; final categoriesProvider = FutureProvider( (ref) async { final spotify = ref.watch(spotifyProvider); - final market = ref - .watch(userPreferencesProvider.select((s) => s.recommendationMarket)); + final market = ref.watch(userPreferencesProvider.select((s) => s.market)); final locale = ref.watch(userPreferencesProvider.select((s) => s.locale)); final categories = await spotify.categories .list( diff --git a/lib/provider/spotify/category/playlists.dart b/lib/provider/spotify/category/playlists.dart index 979b7f31..18d4845f 100644 --- a/lib/provider/spotify/category/playlists.dart +++ b/lib/provider/spotify/category/playlists.dart @@ -33,7 +33,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< final preferences = ref.read(userPreferencesProvider); final playlists = await Pages( spotify, - "v1/browse/categories/$arg/playlists?country=${preferences.recommendationMarket.name}&locale=${preferences.locale}", + "v1/browse/categories/$arg/playlists?country=${preferences.market.name}&locale=${preferences.locale}", (json) => json == null ? null : PlaylistSimple.fromJson(json), 'playlists', (json) => PlaylistsFeatured.fromJson(json), @@ -48,7 +48,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.watch(spotifyProvider); ref.watch(userPreferencesProvider.select((s) => s.locale)); - ref.watch(userPreferencesProvider.select((s) => s.recommendationMarket)); + ref.watch(userPreferencesProvider.select((s) => s.market)); final playlists = await fetch(arg, 0, 8); diff --git a/lib/provider/spotify/playlist/generate.dart b/lib/provider/spotify/playlist/generate.dart index 2e1196dd..0832003e 100644 --- a/lib/provider/spotify/playlist/generate.dart +++ b/lib/provider/spotify/playlist/generate.dart @@ -5,7 +5,7 @@ final generatePlaylistProvider = FutureProvider.autoDispose (ref, input) async { final spotify = ref.watch(spotifyProvider); final market = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final recommendation = await spotify.recommendations diff --git a/lib/provider/spotify/search/search.dart b/lib/provider/spotify/search/search.dart index bd97f08b..dc00d913 100644 --- a/lib/provider/spotify/search/search.dart +++ b/lib/provider/spotify/search/search.dart @@ -42,7 +42,7 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier value.recommendationMarket), + userPreferencesProvider.select((value) => value.market), ); final results = await fetch(arg, 0, 10); diff --git a/lib/provider/spotify/views/home.dart b/lib/provider/spotify/views/home.dart index 810d110d..51586953 100644 --- a/lib/provider/spotify/views/home.dart +++ b/lib/provider/spotify/views/home.dart @@ -5,7 +5,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart final homeViewProvider = FutureProvider((ref) async { final country = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( authenticationProvider.select((s) => s?.getCookie("sp_t")), diff --git a/lib/provider/spotify/views/home_section.dart b/lib/provider/spotify/views/home_section.dart index 1078fa72..04c4cbd6 100644 --- a/lib/provider/spotify/views/home_section.dart +++ b/lib/provider/spotify/views/home_section.dart @@ -8,7 +8,7 @@ final homeSectionViewProvider = FutureProvider.family( (ref, sectionUri) async { final country = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( authenticationProvider.select((s) => s?.getCookie("sp_t")), diff --git a/lib/provider/spotify/views/view.dart b/lib/provider/spotify/views/view.dart index f1af998b..ff565feb 100644 --- a/lib/provider/spotify/views/view.dart +++ b/lib/provider/spotify/views/view.dart @@ -4,7 +4,7 @@ final viewProvider = FutureProvider.family, String>( (ref, viewName) async { final customSpotify = ref.watch(customSpotifyEndpointProvider); final market = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + userPreferencesProvider.select((s) => s.market), ); final locale = ref.watch( userPreferencesProvider.select((s) => s.locale), diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 5825104a..8b96305f 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -1,54 +1,116 @@ -import 'dart:async'; - +import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/player_listeners.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/enums.dart'; - -import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; -import 'package:path/path.dart' as path; import 'package:window_manager/window_manager.dart'; -class UserPreferencesNotifier extends PersistedStateNotifier { - final Ref ref; +typedef UserPreferences = PreferencesTableData; - UserPreferencesNotifier(this.ref) - : super(UserPreferences.withDefaults(), "preferences"); +class UserPreferencesNotifier extends Notifier { + @override + build() { + final db = ref.watch(databaseProvider); - void reset() { - state = UserPreferences.withDefaults(); + (db.select(db.preferencesTable)..where((tbl) => tbl.id.equals(0))) + .getSingleOrNull() + .then((result) async { + if (result == null) { + await db.into(db.preferencesTable).insert( + PreferencesTableCompanion.insert( + id: const Value(0), + downloadLocation: Value(await _getDefaultDownloadDirectory()), + ), + ); + } + + state = await (db.select(db.preferencesTable) + ..where((tbl) => tbl.id.equals(0))) + .getSingle(); + + final subscription = (db.select(db.preferencesTable) + ..where((tbl) => tbl.id.equals(0))) + .watchSingle() + .listen((event) async { + state = event; + + if (kIsDesktop) { + await windowManager.setTitleBarStyle( + state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, + ); + } + + await audioPlayer.setAudioNormalization(state.normalizeAudio); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + }); + + return PreferencesTable.defaults(); + } + + Future _getDefaultDownloadDirectory() async { + if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; + + if (kIsMacOS) { + return join((await getLibraryDirectory()).path, "Caches"); + } + + return getDownloadsDirectory().then((dir) { + return join(dir!.path, "Spotube"); + }); + } + + Future setData(PreferencesTableCompanion data) async { + final db = ref.read(databaseProvider); + + final query = db.update(db.preferencesTable)..where((t) => t.id.equals(0)); + + await query.write(data); + } + + Future reset() async { + final db = ref.read(databaseProvider); + + final query = db.update(db.preferencesTable)..where((t) => t.id.equals(0)); + + await query.replace(PreferencesTableCompanion.insert()); } void setStreamMusicCodec(SourceCodecs codec) { - state = state.copyWith(streamMusicCodec: codec); + setData(PreferencesTableCompanion(streamMusicCodec: Value(codec))); } void setDownloadMusicCodec(SourceCodecs codec) { - state = state.copyWith(downloadMusicCodec: codec); + setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec))); } void setThemeMode(ThemeMode mode) { - state = state.copyWith(themeMode: mode); + setData(PreferencesTableCompanion(themeMode: Value(mode))); } void setRecommendationMarket(Market country) { - state = state.copyWith(recommendationMarket: country); + setData(PreferencesTableCompanion(market: Value(country))); } void setAccentColorScheme(SpotubeColor color) { - state = state.copyWith(accentColorScheme: color); + setData(PreferencesTableCompanion(accentColorScheme: Value(color))); } void setAlbumColorSync(bool sync) { - state = state.copyWith(albumColorSync: sync); + setData(PreferencesTableCompanion(albumColorSync: Value(sync))); if (!sync) { ref.read(paletteProvider.notifier).state = null; @@ -58,126 +120,87 @@ class UserPreferencesNotifier extends PersistedStateNotifier { } void setCheckUpdate(bool check) { - state = state.copyWith(checkUpdate: check); + setData(PreferencesTableCompanion(checkUpdate: Value(check))); } void setAudioQuality(SourceQualities quality) { - state = state.copyWith(audioQuality: quality); + setData(PreferencesTableCompanion(audioQuality: Value(quality))); } void setDownloadLocation(String downloadDir) { if (downloadDir.isEmpty) return; - state = state.copyWith(downloadLocation: downloadDir); + setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir))); } void setLocalLibraryLocation(List localLibraryDirs) { //if (localLibraryDir.isEmpty) return; - state = state.copyWith(localLibraryLocation: localLibraryDirs); + setData(PreferencesTableCompanion( + localLibraryLocation: Value(localLibraryDirs))); } void setLayoutMode(LayoutMode mode) { - state = state.copyWith(layoutMode: mode); + setData(PreferencesTableCompanion(layoutMode: Value(mode))); } void setCloseBehavior(CloseBehavior behavior) { - state = state.copyWith(closeBehavior: behavior); + setData(PreferencesTableCompanion(closeBehavior: Value(behavior))); } void setShowSystemTrayIcon(bool show) { - state = state.copyWith(showSystemTrayIcon: show); + setData(PreferencesTableCompanion(showSystemTrayIcon: Value(show))); } void setLocale(Locale locale) { - state = state.copyWith(locale: locale); + setData(PreferencesTableCompanion(locale: Value(locale))); } void setPipedInstance(String instance) { - state = state.copyWith(pipedInstance: instance); + setData(PreferencesTableCompanion(pipedInstance: Value(instance))); } void setSearchMode(SearchMode mode) { - state = state.copyWith(searchMode: mode); + setData(PreferencesTableCompanion(searchMode: Value(mode))); } void setSkipNonMusic(bool skip) { - state = state.copyWith(skipNonMusic: skip); + setData(PreferencesTableCompanion(skipNonMusic: Value(skip))); } void setAudioSource(AudioSource type) { - state = state.copyWith(audioSource: type); + setData(PreferencesTableCompanion(audioSource: Value(type))); } void setSystemTitleBar(bool isSystemTitleBar) { - state = state.copyWith(systemTitleBar: isSystemTitleBar); - if (kIsDesktop) { - windowManager.setTitleBarStyle( - isSystemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, - ); - } + setData( + PreferencesTableCompanion( + systemTitleBar: Value(isSystemTitleBar), + ), + ); } void setDiscordPresence(bool discordPresence) { - state = state.copyWith(discordPresence: discordPresence); + setData(PreferencesTableCompanion(discordPresence: Value(discordPresence))); } void setAmoledDarkTheme(bool isAmoled) { - state = state.copyWith(amoledDarkTheme: isAmoled); + setData(PreferencesTableCompanion(amoledDarkTheme: Value(isAmoled))); } void setNormalizeAudio(bool normalize) { - state = state.copyWith(normalizeAudio: normalize); + setData(PreferencesTableCompanion(normalizeAudio: Value(normalize))); audioPlayer.setAudioNormalization(normalize); } void setEndlessPlayback(bool endless) { - state = state.copyWith(endlessPlayback: endless); + setData(PreferencesTableCompanion(endlessPlayback: Value(endless))); } void setEnableConnect(bool enable) { - state = state.copyWith(enableConnect: enable); - } - - Future _getDefaultDownloadDirectory() async { - if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; - - if (kIsMacOS) { - return path.join((await getLibraryDirectory()).path, "Caches"); - } - - return getDownloadsDirectory().then((dir) { - return path.join(dir!.path, "Spotube"); - }); - } - - @override - FutureOr onInit() async { - if (state.downloadLocation.isEmpty) { - state = state.copyWith( - downloadLocation: await _getDefaultDownloadDirectory(), - ); - } - - if (kIsDesktop) { - await windowManager.setTitleBarStyle( - state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, - ); - } - - await audioPlayer.setAudioNormalization(state.normalizeAudio); - } - - @override - FutureOr fromJson(Map json) { - return UserPreferences.fromJson(json); - } - - @override - Map toJson() { - return state.toJson(); + setData(PreferencesTableCompanion(enableConnect: Value(enable))); } } final userPreferencesProvider = - StateNotifierProvider( - (ref) => UserPreferencesNotifier(ref), + NotifierProvider( + () => UserPreferencesNotifier(), ); diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart deleted file mode 100644 index 73dd02e8..00000000 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; - -part 'user_preferences_state.g.dart'; -part 'user_preferences_state.freezed.dart'; - -@JsonEnum() -enum LayoutMode { - compact, - extended, - adaptive, -} - -@JsonEnum() -enum CloseBehavior { - minimizeToTray, - close, -} - -@JsonEnum() -enum AudioSource { - youtube, - piped, - jiosaavn; - - String get label => name[0].toUpperCase() + name.substring(1); -} - -@JsonEnum() -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); -} - -@JsonEnum() -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); - } -} - -@freezed -class UserPreferences with _$UserPreferences { - const factory UserPreferences({ - @Default(SourceQualities.high) SourceQualities audioQuality, - @Default(true) bool albumColorSync, - @Default(false) bool amoledDarkTheme, - @Default(true) bool checkUpdate, - @Default(false) bool normalizeAudio, - @Default(false) bool showSystemTrayIcon, - @Default(false) bool skipNonMusic, - @Default(false) bool systemTitleBar, - @Default(CloseBehavior.close) CloseBehavior closeBehavior, - @Default(SpotubeColor(0xFF2196F3, name: "Blue")) - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue, - ) - SpotubeColor accentColorScheme, - @Default(LayoutMode.adaptive) LayoutMode layoutMode, - @Default(Locale("system", "system")) - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue, - ) - Locale locale, - @Default(Market.US) Market recommendationMarket, - @Default(SearchMode.youtube) SearchMode searchMode, - @Default("") String downloadLocation, - @Default([]) List localLibraryLocation, - @Default("https://pipedapi.kavin.rocks") String pipedInstance, - @Default(ThemeMode.system) ThemeMode themeMode, - @Default(AudioSource.youtube) AudioSource audioSource, - @Default(SourceCodecs.weba) SourceCodecs streamMusicCodec, - @Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec, - @Default(true) bool discordPresence, - @Default(true) bool endlessPlayback, - @Default(false) bool enableConnect, - }) = _UserPreferences; - factory UserPreferences.fromJson(Map json) => - _$UserPreferencesFromJson(json); - - factory UserPreferences.withDefaults() => UserPreferences.fromJson({}); - - static SpotubeColor _accentColorSchemeFromJson(Map json) { - return SpotubeColor.fromString(json["color"]); - } - - static Map? _accentColorSchemeReadValue( - Map json, String key) { - if (json[key] is String) { - return {"color": json[key]}; - } - - return json[key] as Map?; - } - - static Map _accentColorSchemeToJson(SpotubeColor color) { - return {"color": color.toString()}; - } - - static Locale _localeFromJson(Map json) { - return Locale(json["languageCode"], json["countryCode"]); - } - - static Map _localeToJson(Locale locale) { - return { - "languageCode": locale.languageCode, - "countryCode": locale.countryCode, - }; - } - - static Map? _localeReadValue( - Map json, String key) { - if (json[key] is String) { - final map = jsonDecode(json[key]); - return { - "languageCode": map["lc"], - "countryCode": map["cc"], - }; - } - - return json[key] as Map?; - } -} diff --git a/lib/provider/user_preferences/user_preferences_state.freezed.dart b/lib/provider/user_preferences/user_preferences_state.freezed.dart deleted file mode 100644 index 89c7210a..00000000 --- a/lib/provider/user_preferences/user_preferences_state.freezed.dart +++ /dev/null @@ -1,751 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'user_preferences_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -UserPreferences _$UserPreferencesFromJson(Map json) { - return _UserPreferences.fromJson(json); -} - -/// @nodoc -mixin _$UserPreferences { - SourceQualities get audioQuality => throw _privateConstructorUsedError; - bool get albumColorSync => throw _privateConstructorUsedError; - bool get amoledDarkTheme => throw _privateConstructorUsedError; - bool get checkUpdate => throw _privateConstructorUsedError; - bool get normalizeAudio => throw _privateConstructorUsedError; - bool get showSystemTrayIcon => throw _privateConstructorUsedError; - bool get skipNonMusic => throw _privateConstructorUsedError; - bool get systemTitleBar => throw _privateConstructorUsedError; - CloseBehavior get closeBehavior => throw _privateConstructorUsedError; - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor get accentColorScheme => throw _privateConstructorUsedError; - LayoutMode get layoutMode => throw _privateConstructorUsedError; - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale get locale => throw _privateConstructorUsedError; - Market get recommendationMarket => throw _privateConstructorUsedError; - SearchMode get searchMode => throw _privateConstructorUsedError; - String get downloadLocation => throw _privateConstructorUsedError; - List get localLibraryLocation => throw _privateConstructorUsedError; - String get pipedInstance => throw _privateConstructorUsedError; - ThemeMode get themeMode => throw _privateConstructorUsedError; - AudioSource get audioSource => throw _privateConstructorUsedError; - SourceCodecs get streamMusicCodec => throw _privateConstructorUsedError; - SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError; - bool get discordPresence => throw _privateConstructorUsedError; - bool get endlessPlayback => throw _privateConstructorUsedError; - bool get enableConnect => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $UserPreferencesCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $UserPreferencesCopyWith<$Res> { - factory $UserPreferencesCopyWith( - UserPreferences value, $Res Function(UserPreferences) then) = - _$UserPreferencesCopyWithImpl<$Res, UserPreferences>; - @useResult - $Res call( - {SourceQualities audioQuality, - bool albumColorSync, - bool amoledDarkTheme, - bool checkUpdate, - bool normalizeAudio, - bool showSystemTrayIcon, - bool skipNonMusic, - bool systemTitleBar, - CloseBehavior closeBehavior, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor accentColorScheme, - LayoutMode layoutMode, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale locale, - Market recommendationMarket, - SearchMode searchMode, - String downloadLocation, - List localLibraryLocation, - String pipedInstance, - ThemeMode themeMode, - AudioSource audioSource, - SourceCodecs streamMusicCodec, - SourceCodecs downloadMusicCodec, - bool discordPresence, - bool endlessPlayback, - bool enableConnect}); -} - -/// @nodoc -class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> - implements $UserPreferencesCopyWith<$Res> { - _$UserPreferencesCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? audioQuality = null, - Object? albumColorSync = null, - Object? amoledDarkTheme = null, - Object? checkUpdate = null, - Object? normalizeAudio = null, - Object? showSystemTrayIcon = null, - Object? skipNonMusic = null, - Object? systemTitleBar = null, - Object? closeBehavior = null, - Object? accentColorScheme = null, - Object? layoutMode = null, - Object? locale = null, - Object? recommendationMarket = null, - Object? searchMode = null, - Object? downloadLocation = null, - Object? localLibraryLocation = null, - Object? pipedInstance = null, - Object? themeMode = null, - Object? audioSource = null, - Object? streamMusicCodec = null, - Object? downloadMusicCodec = null, - Object? discordPresence = null, - Object? endlessPlayback = null, - Object? enableConnect = null, - }) { - return _then(_value.copyWith( - audioQuality: null == audioQuality - ? _value.audioQuality - : audioQuality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - albumColorSync: null == albumColorSync - ? _value.albumColorSync - : albumColorSync // ignore: cast_nullable_to_non_nullable - as bool, - amoledDarkTheme: null == amoledDarkTheme - ? _value.amoledDarkTheme - : amoledDarkTheme // ignore: cast_nullable_to_non_nullable - as bool, - checkUpdate: null == checkUpdate - ? _value.checkUpdate - : checkUpdate // ignore: cast_nullable_to_non_nullable - as bool, - normalizeAudio: null == normalizeAudio - ? _value.normalizeAudio - : normalizeAudio // ignore: cast_nullable_to_non_nullable - as bool, - showSystemTrayIcon: null == showSystemTrayIcon - ? _value.showSystemTrayIcon - : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable - as bool, - skipNonMusic: null == skipNonMusic - ? _value.skipNonMusic - : skipNonMusic // ignore: cast_nullable_to_non_nullable - as bool, - systemTitleBar: null == systemTitleBar - ? _value.systemTitleBar - : systemTitleBar // ignore: cast_nullable_to_non_nullable - as bool, - closeBehavior: null == closeBehavior - ? _value.closeBehavior - : closeBehavior // ignore: cast_nullable_to_non_nullable - as CloseBehavior, - accentColorScheme: null == accentColorScheme - ? _value.accentColorScheme - : accentColorScheme // ignore: cast_nullable_to_non_nullable - as SpotubeColor, - layoutMode: null == layoutMode - ? _value.layoutMode - : layoutMode // ignore: cast_nullable_to_non_nullable - as LayoutMode, - locale: null == locale - ? _value.locale - : locale // ignore: cast_nullable_to_non_nullable - as Locale, - recommendationMarket: null == recommendationMarket - ? _value.recommendationMarket - : recommendationMarket // ignore: cast_nullable_to_non_nullable - as Market, - searchMode: null == searchMode - ? _value.searchMode - : searchMode // ignore: cast_nullable_to_non_nullable - as SearchMode, - downloadLocation: null == downloadLocation - ? _value.downloadLocation - : downloadLocation // ignore: cast_nullable_to_non_nullable - as String, - localLibraryLocation: null == localLibraryLocation - ? _value.localLibraryLocation - : localLibraryLocation // ignore: cast_nullable_to_non_nullable - as List, - pipedInstance: null == pipedInstance - ? _value.pipedInstance - : pipedInstance // ignore: cast_nullable_to_non_nullable - as String, - themeMode: null == themeMode - ? _value.themeMode - : themeMode // ignore: cast_nullable_to_non_nullable - as ThemeMode, - audioSource: null == audioSource - ? _value.audioSource - : audioSource // ignore: cast_nullable_to_non_nullable - as AudioSource, - streamMusicCodec: null == streamMusicCodec - ? _value.streamMusicCodec - : streamMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - downloadMusicCodec: null == downloadMusicCodec - ? _value.downloadMusicCodec - : downloadMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - discordPresence: null == discordPresence - ? _value.discordPresence - : discordPresence // ignore: cast_nullable_to_non_nullable - as bool, - endlessPlayback: null == endlessPlayback - ? _value.endlessPlayback - : endlessPlayback // ignore: cast_nullable_to_non_nullable - as bool, - enableConnect: null == enableConnect - ? _value.enableConnect - : enableConnect // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$UserPreferencesImplCopyWith<$Res> - implements $UserPreferencesCopyWith<$Res> { - factory _$$UserPreferencesImplCopyWith(_$UserPreferencesImpl value, - $Res Function(_$UserPreferencesImpl) then) = - __$$UserPreferencesImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {SourceQualities audioQuality, - bool albumColorSync, - bool amoledDarkTheme, - bool checkUpdate, - bool normalizeAudio, - bool showSystemTrayIcon, - bool skipNonMusic, - bool systemTitleBar, - CloseBehavior closeBehavior, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor accentColorScheme, - LayoutMode layoutMode, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale locale, - Market recommendationMarket, - SearchMode searchMode, - String downloadLocation, - List localLibraryLocation, - String pipedInstance, - ThemeMode themeMode, - AudioSource audioSource, - SourceCodecs streamMusicCodec, - SourceCodecs downloadMusicCodec, - bool discordPresence, - bool endlessPlayback, - bool enableConnect}); -} - -/// @nodoc -class __$$UserPreferencesImplCopyWithImpl<$Res> - extends _$UserPreferencesCopyWithImpl<$Res, _$UserPreferencesImpl> - implements _$$UserPreferencesImplCopyWith<$Res> { - __$$UserPreferencesImplCopyWithImpl( - _$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? audioQuality = null, - Object? albumColorSync = null, - Object? amoledDarkTheme = null, - Object? checkUpdate = null, - Object? normalizeAudio = null, - Object? showSystemTrayIcon = null, - Object? skipNonMusic = null, - Object? systemTitleBar = null, - Object? closeBehavior = null, - Object? accentColorScheme = null, - Object? layoutMode = null, - Object? locale = null, - Object? recommendationMarket = null, - Object? searchMode = null, - Object? downloadLocation = null, - Object? localLibraryLocation = null, - Object? pipedInstance = null, - Object? themeMode = null, - Object? audioSource = null, - Object? streamMusicCodec = null, - Object? downloadMusicCodec = null, - Object? discordPresence = null, - Object? endlessPlayback = null, - Object? enableConnect = null, - }) { - return _then(_$UserPreferencesImpl( - audioQuality: null == audioQuality - ? _value.audioQuality - : audioQuality // ignore: cast_nullable_to_non_nullable - as SourceQualities, - albumColorSync: null == albumColorSync - ? _value.albumColorSync - : albumColorSync // ignore: cast_nullable_to_non_nullable - as bool, - amoledDarkTheme: null == amoledDarkTheme - ? _value.amoledDarkTheme - : amoledDarkTheme // ignore: cast_nullable_to_non_nullable - as bool, - checkUpdate: null == checkUpdate - ? _value.checkUpdate - : checkUpdate // ignore: cast_nullable_to_non_nullable - as bool, - normalizeAudio: null == normalizeAudio - ? _value.normalizeAudio - : normalizeAudio // ignore: cast_nullable_to_non_nullable - as bool, - showSystemTrayIcon: null == showSystemTrayIcon - ? _value.showSystemTrayIcon - : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable - as bool, - skipNonMusic: null == skipNonMusic - ? _value.skipNonMusic - : skipNonMusic // ignore: cast_nullable_to_non_nullable - as bool, - systemTitleBar: null == systemTitleBar - ? _value.systemTitleBar - : systemTitleBar // ignore: cast_nullable_to_non_nullable - as bool, - closeBehavior: null == closeBehavior - ? _value.closeBehavior - : closeBehavior // ignore: cast_nullable_to_non_nullable - as CloseBehavior, - accentColorScheme: null == accentColorScheme - ? _value.accentColorScheme - : accentColorScheme // ignore: cast_nullable_to_non_nullable - as SpotubeColor, - layoutMode: null == layoutMode - ? _value.layoutMode - : layoutMode // ignore: cast_nullable_to_non_nullable - as LayoutMode, - locale: null == locale - ? _value.locale - : locale // ignore: cast_nullable_to_non_nullable - as Locale, - recommendationMarket: null == recommendationMarket - ? _value.recommendationMarket - : recommendationMarket // ignore: cast_nullable_to_non_nullable - as Market, - searchMode: null == searchMode - ? _value.searchMode - : searchMode // ignore: cast_nullable_to_non_nullable - as SearchMode, - downloadLocation: null == downloadLocation - ? _value.downloadLocation - : downloadLocation // ignore: cast_nullable_to_non_nullable - as String, - localLibraryLocation: null == localLibraryLocation - ? _value._localLibraryLocation - : localLibraryLocation // ignore: cast_nullable_to_non_nullable - as List, - pipedInstance: null == pipedInstance - ? _value.pipedInstance - : pipedInstance // ignore: cast_nullable_to_non_nullable - as String, - themeMode: null == themeMode - ? _value.themeMode - : themeMode // ignore: cast_nullable_to_non_nullable - as ThemeMode, - audioSource: null == audioSource - ? _value.audioSource - : audioSource // ignore: cast_nullable_to_non_nullable - as AudioSource, - streamMusicCodec: null == streamMusicCodec - ? _value.streamMusicCodec - : streamMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - downloadMusicCodec: null == downloadMusicCodec - ? _value.downloadMusicCodec - : downloadMusicCodec // ignore: cast_nullable_to_non_nullable - as SourceCodecs, - discordPresence: null == discordPresence - ? _value.discordPresence - : discordPresence // ignore: cast_nullable_to_non_nullable - as bool, - endlessPlayback: null == endlessPlayback - ? _value.endlessPlayback - : endlessPlayback // ignore: cast_nullable_to_non_nullable - as bool, - enableConnect: null == enableConnect - ? _value.enableConnect - : enableConnect // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$UserPreferencesImpl implements _UserPreferences { - const _$UserPreferencesImpl( - {this.audioQuality = SourceQualities.high, - this.albumColorSync = true, - this.amoledDarkTheme = false, - this.checkUpdate = true, - this.normalizeAudio = false, - this.showSystemTrayIcon = false, - this.skipNonMusic = false, - this.systemTitleBar = false, - this.closeBehavior = CloseBehavior.close, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - this.accentColorScheme = const SpotubeColor(0xFF2196F3, name: "Blue"), - this.layoutMode = LayoutMode.adaptive, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - this.locale = const Locale("system", "system"), - this.recommendationMarket = Market.US, - this.searchMode = SearchMode.youtube, - this.downloadLocation = "", - final List localLibraryLocation = const [], - this.pipedInstance = "https://pipedapi.kavin.rocks", - this.themeMode = ThemeMode.system, - this.audioSource = AudioSource.youtube, - this.streamMusicCodec = SourceCodecs.weba, - this.downloadMusicCodec = SourceCodecs.m4a, - this.discordPresence = true, - this.endlessPlayback = true, - this.enableConnect = false}) - : _localLibraryLocation = localLibraryLocation; - - factory _$UserPreferencesImpl.fromJson(Map json) => - _$$UserPreferencesImplFromJson(json); - - @override - @JsonKey() - final SourceQualities audioQuality; - @override - @JsonKey() - final bool albumColorSync; - @override - @JsonKey() - final bool amoledDarkTheme; - @override - @JsonKey() - final bool checkUpdate; - @override - @JsonKey() - final bool normalizeAudio; - @override - @JsonKey() - final bool showSystemTrayIcon; - @override - @JsonKey() - final bool skipNonMusic; - @override - @JsonKey() - final bool systemTitleBar; - @override - @JsonKey() - final CloseBehavior closeBehavior; - @override - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - final SpotubeColor accentColorScheme; - @override - @JsonKey() - final LayoutMode layoutMode; - @override - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - final Locale locale; - @override - @JsonKey() - final Market recommendationMarket; - @override - @JsonKey() - final SearchMode searchMode; - @override - @JsonKey() - final String downloadLocation; - final List _localLibraryLocation; - @override - @JsonKey() - List get localLibraryLocation { - if (_localLibraryLocation is EqualUnmodifiableListView) - return _localLibraryLocation; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_localLibraryLocation); - } - - @override - @JsonKey() - final String pipedInstance; - @override - @JsonKey() - final ThemeMode themeMode; - @override - @JsonKey() - final AudioSource audioSource; - @override - @JsonKey() - final SourceCodecs streamMusicCodec; - @override - @JsonKey() - final SourceCodecs downloadMusicCodec; - @override - @JsonKey() - final bool discordPresence; - @override - @JsonKey() - final bool endlessPlayback; - @override - @JsonKey() - final bool enableConnect; - - @override - String toString() { - return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, localLibraryLocation: $localLibraryLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$UserPreferencesImpl && - (identical(other.audioQuality, audioQuality) || - other.audioQuality == audioQuality) && - (identical(other.albumColorSync, albumColorSync) || - other.albumColorSync == albumColorSync) && - (identical(other.amoledDarkTheme, amoledDarkTheme) || - other.amoledDarkTheme == amoledDarkTheme) && - (identical(other.checkUpdate, checkUpdate) || - other.checkUpdate == checkUpdate) && - (identical(other.normalizeAudio, normalizeAudio) || - other.normalizeAudio == normalizeAudio) && - (identical(other.showSystemTrayIcon, showSystemTrayIcon) || - other.showSystemTrayIcon == showSystemTrayIcon) && - (identical(other.skipNonMusic, skipNonMusic) || - other.skipNonMusic == skipNonMusic) && - (identical(other.systemTitleBar, systemTitleBar) || - other.systemTitleBar == systemTitleBar) && - (identical(other.closeBehavior, closeBehavior) || - other.closeBehavior == closeBehavior) && - (identical(other.accentColorScheme, accentColorScheme) || - other.accentColorScheme == accentColorScheme) && - (identical(other.layoutMode, layoutMode) || - other.layoutMode == layoutMode) && - (identical(other.locale, locale) || other.locale == locale) && - (identical(other.recommendationMarket, recommendationMarket) || - other.recommendationMarket == recommendationMarket) && - (identical(other.searchMode, searchMode) || - other.searchMode == searchMode) && - (identical(other.downloadLocation, downloadLocation) || - other.downloadLocation == downloadLocation) && - const DeepCollectionEquality() - .equals(other._localLibraryLocation, _localLibraryLocation) && - (identical(other.pipedInstance, pipedInstance) || - other.pipedInstance == pipedInstance) && - (identical(other.themeMode, themeMode) || - other.themeMode == themeMode) && - (identical(other.audioSource, audioSource) || - other.audioSource == audioSource) && - (identical(other.streamMusicCodec, streamMusicCodec) || - other.streamMusicCodec == streamMusicCodec) && - (identical(other.downloadMusicCodec, downloadMusicCodec) || - other.downloadMusicCodec == downloadMusicCodec) && - (identical(other.discordPresence, discordPresence) || - other.discordPresence == discordPresence) && - (identical(other.endlessPlayback, endlessPlayback) || - other.endlessPlayback == endlessPlayback) && - (identical(other.enableConnect, enableConnect) || - other.enableConnect == enableConnect)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hashAll([ - runtimeType, - audioQuality, - albumColorSync, - amoledDarkTheme, - checkUpdate, - normalizeAudio, - showSystemTrayIcon, - skipNonMusic, - systemTitleBar, - closeBehavior, - accentColorScheme, - layoutMode, - locale, - recommendationMarket, - searchMode, - downloadLocation, - const DeepCollectionEquality().hash(_localLibraryLocation), - pipedInstance, - themeMode, - audioSource, - streamMusicCodec, - downloadMusicCodec, - discordPresence, - endlessPlayback, - enableConnect - ]); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => - __$$UserPreferencesImplCopyWithImpl<_$UserPreferencesImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$UserPreferencesImplToJson( - this, - ); - } -} - -abstract class _UserPreferences implements UserPreferences { - const factory _UserPreferences( - {final SourceQualities audioQuality, - final bool albumColorSync, - final bool amoledDarkTheme, - final bool checkUpdate, - final bool normalizeAudio, - final bool showSystemTrayIcon, - final bool skipNonMusic, - final bool systemTitleBar, - final CloseBehavior closeBehavior, - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - final SpotubeColor accentColorScheme, - final LayoutMode layoutMode, - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - final Locale locale, - final Market recommendationMarket, - final SearchMode searchMode, - final String downloadLocation, - final List localLibraryLocation, - final String pipedInstance, - final ThemeMode themeMode, - final AudioSource audioSource, - final SourceCodecs streamMusicCodec, - final SourceCodecs downloadMusicCodec, - final bool discordPresence, - final bool endlessPlayback, - final bool enableConnect}) = _$UserPreferencesImpl; - - factory _UserPreferences.fromJson(Map json) = - _$UserPreferencesImpl.fromJson; - - @override - SourceQualities get audioQuality; - @override - bool get albumColorSync; - @override - bool get amoledDarkTheme; - @override - bool get checkUpdate; - @override - bool get normalizeAudio; - @override - bool get showSystemTrayIcon; - @override - bool get skipNonMusic; - @override - bool get systemTitleBar; - @override - CloseBehavior get closeBehavior; - @override - @JsonKey( - fromJson: UserPreferences._accentColorSchemeFromJson, - toJson: UserPreferences._accentColorSchemeToJson, - readValue: UserPreferences._accentColorSchemeReadValue) - SpotubeColor get accentColorScheme; - @override - LayoutMode get layoutMode; - @override - @JsonKey( - fromJson: UserPreferences._localeFromJson, - toJson: UserPreferences._localeToJson, - readValue: UserPreferences._localeReadValue) - Locale get locale; - @override - Market get recommendationMarket; - @override - SearchMode get searchMode; - @override - String get downloadLocation; - @override - List get localLibraryLocation; - @override - String get pipedInstance; - @override - ThemeMode get themeMode; - @override - AudioSource get audioSource; - @override - SourceCodecs get streamMusicCodec; - @override - SourceCodecs get downloadMusicCodec; - @override - bool get discordPresence; - @override - bool get endlessPlayback; - @override - bool get enableConnect; - @override - @JsonKey(ignore: true) - _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart deleted file mode 100644 index 4bcb3a46..00000000 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ /dev/null @@ -1,388 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'user_preferences_state.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$UserPreferencesImpl _$$UserPreferencesImplFromJson(Map json) => - _$UserPreferencesImpl( - audioQuality: - $enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ?? - SourceQualities.high, - albumColorSync: json['albumColorSync'] as bool? ?? true, - amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, - checkUpdate: json['checkUpdate'] as bool? ?? true, - normalizeAudio: json['normalizeAudio'] as bool? ?? false, - showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? false, - skipNonMusic: json['skipNonMusic'] as bool? ?? false, - systemTitleBar: json['systemTitleBar'] as bool? ?? false, - closeBehavior: - $enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ?? - CloseBehavior.close, - accentColorScheme: UserPreferences._accentColorSchemeReadValue( - json, 'accentColorScheme') == - null - ? const SpotubeColor(0xFF2196F3, name: "Blue") - : UserPreferences._accentColorSchemeFromJson( - UserPreferences._accentColorSchemeReadValue( - json, 'accentColorScheme') as Map), - layoutMode: - $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode']) ?? - LayoutMode.adaptive, - locale: UserPreferences._localeReadValue(json, 'locale') == null - ? const Locale("system", "system") - : UserPreferences._localeFromJson( - UserPreferences._localeReadValue(json, 'locale') - as Map), - recommendationMarket: - $enumDecodeNullable(_$MarketEnumMap, json['recommendationMarket']) ?? - Market.US, - searchMode: - $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ?? - SearchMode.youtube, - downloadLocation: json['downloadLocation'] as String? ?? "", - localLibraryLocation: (json['localLibraryLocation'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - pipedInstance: - json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks", - themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? - ThemeMode.system, - audioSource: - $enumDecodeNullable(_$AudioSourceEnumMap, json['audioSource']) ?? - AudioSource.youtube, - streamMusicCodec: $enumDecodeNullable( - _$SourceCodecsEnumMap, json['streamMusicCodec']) ?? - SourceCodecs.weba, - downloadMusicCodec: $enumDecodeNullable( - _$SourceCodecsEnumMap, json['downloadMusicCodec']) ?? - SourceCodecs.m4a, - discordPresence: json['discordPresence'] as bool? ?? true, - endlessPlayback: json['endlessPlayback'] as bool? ?? true, - enableConnect: json['enableConnect'] as bool? ?? false, - ); - -Map _$$UserPreferencesImplToJson( - _$UserPreferencesImpl instance) => - { - 'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!, - 'albumColorSync': instance.albumColorSync, - 'amoledDarkTheme': instance.amoledDarkTheme, - 'checkUpdate': instance.checkUpdate, - 'normalizeAudio': instance.normalizeAudio, - 'showSystemTrayIcon': instance.showSystemTrayIcon, - 'skipNonMusic': instance.skipNonMusic, - 'systemTitleBar': instance.systemTitleBar, - 'closeBehavior': _$CloseBehaviorEnumMap[instance.closeBehavior]!, - 'accentColorScheme': - UserPreferences._accentColorSchemeToJson(instance.accentColorScheme), - 'layoutMode': _$LayoutModeEnumMap[instance.layoutMode]!, - 'locale': UserPreferences._localeToJson(instance.locale), - 'recommendationMarket': _$MarketEnumMap[instance.recommendationMarket]!, - 'searchMode': _$SearchModeEnumMap[instance.searchMode]!, - 'downloadLocation': instance.downloadLocation, - 'localLibraryLocation': instance.localLibraryLocation, - 'pipedInstance': instance.pipedInstance, - 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, - 'audioSource': _$AudioSourceEnumMap[instance.audioSource]!, - 'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!, - 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, - 'discordPresence': instance.discordPresence, - 'endlessPlayback': instance.endlessPlayback, - 'enableConnect': instance.enableConnect, - }; - -const _$SourceQualitiesEnumMap = { - SourceQualities.high: 'high', - SourceQualities.medium: 'medium', - SourceQualities.low: 'low', -}; - -const _$CloseBehaviorEnumMap = { - CloseBehavior.minimizeToTray: 'minimizeToTray', - CloseBehavior.close: 'close', -}; - -const _$LayoutModeEnumMap = { - LayoutMode.compact: 'compact', - LayoutMode.extended: 'extended', - LayoutMode.adaptive: 'adaptive', -}; - -const _$MarketEnumMap = { - Market.AD: 'AD', - Market.AE: 'AE', - Market.AF: 'AF', - Market.AG: 'AG', - Market.AI: 'AI', - Market.AL: 'AL', - Market.AM: 'AM', - Market.AO: 'AO', - Market.AQ: 'AQ', - Market.AR: 'AR', - Market.AS: 'AS', - Market.AT: 'AT', - Market.AU: 'AU', - Market.AW: 'AW', - Market.AX: 'AX', - Market.AZ: 'AZ', - Market.BA: 'BA', - Market.BB: 'BB', - Market.BD: 'BD', - Market.BE: 'BE', - Market.BF: 'BF', - Market.BG: 'BG', - Market.BH: 'BH', - Market.BI: 'BI', - Market.BJ: 'BJ', - Market.BL: 'BL', - Market.BM: 'BM', - Market.BN: 'BN', - Market.BO: 'BO', - Market.BQ: 'BQ', - Market.BR: 'BR', - Market.BS: 'BS', - Market.BT: 'BT', - Market.BV: 'BV', - Market.BW: 'BW', - Market.BY: 'BY', - Market.BZ: 'BZ', - Market.CA: 'CA', - Market.CC: 'CC', - Market.CD: 'CD', - Market.CF: 'CF', - Market.CG: 'CG', - Market.CH: 'CH', - Market.CI: 'CI', - Market.CK: 'CK', - Market.CL: 'CL', - Market.CM: 'CM', - Market.CN: 'CN', - Market.CO: 'CO', - Market.CR: 'CR', - Market.CU: 'CU', - Market.CV: 'CV', - Market.CW: 'CW', - Market.CX: 'CX', - Market.CY: 'CY', - Market.CZ: 'CZ', - Market.DE: 'DE', - Market.DJ: 'DJ', - Market.DK: 'DK', - Market.DM: 'DM', - Market.DO: 'DO', - Market.DZ: 'DZ', - Market.EC: 'EC', - Market.EE: 'EE', - Market.EG: 'EG', - Market.EH: 'EH', - Market.ER: 'ER', - Market.ES: 'ES', - Market.ET: 'ET', - Market.FI: 'FI', - Market.FJ: 'FJ', - Market.FK: 'FK', - Market.FM: 'FM', - Market.FO: 'FO', - Market.FR: 'FR', - Market.GA: 'GA', - Market.GB: 'GB', - Market.GD: 'GD', - Market.GE: 'GE', - Market.GF: 'GF', - Market.GG: 'GG', - Market.GH: 'GH', - Market.GI: 'GI', - Market.GL: 'GL', - Market.GM: 'GM', - Market.GN: 'GN', - Market.GP: 'GP', - Market.GQ: 'GQ', - Market.GR: 'GR', - Market.GS: 'GS', - Market.GT: 'GT', - Market.GU: 'GU', - Market.GW: 'GW', - Market.GY: 'GY', - Market.HK: 'HK', - Market.HM: 'HM', - Market.HN: 'HN', - Market.HR: 'HR', - Market.HT: 'HT', - Market.HU: 'HU', - Market.ID: 'ID', - Market.IE: 'IE', - Market.IL: 'IL', - Market.IM: 'IM', - Market.IN: 'IN', - Market.IO: 'IO', - Market.IQ: 'IQ', - Market.IR: 'IR', - Market.IS: 'IS', - Market.IT: 'IT', - Market.JE: 'JE', - Market.JM: 'JM', - Market.JO: 'JO', - Market.JP: 'JP', - Market.KE: 'KE', - Market.KG: 'KG', - Market.KH: 'KH', - Market.KI: 'KI', - Market.KM: 'KM', - Market.KN: 'KN', - Market.KP: 'KP', - Market.KR: 'KR', - Market.KW: 'KW', - Market.KY: 'KY', - Market.KZ: 'KZ', - Market.LA: 'LA', - Market.LB: 'LB', - Market.LC: 'LC', - Market.LI: 'LI', - Market.LK: 'LK', - Market.LR: 'LR', - Market.LS: 'LS', - Market.LT: 'LT', - Market.LU: 'LU', - Market.LV: 'LV', - Market.LY: 'LY', - Market.MA: 'MA', - Market.MC: 'MC', - Market.MD: 'MD', - Market.ME: 'ME', - Market.MF: 'MF', - Market.MG: 'MG', - Market.MH: 'MH', - Market.MK: 'MK', - Market.ML: 'ML', - Market.MM: 'MM', - Market.MN: 'MN', - Market.MO: 'MO', - Market.MP: 'MP', - Market.MQ: 'MQ', - Market.MR: 'MR', - Market.MS: 'MS', - Market.MT: 'MT', - Market.MU: 'MU', - Market.MV: 'MV', - Market.MW: 'MW', - Market.MX: 'MX', - Market.MY: 'MY', - Market.MZ: 'MZ', - Market.NA: 'NA', - Market.NC: 'NC', - Market.NE: 'NE', - Market.NF: 'NF', - Market.NG: 'NG', - Market.NI: 'NI', - Market.NL: 'NL', - Market.NO: 'NO', - Market.NP: 'NP', - Market.NR: 'NR', - Market.NU: 'NU', - Market.NZ: 'NZ', - Market.OM: 'OM', - Market.PA: 'PA', - Market.PE: 'PE', - Market.PF: 'PF', - Market.PG: 'PG', - Market.PH: 'PH', - Market.PK: 'PK', - Market.PL: 'PL', - Market.PM: 'PM', - Market.PN: 'PN', - Market.PR: 'PR', - Market.PS: 'PS', - Market.PT: 'PT', - Market.PW: 'PW', - Market.PY: 'PY', - Market.QA: 'QA', - Market.RE: 'RE', - Market.RO: 'RO', - Market.RS: 'RS', - Market.RU: 'RU', - Market.RW: 'RW', - Market.SA: 'SA', - Market.SB: 'SB', - Market.SC: 'SC', - Market.SD: 'SD', - Market.SE: 'SE', - Market.SG: 'SG', - Market.SH: 'SH', - Market.SI: 'SI', - Market.SJ: 'SJ', - Market.SK: 'SK', - Market.SL: 'SL', - Market.SM: 'SM', - Market.SN: 'SN', - Market.SO: 'SO', - Market.SR: 'SR', - Market.SS: 'SS', - Market.ST: 'ST', - Market.SV: 'SV', - Market.SX: 'SX', - Market.SY: 'SY', - Market.SZ: 'SZ', - Market.TC: 'TC', - Market.TD: 'TD', - Market.TF: 'TF', - Market.TG: 'TG', - Market.TH: 'TH', - Market.TJ: 'TJ', - Market.TK: 'TK', - Market.TL: 'TL', - Market.TM: 'TM', - Market.TN: 'TN', - Market.TO: 'TO', - Market.TR: 'TR', - Market.TT: 'TT', - Market.TV: 'TV', - Market.TW: 'TW', - Market.TZ: 'TZ', - Market.UA: 'UA', - Market.UG: 'UG', - Market.UM: 'UM', - Market.US: 'US', - Market.UY: 'UY', - Market.UZ: 'UZ', - Market.VA: 'VA', - Market.VC: 'VC', - Market.VE: 'VE', - Market.VG: 'VG', - Market.VI: 'VI', - Market.VN: 'VN', - Market.VU: 'VU', - Market.WF: 'WF', - Market.WS: 'WS', - Market.XK: 'XK', - Market.YE: 'YE', - Market.YT: 'YT', - Market.ZA: 'ZA', - Market.ZM: 'ZM', - Market.ZW: 'ZW', -}; - -const _$SearchModeEnumMap = { - SearchMode.youtube: 'youtube', - SearchMode.youtubeMusic: 'youtubeMusic', -}; - -const _$ThemeModeEnumMap = { - ThemeMode.system: 'system', - ThemeMode.light: 'light', - ThemeMode.dark: 'dark', -}; - -const _$AudioSourceEnumMap = { - AudioSource.youtube: 'youtube', - AudioSource.piped: 'piped', - AudioSource.jiosaavn: 'jiosaavn', -}; - -const _$SourceCodecsEnumMap = { - SourceCodecs.m4a: 'm4a', - SourceCodecs.weba: 'weba', -}; diff --git a/lib/services/sourced_track/models/video_info.dart b/lib/services/sourced_track/models/video_info.dart index 031a8943..58dd0280 100644 --- a/lib/services/sourced_track/models/video_info.dart +++ b/lib/services/sourced_track/models/video_info.dart @@ -1,5 +1,6 @@ import 'package:piped_client/piped_client.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/models/database/database.dart'; + import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class YoutubeVideoInfo { diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 7eedfad8..977b980b 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -5,8 +5,9 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 8444db53..b6689f6a 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -2,9 +2,10 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; + import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e22c5732..b8e26367 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) system_theme_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin"); system_theme_plugin_register_with_registrar(system_theme_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 9ddc2b98..20d4a4dd 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST local_notifier media_kit_libs_linux screen_retriever + sqlite3_flutter_libs system_theme system_tray tray_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 047e7f3d..54546705 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -20,6 +20,7 @@ import path_provider_foundation import screen_retriever import shared_preferences_foundation import sqflite +import sqlite3_flutter_libs import system_theme import system_tray import tray_manager @@ -42,6 +43,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin")) SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index fcba2934..58d09cd9 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -39,6 +39,21 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS + - sqlite3 (3.46.0): + - sqlite3/common (= 3.46.0) + - sqlite3/common (3.46.0) + - sqlite3/fts5 (3.46.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.46.0): + - sqlite3/common + - sqlite3/rtree (3.46.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - sqlite3 (~> 3.46.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - system_theme (0.0.1): - FlutterMacOS - system_tray (0.0.1): @@ -69,6 +84,7 @@ DEPENDENCIES: - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) - system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`) - system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) @@ -78,6 +94,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - OrderedSet + - sqlite3 EXTERNAL SOURCES: app_links: @@ -116,6 +133,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos system_theme: :path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos system_tray: @@ -147,6 +166,8 @@ SPEC CHECKSUMS: screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 154b084339ede06960a5b3c8160066adc9176b7d + sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83 system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 diff --git a/pubspec.lock b/pubspec.lock index c1866e7d..c5871a2a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -507,6 +515,22 @@ packages: url: "https://github.com/thielepaul/flutter-draggable-scrollbar.git" source: git version: "0.1.0" + drift: + dependency: "direct main" + description: + name: drift + sha256: "6acedc562ffeed308049f78fb1906abad3d65714580b6745441ee6d50ec564cd" + url: "https://pub.dev" + source: hosted + version: "2.18.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: d9b020736ea85fff1568699ce18b89fabb3f0f042e8a7a05e84a3ec20d39acde + url: "https://pub.dev" + source: hosted + version: "2.18.0" duration: dependency: "direct main" description: @@ -1997,6 +2021,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.4" + sqlite3: + dependency: "direct main" + description: + name: sqlite3 + sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "9f89a7e7dc36eac2035808427eba1c3fbd79e59c3a22093d8dace6d36b1fe89e" + url: "https://pub.dev" + source: hosted + version: "0.5.23" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: ade9a67fd70d0369329ed3373208de7ebd8662470e8c396fc8d0d60f9acdfc9f + url: "https://pub.dev" + source: hosted + version: "0.36.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bd1717c8..ddace46e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -121,6 +121,9 @@ dependencies: tray_manager: ^0.2.2 http: ^1.2.1 riverpod: ^2.5.1 + drift: ^2.18.0 + sqlite3_flutter_libs: ^0.5.23 + sqlite3: ^2.4.3 dev_dependencies: build_runner: ^2.4.11 @@ -143,6 +146,7 @@ dev_dependencies: pub_api_client: ^2.4.0 xml: ^6.5.0 io: ^1.0.4 + drift_dev: ^2.18.0 dependency_overrides: uuid: ^4.4.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 559db310..b978edb9 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); SystemThemePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemThemePlugin")); SystemTrayPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d1464df5..4fcc467a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_windows_audio permission_handler_windows screen_retriever + sqlite3_flutter_libs system_theme system_tray tray_manager From 52d4f60ccc59dcbe7b1229dd4a7c8a2226c3b6b0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Jun 2024 21:24:42 +0600 Subject: [PATCH 02/24] refactor: use drift for skip segments and source matches --- lib/main.dart | 19 - lib/models/database/database.dart | 5 +- lib/models/database/database.g.dart | 878 +++++++++++++++++- lib/models/database/tables/skip_segment.dart | 9 + lib/models/database/tables/source_match.dart | 25 + lib/models/skip_segment.dart | 25 - lib/models/skip_segment.g.dart | 44 - lib/models/source_match.dart | 54 -- lib/models/source_match.g.dart | 119 --- .../proxy_playlist/skip_segments.dart | 50 +- .../sourced_track/sources/jiosaavn.dart | 42 +- lib/services/sourced_track/sources/piped.dart | 47 +- .../sourced_track/sources/youtube.dart | 42 +- lib/utils/service_utils.dart | 11 +- 14 files changed, 1019 insertions(+), 351 deletions(-) create mode 100644 lib/models/database/tables/skip_segment.dart create mode 100644 lib/models/database/tables/source_match.dart delete mode 100644 lib/models/skip_segment.dart delete mode 100644 lib/models/skip_segment.g.dart delete mode 100644 lib/models/source_match.dart delete mode 100644 lib/models/source_match.g.dart diff --git a/lib/main.dart b/lib/main.dart index 1f5e5909..bdccadd4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,8 +22,6 @@ import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; import 'package:spotube/l10n/l10n.dart'; -import 'package:spotube/models/skip_segment.dart'; -import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -84,23 +82,6 @@ Future main(List rawArgs) async { Hive.init(hiveCacheDir); - Hive.registerAdapter(SkipSegmentAdapter()); - - Hive.registerAdapter(SourceMatchAdapter()); - Hive.registerAdapter(SourceTypeAdapter()); - - // Cache versioning entities with Adapter - SourceMatch.version = 'v1'; - SkipSegment.version = 'v1'; - - await Hive.openLazyBox( - SourceMatch.boxName, - path: hiveCacheDir, - ); - await Hive.openLazyBox( - SkipSegment.boxName, - path: hiveCacheDir, - ); await PersistedStateNotifier.initializeBoxes( path: hiveCacheDir, ); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 7d8fe088..e7ac2558 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -17,11 +17,14 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; part 'database.g.dart'; part 'tables/preferences.dart'; +part 'tables/source_match.dart'; +part 'tables/skip_segment.dart'; + part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; -@DriftDatabase(tables: [PreferencesTable]) +@DriftDatabase(tables: [PreferencesTable, SourceMatchTable, SkipSegmentTable]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 1516b266..9cc8a1c1 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1204,16 +1204,608 @@ class PreferencesTableCompanion extends UpdateCompanion { } } +class $SourceMatchTableTable extends SourceMatchTable + with TableInfo<$SourceMatchTableTable, SourceMatchTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SourceMatchTableTable(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 _sourceIdMeta = + const VerificationMeta('sourceId'); + @override + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _sourceTypeMeta = + const VerificationMeta('sourceType'); + @override + late final GeneratedColumnWithTypeConverter sourceType = + GeneratedColumn('source_type', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(SourceType.youtube.name)) + .withConverter( + $SourceMatchTableTable.$convertersourceType); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, trackId, sourceId, sourceType, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'source_match_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); + } + if (data.containsKey('source_id')) { + context.handle(_sourceIdMeta, + sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta)); + } else if (isInserting) { + context.missing(_sourceIdMeta); + } + context.handle(_sourceTypeMeta, const VerificationResult.success()); + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SourceMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SourceMatchTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + sourceId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}source_id'])!, + sourceType: $SourceMatchTableTable.$convertersourceType.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}source_type'])!), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SourceMatchTableTable createAlias(String alias) { + return $SourceMatchTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertersourceType = + const EnumNameConverter(SourceType.values); +} + +class SourceMatchTableData extends DataClass + implements Insertable { + final int id; + final String trackId; + final String sourceId; + final SourceType sourceType; + final DateTime createdAt; + const SourceMatchTableData( + {required this.id, + required this.trackId, + required this.sourceId, + required this.sourceType, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['track_id'] = Variable(trackId); + map['source_id'] = Variable(sourceId); + { + map['source_type'] = Variable( + $SourceMatchTableTable.$convertersourceType.toSql(sourceType)); + } + map['created_at'] = Variable(createdAt); + return map; + } + + SourceMatchTableCompanion toCompanion(bool nullToAbsent) { + return SourceMatchTableCompanion( + id: Value(id), + trackId: Value(trackId), + sourceId: Value(sourceId), + sourceType: Value(sourceType), + createdAt: Value(createdAt), + ); + } + + factory SourceMatchTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SourceMatchTableData( + id: serializer.fromJson(json['id']), + trackId: serializer.fromJson(json['trackId']), + sourceId: serializer.fromJson(json['sourceId']), + sourceType: $SourceMatchTableTable.$convertersourceType + .fromJson(serializer.fromJson(json['sourceType'])), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'trackId': serializer.toJson(trackId), + 'sourceId': serializer.toJson(sourceId), + 'sourceType': serializer.toJson( + $SourceMatchTableTable.$convertersourceType.toJson(sourceType)), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SourceMatchTableData copyWith( + {int? id, + String? trackId, + String? sourceId, + SourceType? sourceType, + DateTime? createdAt}) => + SourceMatchTableData( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + @override + String toString() { + return (StringBuffer('SourceMatchTableData(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, trackId, sourceId, sourceType, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SourceMatchTableData && + other.id == this.id && + other.trackId == this.trackId && + other.sourceId == this.sourceId && + other.sourceType == this.sourceType && + other.createdAt == this.createdAt); +} + +class SourceMatchTableCompanion extends UpdateCompanion { + final Value id; + final Value trackId; + final Value sourceId; + final Value sourceType; + final Value createdAt; + const SourceMatchTableCompanion({ + this.id = const Value.absent(), + this.trackId = const Value.absent(), + this.sourceId = const Value.absent(), + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SourceMatchTableCompanion.insert({ + this.id = const Value.absent(), + required String trackId, + required String sourceId, + this.sourceType = const Value.absent(), + this.createdAt = const Value.absent(), + }) : trackId = Value(trackId), + sourceId = Value(sourceId); + static Insertable custom({ + Expression? id, + Expression? trackId, + Expression? sourceId, + Expression? sourceType, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (trackId != null) 'track_id': trackId, + if (sourceId != null) 'source_id': sourceId, + if (sourceType != null) 'source_type': sourceType, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SourceMatchTableCompanion copyWith( + {Value? id, + Value? trackId, + Value? sourceId, + Value? sourceType, + Value? createdAt}) { + return SourceMatchTableCompanion( + id: id ?? this.id, + trackId: trackId ?? this.trackId, + sourceId: sourceId ?? this.sourceId, + sourceType: sourceType ?? this.sourceType, + createdAt: createdAt ?? this.createdAt, + ); + } + + @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 (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (sourceType.present) { + map['source_type'] = Variable( + $SourceMatchTableTable.$convertersourceType.toSql(sourceType.value)); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SourceMatchTableCompanion(') + ..write('id: $id, ') + ..write('trackId: $trackId, ') + ..write('sourceId: $sourceId, ') + ..write('sourceType: $sourceType, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $SkipSegmentTableTable extends SkipSegmentTable + with TableInfo<$SkipSegmentTableTable, SkipSegmentTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SkipSegmentTableTable(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 _startMeta = const VerificationMeta('start'); + @override + late final GeneratedColumn start = GeneratedColumn( + 'start', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _endMeta = const VerificationMeta('end'); + @override + late final GeneratedColumn end = GeneratedColumn( + 'end', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + 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 _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [id, start, end, trackId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'skip_segment_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('start')) { + context.handle( + _startMeta, start.isAcceptableOrUnknown(data['start']!, _startMeta)); + } else if (isInserting) { + context.missing(_startMeta); + } + if (data.containsKey('end')) { + context.handle( + _endMeta, end.isAcceptableOrUnknown(data['end']!, _endMeta)); + } else if (isInserting) { + context.missing(_endMeta); + } + if (data.containsKey('track_id')) { + context.handle(_trackIdMeta, + trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); + } else if (isInserting) { + context.missing(_trackIdMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SkipSegmentTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SkipSegmentTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + start: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}start'])!, + end: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}end'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SkipSegmentTableTable createAlias(String alias) { + return $SkipSegmentTableTable(attachedDatabase, alias); + } +} + +class SkipSegmentTableData extends DataClass + implements Insertable { + final int id; + final int start; + final int end; + final String trackId; + final DateTime createdAt; + const SkipSegmentTableData( + {required this.id, + required this.start, + required this.end, + required this.trackId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['start'] = Variable(start); + map['end'] = Variable(end); + map['track_id'] = Variable(trackId); + map['created_at'] = Variable(createdAt); + return map; + } + + SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { + return SkipSegmentTableCompanion( + id: Value(id), + start: Value(start), + end: Value(end), + trackId: Value(trackId), + createdAt: Value(createdAt), + ); + } + + factory SkipSegmentTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SkipSegmentTableData( + id: serializer.fromJson(json['id']), + start: serializer.fromJson(json['start']), + end: serializer.fromJson(json['end']), + trackId: serializer.fromJson(json['trackId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'start': serializer.toJson(start), + 'end': serializer.toJson(end), + 'trackId': serializer.toJson(trackId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SkipSegmentTableData copyWith( + {int? id, + int? start, + int? end, + String? trackId, + DateTime? createdAt}) => + SkipSegmentTableData( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + @override + String toString() { + return (StringBuffer('SkipSegmentTableData(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, start, end, trackId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SkipSegmentTableData && + other.id == this.id && + other.start == this.start && + other.end == this.end && + other.trackId == this.trackId && + other.createdAt == this.createdAt); +} + +class SkipSegmentTableCompanion extends UpdateCompanion { + final Value id; + final Value start; + final Value end; + final Value trackId; + final Value createdAt; + const SkipSegmentTableCompanion({ + this.id = const Value.absent(), + this.start = const Value.absent(), + this.end = const Value.absent(), + this.trackId = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SkipSegmentTableCompanion.insert({ + this.id = const Value.absent(), + required int start, + required int end, + required String trackId, + this.createdAt = const Value.absent(), + }) : start = Value(start), + end = Value(end), + trackId = Value(trackId); + static Insertable custom({ + Expression? id, + Expression? start, + Expression? end, + Expression? trackId, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (start != null) 'start': start, + if (end != null) 'end': end, + if (trackId != null) 'track_id': trackId, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SkipSegmentTableCompanion copyWith( + {Value? id, + Value? start, + Value? end, + Value? trackId, + Value? createdAt}) { + return SkipSegmentTableCompanion( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (start.present) { + map['start'] = Variable(start.value); + } + if (end.present) { + map['end'] = Variable(end.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableCompanion(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); late final $PreferencesTableTable preferencesTable = $PreferencesTableTable(this); + late final $SourceMatchTableTable sourceMatchTable = + $SourceMatchTableTable(this); + late final $SkipSegmentTableTable skipSegmentTable = + $SkipSegmentTableTable(this); + late final Index uniqTrackMatch = Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [preferencesTable]; + List get allSchemaEntities => + [preferencesTable, sourceMatchTable, skipSegmentTable, uniqTrackMatch]; } typedef $$PreferencesTableTableInsertCompanionBuilder @@ -1699,9 +2291,293 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$SourceMatchTableTableInsertCompanionBuilder + = SourceMatchTableCompanion Function({ + Value id, + required String trackId, + required String sourceId, + Value sourceType, + Value createdAt, +}); +typedef $$SourceMatchTableTableUpdateCompanionBuilder + = SourceMatchTableCompanion Function({ + Value id, + Value trackId, + Value sourceId, + Value sourceType, + Value createdAt, +}); + +class $$SourceMatchTableTableTableManager extends RootTableManager< + _$AppDatabase, + $SourceMatchTableTable, + SourceMatchTableData, + $$SourceMatchTableTableFilterComposer, + $$SourceMatchTableTableOrderingComposer, + $$SourceMatchTableTableProcessedTableManager, + $$SourceMatchTableTableInsertCompanionBuilder, + $$SourceMatchTableTableUpdateCompanionBuilder> { + $$SourceMatchTableTableTableManager( + _$AppDatabase db, $SourceMatchTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$SourceMatchTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$SourceMatchTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$SourceMatchTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value trackId = const Value.absent(), + Value sourceId = const Value.absent(), + Value sourceType = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SourceMatchTableCompanion( + id: id, + trackId: trackId, + sourceId: sourceId, + sourceType: sourceType, + createdAt: createdAt, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required String trackId, + required String sourceId, + Value sourceType = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SourceMatchTableCompanion.insert( + id: id, + trackId: trackId, + sourceId: sourceId, + sourceType: sourceType, + createdAt: createdAt, + ), + )); +} + +class $$SourceMatchTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $SourceMatchTableTable, + SourceMatchTableData, + $$SourceMatchTableTableFilterComposer, + $$SourceMatchTableTableOrderingComposer, + $$SourceMatchTableTableProcessedTableManager, + $$SourceMatchTableTableInsertCompanionBuilder, + $$SourceMatchTableTableUpdateCompanionBuilder> { + $$SourceMatchTableTableProcessedTableManager(super.$state); +} + +class $$SourceMatchTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $SourceMatchTableTable> { + $$SourceMatchTableTableFilterComposer(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)); + + ColumnFilters get sourceId => $state.composableBuilder( + column: $state.table.sourceId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get sourceType => $state.composableBuilder( + column: $state.table.sourceType, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$SourceMatchTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $SourceMatchTableTable> { + $$SourceMatchTableTableOrderingComposer(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 sourceId => $state.composableBuilder( + column: $state.table.sourceId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get sourceType => $state.composableBuilder( + column: $state.table.sourceType, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +typedef $$SkipSegmentTableTableInsertCompanionBuilder + = SkipSegmentTableCompanion Function({ + Value id, + required int start, + required int end, + required String trackId, + Value createdAt, +}); +typedef $$SkipSegmentTableTableUpdateCompanionBuilder + = SkipSegmentTableCompanion Function({ + Value id, + Value start, + Value end, + Value trackId, + Value createdAt, +}); + +class $$SkipSegmentTableTableTableManager extends RootTableManager< + _$AppDatabase, + $SkipSegmentTableTable, + SkipSegmentTableData, + $$SkipSegmentTableTableFilterComposer, + $$SkipSegmentTableTableOrderingComposer, + $$SkipSegmentTableTableProcessedTableManager, + $$SkipSegmentTableTableInsertCompanionBuilder, + $$SkipSegmentTableTableUpdateCompanionBuilder> { + $$SkipSegmentTableTableTableManager( + _$AppDatabase db, $SkipSegmentTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$SkipSegmentTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$SkipSegmentTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$SkipSegmentTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value start = const Value.absent(), + Value end = const Value.absent(), + Value trackId = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SkipSegmentTableCompanion( + id: id, + start: start, + end: end, + trackId: trackId, + createdAt: createdAt, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required int start, + required int end, + required String trackId, + Value createdAt = const Value.absent(), + }) => + SkipSegmentTableCompanion.insert( + id: id, + start: start, + end: end, + trackId: trackId, + createdAt: createdAt, + ), + )); +} + +class $$SkipSegmentTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $SkipSegmentTableTable, + SkipSegmentTableData, + $$SkipSegmentTableTableFilterComposer, + $$SkipSegmentTableTableOrderingComposer, + $$SkipSegmentTableTableProcessedTableManager, + $$SkipSegmentTableTableInsertCompanionBuilder, + $$SkipSegmentTableTableUpdateCompanionBuilder> { + $$SkipSegmentTableTableProcessedTableManager(super.$state); +} + +class $$SkipSegmentTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $SkipSegmentTableTable> { + $$SkipSegmentTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get start => $state.composableBuilder( + column: $state.table.start, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get end => $state.composableBuilder( + column: $state.table.end, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$SkipSegmentTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $SkipSegmentTableTable> { + $$SkipSegmentTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get start => $state.composableBuilder( + column: $state.table.start, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get end => $state.composableBuilder( + column: $state.table.end, + 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 createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); $$PreferencesTableTableTableManager get preferencesTable => $$PreferencesTableTableTableManager(_db, _db.preferencesTable); + $$SourceMatchTableTableTableManager get sourceMatchTable => + $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); + $$SkipSegmentTableTableTableManager get skipSegmentTable => + $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); } diff --git a/lib/models/database/tables/skip_segment.dart b/lib/models/database/tables/skip_segment.dart new file mode 100644 index 00000000..719f2617 --- /dev/null +++ b/lib/models/database/tables/skip_segment.dart @@ -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)(); +} diff --git a/lib/models/database/tables/source_match.dart b/lib/models/database/tables/source_match.dart new file mode 100644 index 00000000..78d0eb05 --- /dev/null +++ b/lib/models/database/tables/source_match.dart @@ -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().withDefault(Constant(SourceType.youtube.name))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); +} diff --git a/lib/models/skip_segment.dart b/lib/models/skip_segment.dart deleted file mode 100644 index 90f20f5a..00000000 --- a/lib/models/skip_segment.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:hive/hive.dart'; - -part 'skip_segment.g.dart'; - -@HiveType(typeId: 2) -class SkipSegment { - @HiveField(0) - final int start; - @HiveField(1) - final int end; - SkipSegment(this.start, this.end); - - static String version = 'v1'; - static final boxName = "oss.krtirtho.spotube.skip_segments.$version"; - static LazyBox get box => Hive.lazyBox(boxName); - - SkipSegment.fromJson(Map json) - : start = json['start'], - end = json['end']; - - Map toJson() => { - 'start': start, - 'end': end, - }; -} diff --git a/lib/models/skip_segment.g.dart b/lib/models/skip_segment.g.dart deleted file mode 100644 index f2ad4459..00000000 --- a/lib/models/skip_segment.g.dart +++ /dev/null @@ -1,44 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'skip_segment.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class SkipSegmentAdapter extends TypeAdapter { - @override - final int typeId = 2; - - @override - SkipSegment read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SkipSegment( - fields[0] as int, - fields[1] as int, - ); - } - - @override - void write(BinaryWriter writer, SkipSegment obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.start) - ..writeByte(1) - ..write(obj.end); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SkipSegmentAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/models/source_match.dart b/lib/models/source_match.dart deleted file mode 100644 index 57a9f963..00000000 --- a/lib/models/source_match.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'source_match.g.dart'; - -@JsonEnum() -@HiveType(typeId: 5) -enum SourceType { - @HiveField(0) - youtube._("YouTube"), - - @HiveField(1) - youtubeMusic._("YouTube Music"), - - @HiveField(2) - jiosaavn._("JioSaavn"); - - final String label; - - const SourceType._(this.label); -} - -@JsonSerializable() -@HiveType(typeId: 6) -class SourceMatch { - @HiveField(0) - String id; - - @HiveField(1) - String sourceId; - - @HiveField(2) - SourceType sourceType; - - @HiveField(3) - DateTime createdAt; - - SourceMatch({ - required this.id, - required this.sourceId, - required this.sourceType, - required this.createdAt, - }); - - factory SourceMatch.fromJson(Map json) => - _$SourceMatchFromJson(json); - - Map toJson() => _$SourceMatchToJson(this); - - static String version = 'v1'; - static final boxName = "oss.krtirtho.spotube.source_matches.$version"; - - static LazyBox get box => Hive.lazyBox(boxName); -} diff --git a/lib/models/source_match.g.dart b/lib/models/source_match.g.dart deleted file mode 100644 index 3b469694..00000000 --- a/lib/models/source_match.g.dart +++ /dev/null @@ -1,119 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'source_match.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class SourceMatchAdapter extends TypeAdapter { - @override - final int typeId = 6; - - @override - SourceMatch read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SourceMatch( - id: fields[0] as String, - sourceId: fields[1] as String, - sourceType: fields[2] as SourceType, - createdAt: fields[3] as DateTime, - ); - } - - @override - void write(BinaryWriter writer, SourceMatch obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.sourceId) - ..writeByte(2) - ..write(obj.sourceType) - ..writeByte(3) - ..write(obj.createdAt); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SourceMatchAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SourceTypeAdapter extends TypeAdapter { - @override - final int typeId = 5; - - @override - SourceType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return SourceType.youtube; - case 1: - return SourceType.youtubeMusic; - case 2: - return SourceType.jiosaavn; - default: - return SourceType.youtube; - } - } - - @override - void write(BinaryWriter writer, SourceType obj) { - switch (obj) { - case SourceType.youtube: - writer.writeByte(0); - break; - case SourceType.youtubeMusic: - writer.writeByte(1); - break; - case SourceType.jiosaavn: - writer.writeByte(2); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SourceTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch( - id: json['id'] as String, - sourceId: json['sourceId'] as String, - sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']), - createdAt: DateTime.parse(json['createdAt'] as String), - ); - -Map _$SourceMatchToJson(SourceMatch instance) => - { - 'id': instance.id, - 'sourceId': instance.sourceId, - 'sourceType': _$SourceTypeEnumMap[instance.sourceType]!, - 'createdAt': instance.createdAt.toIso8601String(), - }; - -const _$SourceTypeEnumMap = { - SourceType.youtube: 'youtube', - SourceType.youtubeMusic: 'youtubeMusic', - SourceType.jiosaavn: 'jiosaavn', -}; diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/proxy_playlist/skip_segments.dart index 461ac24e..005797f4 100644 --- a/lib/provider/proxy_playlist/skip_segments.dart +++ b/lib/provider/proxy_playlist/skip_segments.dart @@ -1,8 +1,8 @@ import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/models/skip_segment.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -10,24 +10,21 @@ import 'package:spotube/services/dio/dio.dart'; class SourcedSegments { final String source; - final List segments; + final List segments; SourcedSegments({required this.source, required this.segments}); } -Future> getAndCacheSkipSegments(String id) async { +Future> getAndCacheSkipSegments( + String id, Ref ref) async { + final database = ref.read(databaseProvider); try { - final cached = await SkipSegment.box.get(id) as List?; - if (cached != null && cached.isNotEmpty) { - return List.castFrom( - cached - .map( - (json) => SkipSegment.fromJson( - Map.castFrom(json), - ), - ) - .toList(), - ); + final cached = await (database.select(database.skipSegmentTable) + ..where((s) => s.trackId.equals(id))) + .get(); + + if (cached.isNotEmpty) { + return cached; } final res = await globalDio.getUri( @@ -55,25 +52,30 @@ Future> getAndCacheSkipSegments(String id) async { ); if (res.data == "Not Found") { - return List.castFrom([]); + return List.castFrom([]); } final data = res.data as List; final segments = data.map((obj) { final start = obj["segment"].first.toInt(); final end = obj["segment"].last.toInt(); - return SkipSegment(start, end); + return SkipSegmentTableCompanion.insert( + trackId: id, + start: start, + end: end, + ); }).toList(); - await SkipSegment.box.put( - id, - segments.map((e) => e.toJson()).toList(), - ); - return List.castFrom(segments); + await database.batch((b) { + b.insertAll(database.skipSegmentTable, segments); + }); + + return await (database.select(database.skipSegmentTable) + ..where((s) => s.trackId.equals(id))) + .get(); } catch (e, stack) { - await SkipSegment.box.put(id, []); AppLogger.reportError(e, stack); - return List.castFrom([]); + return List.castFrom([]); } } @@ -100,7 +102,7 @@ final segmentProvider = FutureProvider( ); } - final segments = await getAndCacheSkipSegments(track.sourceInfo.id); + final segments = await getAndCacheSkipSegments(track.sourceInfo.id, ref); return SourcedSegments( source: track.sourceInfo.id, diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index f731de6c..865e3d63 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/source_match.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; @@ -39,7 +41,10 @@ class JioSaavnSourcedTrack extends SourcedTrack { required Ref ref, bool weakMatch = false, }) async { - final cachedSource = await SourceMatch.box.get(track.id); + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => s.trackId.equals(track.id!))) + .getSingleOrNull(); if (cachedSource == null || cachedSource.sourceType != SourceType.jiosaavn) { @@ -50,15 +55,13 @@ class JioSaavnSourcedTrack extends SourcedTrack { throw TrackNotFoundError(track); } - await SourceMatch.box.put( - track.id!, - SourceMatch( - id: track.id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: siblings.first.info.id, - ), - ); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: track.id!, + sourceId: siblings.first.info.id, + sourceType: const Value(SourceType.jiosaavn), + ), + ); return JioSaavnSourcedTrack( ref: ref, @@ -206,15 +209,14 @@ class JioSaavnSourcedTrack extends SourcedTrack { final (:info, :source) = toSiblingType(item); - await SourceMatch.box.put( - id!, - SourceMatch( - id: id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: info.id, - ), - ); + final database = ref.read(databaseProvider); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: id!, + sourceId: info.id, + sourceType: const Value(SourceType.jiosaavn), + ), + ); return JioSaavnSourcedTrack( ref: ref, diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index b6689f6a..d156b26e 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -1,9 +1,10 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:piped_client/piped_client.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; -import 'package:spotube/models/source_match.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -49,7 +50,10 @@ class PipedSourcedTrack extends SourcedTrack { required Track track, required Ref ref, }) async { - final cachedSource = await SourceMatch.box.get(track.id); + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => s.trackId.equals(track.id!))) + .getSingleOrNull(); final preferences = ref.read(userPreferencesProvider); final pipedClient = ref.read(pipedProvider); @@ -59,17 +63,17 @@ class PipedSourcedTrack extends SourcedTrack { throw TrackNotFoundError(track); } - await SourceMatch.box.put( - track.id!, - SourceMatch( - id: track.id!, - sourceType: preferences.searchMode == SearchMode.youtube - ? SourceType.youtube - : SourceType.youtubeMusic, - createdAt: DateTime.now(), - sourceId: siblings.first.info.id, - ), - ); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: track.id!, + sourceId: siblings.first.info.id, + sourceType: Value( + preferences.searchMode == SearchMode.youtube + ? SourceType.youtube + : SourceType.youtubeMusic, + ), + ), + ); return PipedSourcedTrack( ref: ref, @@ -268,15 +272,14 @@ class PipedSourcedTrack extends SourcedTrack { final manifest = await pipedClient.streams(newSourceInfo.id); - await SourceMatch.box.put( - id!, - SourceMatch( - id: id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: newSourceInfo.id, - ), - ); + final database = ref.read(databaseProvider); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: id!, + sourceId: newSourceInfo.id, + sourceType: const Value(SourceType.youtube), + ), + ); return PipedSourcedTrack( ref: ref, diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index b144d701..0501a499 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -1,8 +1,10 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/source_match.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/song_link/song_link.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -46,7 +48,10 @@ class YoutubeSourcedTrack extends SourcedTrack { required Track track, required Ref ref, }) async { - final cachedSource = await SourceMatch.box.get(track.id); + final database = ref.read(databaseProvider); + final cachedSource = await (database.select(database.sourceMatchTable) + ..where((s) => s.trackId.equals(track.id!))) + .getSingleOrNull(); if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) { final siblings = await fetchSiblings(ref: ref, track: track); @@ -54,15 +59,13 @@ class YoutubeSourcedTrack extends SourcedTrack { throw TrackNotFoundError(track); } - await SourceMatch.box.put( - track.id!, - SourceMatch( - id: track.id!, - sourceType: SourceType.youtube, - createdAt: DateTime.now(), - sourceId: siblings.first.info.id, - ), - ); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: track.id!, + sourceId: siblings.first.info.id, + sourceType: const Value(SourceType.youtube), + ), + ); return YoutubeSourcedTrack( ref: ref, @@ -283,15 +286,14 @@ class YoutubeSourcedTrack extends SourcedTrack { onTimeout: () => throw ClientException("Timeout"), ); - await SourceMatch.box.put( - id!, - SourceMatch( - id: id!, - sourceType: SourceType.jiosaavn, - createdAt: DateTime.now(), - sourceId: newSourceInfo.id, - ), - ); + final database = ref.read(databaseProvider); + await database.into(database.sourceMatchTable).insert( + SourceMatchTableCompanion.insert( + trackId: id!, + sourceId: newSourceInfo.id, + sourceType: const Value(SourceType.youtube), + ), + ); return YoutubeSourcedTrack( ref: ref, diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 885f9a2c..5950bc8c 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -6,6 +6,7 @@ import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/modules/root/update_dialog.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/lyrics.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -20,7 +21,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:spotube/collections/env.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:version/version.dart'; abstract class ServiceUtils { @@ -392,7 +392,14 @@ abstract class ServiceUtils { WidgetRef ref, ) async { if (!Env.enableUpdateChecker) return; - if (!ref.read(userPreferencesProvider.select((s) => s.checkUpdate))) return; + final database = ref.read(databaseProvider); + final checkUpdate = await (database.selectOnly(database.preferencesTable) + ..addColumns([database.preferencesTable.checkUpdate]) + ..where(database.preferencesTable.id.equals(0))) + .map((row) => row.read(database.preferencesTable.checkUpdate)) + .getSingleOrNull(); + + if (checkUpdate == false) return; final packageInfo = await PackageInfo.fromPlatform(); if (Env.releaseChannel == ReleaseChannel.nightly) { From bf6cec8d6939bd373f6015af08d8ab34a6e631cd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Jun 2024 22:23:12 +0600 Subject: [PATCH 03/24] refactor(blacklist): use drift sql db instead of hive --- lib/components/track_tile/track_options.dart | 29 +- lib/components/track_tile/track_tile.dart | 8 +- lib/models/database/database.dart | 10 +- lib/models/database/database.g.dart | 399 ++++++++++++++++++- lib/models/database/tables/blacklist.dart | 18 + lib/modules/artist/artist_card.dart | 6 +- lib/pages/artist/section/header.dart | 24 +- lib/pages/settings/blacklist.dart | 32 +- lib/provider/blacklist_provider.dart | 108 ++--- 9 files changed, 512 insertions(+), 122 deletions(-) create mode 100644 lib/models/database/tables/blacklist.dart diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 89f6679d..fd3018ba 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -18,6 +18,7 @@ import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; @@ -170,11 +171,8 @@ class TrackOptions extends HookConsumerWidget { final favorites = useTrackToggleLike(track, ref); final isBlackListed = useMemoized( - () => blacklist.contains( - BlacklistedElement.track( - track.id!, - track.name!, - ), + () => blacklist.asData?.value.any( + (element) => element.elementId == track.id, ), [blacklist, track], ); @@ -258,13 +256,16 @@ class TrackOptions extends HookConsumerWidget { .removeTracks(playlistId ?? "", [track.id!]); break; case TrackOptionValue.blacklist: - if (isBlackListed) { - ref.read(blacklistProvider.notifier).remove( - BlacklistedElement.track(track.id!, track.name!), - ); + if (isBlackListed == null) break; + if (isBlackListed == true) { + await ref.read(blacklistProvider.notifier).remove(track.id!); } else { - ref.read(blacklistProvider.notifier).add( - BlacklistedElement.track(track.id!, track.name!), + await ref.read(blacklistProvider.notifier).add( + BlacklistTableCompanion.insert( + name: track.name!, + elementId: track.id!, + elementType: BlacklistedType.track, + ), ); } break; @@ -399,10 +400,10 @@ class TrackOptions extends HookConsumerWidget { PopSheetEntry( value: TrackOptionValue.blacklist, leading: const Icon(SpotubeIcons.playlistRemove), - iconColor: !isBlackListed ? Colors.red[400] : null, - textColor: !isBlackListed ? Colors.red[400] : null, + iconColor: isBlackListed != true ? Colors.red[400] : null, + textColor: isBlackListed != true ? Colors.red[400] : null, title: Text( - isBlackListed + isBlackListed == true ? context.l10n.remove_from_blacklist : context.l10n.add_to_blacklist, ), diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index 9ba87abe..e2e7e293 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -53,14 +53,10 @@ class TrackTile extends HookConsumerWidget { final theme = Theme.of(context); final blacklist = ref.watch(blacklistProvider); + final blacklistNotifier = ref.watch(blacklistProvider.notifier); final isBlackListed = useMemoized( - () => blacklist.contains( - BlacklistedElement.track( - track.id!, - track.name!, - ), - ), + () => blacklistNotifier.contains(track), [blacklist, track], ); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index e7ac2558..ad09933d 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,12 +19,20 @@ part 'database.g.dart'; part 'tables/preferences.dart'; part 'tables/source_match.dart'; part 'tables/skip_segment.dart'; +part 'tables/blacklist.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; -@DriftDatabase(tables: [PreferencesTable, SourceMatchTable, SkipSegmentTable]) +@DriftDatabase( + tables: [ + PreferencesTable, + SourceMatchTable, + SkipSegmentTable, + BlacklistTable, + ], +) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 9cc8a1c1..8c996d21 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1789,6 +1789,266 @@ class SkipSegmentTableCompanion extends UpdateCompanion { } } +class $BlacklistTableTable extends BlacklistTable + with TableInfo<$BlacklistTableTable, BlacklistTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $BlacklistTableTable(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 _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _elementTypeMeta = + const VerificationMeta('elementType'); + @override + late final GeneratedColumnWithTypeConverter + elementType = GeneratedColumn('element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $BlacklistTableTable.$converterelementType); + static const VerificationMeta _elementIdMeta = + const VerificationMeta('elementId'); + @override + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'blacklist_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('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + context.handle(_elementTypeMeta, const VerificationResult.success()); + if (data.containsKey('element_id')) { + context.handle(_elementIdMeta, + elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); + } else if (isInserting) { + context.missing(_elementIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + BlacklistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BlacklistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: $BlacklistTableTable.$converterelementType.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}element_type'])!), + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + ); + } + + @override + $BlacklistTableTable createAlias(String alias) { + return $BlacklistTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converterelementType = + const EnumNameConverter(BlacklistedType.values); +} + +class BlacklistTableData extends DataClass + implements Insertable { + final int id; + final String name; + final BlacklistedType elementType; + final String elementId; + const BlacklistTableData( + {required this.id, + required this.name, + required this.elementType, + required this.elementId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + { + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType)); + } + map['element_id'] = Variable(elementId); + return map; + } + + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( + id: Value(id), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), + ); + } + + factory BlacklistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BlacklistTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + elementType: $BlacklistTableTable.$converterelementType + .fromJson(serializer.fromJson(json['elementType'])), + elementId: serializer.fromJson(json['elementId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson( + $BlacklistTableTable.$converterelementType.toJson(elementType)), + 'elementId': serializer.toJson(elementId), + }; + } + + BlacklistTableData copyWith( + {int? id, + String? name, + BlacklistedType? elementType, + String? elementId}) => + BlacklistTableData( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + @override + String toString() { + return (StringBuffer('BlacklistTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, elementType, elementId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlacklistTableData && + other.id == this.id && + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); +} + +class BlacklistTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), + }); + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? elementType, + Expression? elementId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, + }); + } + + BlacklistTableCompanion copyWith( + {Value? id, + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (elementType.present) { + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType.value)); + } + if (elementId.present) { + map['element_id'] = Variable(elementId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlacklistTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); @@ -1798,14 +2058,23 @@ abstract class _$AppDatabase extends GeneratedDatabase { $SourceMatchTableTable(this); late final $SkipSegmentTableTable skipSegmentTable = $SkipSegmentTableTable(this); + late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); late final Index uniqTrackMatch = Index('uniq_track_match', 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); + late final Index uniqueBlacklist = Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [preferencesTable, sourceMatchTable, skipSegmentTable, uniqTrackMatch]; + List get allSchemaEntities => [ + preferencesTable, + sourceMatchTable, + skipSegmentTable, + blacklistTable, + uniqTrackMatch, + uniqueBlacklist + ]; } typedef $$PreferencesTableTableInsertCompanionBuilder @@ -2571,6 +2840,130 @@ class $$SkipSegmentTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$BlacklistTableTableInsertCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + required String name, + required BlacklistedType elementType, + required String elementId, +}); +typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + Value name, + Value elementType, + Value elementId, +}); + +class $$BlacklistTableTableTableManager extends RootTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableTableManager( + _$AppDatabase db, $BlacklistTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$BlacklistTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$BlacklistTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$BlacklistTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value elementType = const Value.absent(), + Value elementId = const Value.absent(), + }) => + BlacklistTableCompanion( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) => + BlacklistTableCompanion.insert( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + )); +} + +class $$BlacklistTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableProcessedTableManager(super.$state); +} + +class $$BlacklistTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$BlacklistTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); @@ -2580,4 +2973,6 @@ class _$AppDatabaseManager { $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); $$SkipSegmentTableTableTableManager get skipSegmentTable => $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); + $$BlacklistTableTableTableManager get blacklistTable => + $$BlacklistTableTableTableManager(_db, _db.blacklistTable); } diff --git a/lib/models/database/tables/blacklist.dart b/lib/models/database/tables/blacklist.dart new file mode 100644 index 00000000..8a8d9dee --- /dev/null +++ b/lib/models/database/tables/blacklist.dart @@ -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()(); + TextColumn get elementId => text()(); +} diff --git a/lib/modules/artist/artist_card.dart b/lib/modules/artist/artist_card.dart index c1404e42..896271f2 100644 --- a/lib/modules/artist/artist_card.dart +++ b/lib/modules/artist/artist_card.dart @@ -27,8 +27,8 @@ class ArtistCard extends HookConsumerWidget { ); final isBlackListed = ref.watch( blacklistProvider.select( - (blacklist) => blacklist.contains( - BlacklistedElement.artist(artist.id!, artist.name!), + (blacklist) => blacklist.asData?.value.any( + (element) => element.elementId == artist.id, ), ), ); @@ -55,7 +55,7 @@ class ArtistCard extends HookConsumerWidget { elevation: 3, shape: RoundedRectangleBorder( borderRadius: radius, - side: isBlackListed + side: isBlackListed == true ? const BorderSide( color: Colors.red, width: 2, diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 7ca8964d..a30535dd 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -10,6 +10,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -39,10 +40,9 @@ class ArtistPageHeader extends HookConsumerWidget { ); final auth = ref.watch(authenticationProvider); - final blacklist = ref.watch(blacklistProvider); - final isBlackListed = blacklist.contains( - BlacklistedElement.artist(artistId, artist.name!), - ); + ref.watch(blacklistProvider); + final blacklistNotifier = ref.watch(blacklistProvider.notifier); + final isBlackListed = blacklistNotifier.containsArtist(artist); final image = artist.images.asUrlString( placeholder: ImagePlaceholder.artist, @@ -187,14 +187,16 @@ class ArtistPageHeader extends HookConsumerWidget { ), onPressed: () async { if (isBlackListed) { - ref.read(blacklistProvider.notifier).remove( - BlacklistedElement.artist( - artist.id!, artist.name!), - ); + await ref + .read(blacklistProvider.notifier) + .remove(artist.id!); } else { - ref.read(blacklistProvider.notifier).add( - BlacklistedElement.artist( - artist.id!, artist.name!), + await ref.read(blacklistProvider.notifier).add( + BlacklistTableCompanion.insert( + name: artist.name!, + elementId: artist.id!, + elementType: BlacklistedType.artist, + ), ); } }, diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index b5e10821..1f018dab 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -24,19 +24,21 @@ class BlackListPage extends HookConsumerWidget { final filteredBlacklist = useMemoized( () { if (searchText.value.isEmpty) { - return blacklist; + return blacklist.asData?.value ?? []; } - return blacklist - .map( - (e) => ( - weightedRatio("${e.name} ${e.type.name}", searchText.value), - e, - ), - ) - .sorted((a, b) => b.$1.compareTo(a.$1)) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList(); + return blacklist.asData?.value + .map( + (e) => ( + weightedRatio( + "${e.name} ${e.elementType.name}", searchText.value), + e, + ), + ) + .sorted((a, b) => b.$1.compareTo(a.$1)) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList() ?? + []; }, [blacklist, searchText.value], ); @@ -70,14 +72,14 @@ class BlackListPage extends HookConsumerWidget { final item = filteredBlacklist.elementAt(index); return ListTile( leading: Text("${index + 1}."), - title: Text("${item.name} (${item.type.name})"), - subtitle: Text(item.id), + title: Text("${item.name} (${item.elementType.name})"), + subtitle: Text(item.elementId), trailing: IconButton( icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), onPressed: () { ref .read(blacklistProvider.notifier) - .remove(filteredBlacklist.elementAt(index)); + .remove(filteredBlacklist.elementAt(index).elementId); }, ), ); diff --git a/lib/provider/blacklist_provider.dart b/lib/provider/blacklist_provider.dart index 4f488112..a51d399f 100644 --- a/lib/provider/blacklist_provider.dart +++ b/lib/provider/blacklist_provider.dart @@ -2,69 +2,59 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/current_playlist.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; - -enum BlacklistedType { - artist, - track; - - static BlacklistedType fromName(String name) => - BlacklistedType.values.firstWhere((e) => e.name == name); -} - -class BlacklistedElement { - final String id; - final String name; - final BlacklistedType type; - - BlacklistedElement.artist(this.id, this.name) : type = BlacklistedType.artist; - - BlacklistedElement.track(this.id, this.name) : type = BlacklistedType.track; - - BlacklistedElement.fromJson(Map json) - : id = json['id'], - name = json['name'], - type = BlacklistedType.fromName(json['type']); - - Map toJson() => {'id': id, 'type': type.name, 'name': name}; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +class BlackListNotifier extends AsyncNotifier> { @override - operator ==(other) => - other is BlacklistedElement && - other.id == id && - other.type == type && - other.name == name; + build() async { + final database = ref.watch(databaseProvider); - @override - int get hashCode => id.hashCode ^ type.hashCode ^ name.hashCode; -} + final subscription = database + .select(database.blacklistTable) + .watch() + .listen((event) => state = AsyncData(event)); -class BlackListNotifier - extends PersistedStateNotifier> { - BlackListNotifier() : super({}, "blacklist"); + ref.onDispose(() { + subscription.cancel(); + }); - void add(BlacklistedElement element) { - state = state.union({element}); + return await database.select(database.blacklistTable).get(); } - void remove(BlacklistedElement element) { - state = state.difference({element}); + AppDatabase get _database => ref.read(databaseProvider); + + Future add(BlacklistTableCompanion element) async { + _database.into(_database.blacklistTable).insert(element); + } + + Future remove(String elementId) async { + await (_database.delete(_database.blacklistTable) + ..where((tbl) => tbl.elementId.equals(elementId))) + .go(); } bool contains(TrackSimple track) { final containsTrack = - state.contains(BlacklistedElement.track(track.id!, track.name!)); + state.asData?.value.any((element) => element.elementId == track.id) ?? + false; final containsTrackArtists = track.artists?.any( - (artist) => state.contains( - BlacklistedElement.artist(artist.id!, artist.name ?? "Spotify"), - ), + (artist) => + state.asData?.value.any((el) => el.elementId == artist.id) ?? + false, ) ?? false; return containsTrack || containsTrackArtists; } + bool containsArtist(ArtistSimple artist) { + return state.asData?.value + .any((element) => element.elementId == artist.id) ?? + false; + } + /// Filters the non blacklisted tracks from the given [tracks] Iterable filter(Iterable tracks) { return tracks.whereNot(contains).toList(); @@ -75,34 +65,12 @@ class BlackListNotifier id: playlist.id, name: playlist.name, thumbnail: playlist.thumbnail, - tracks: playlist.tracks.where( - (track) { - return !state - .contains(BlacklistedElement.track(track.id!, track.name!)) && - !(track.artists ?? []).any( - (artist) => state.contains( - BlacklistedElement.artist(artist.id!, artist.name!), - ), - ); - }, - ).toList(), + tracks: playlist.tracks.where((track) => !contains(track)).toList(), ); } - - @override - Set fromJson(Map json) { - return json['blacklist'] - .map((e) => BlacklistedElement.fromJson(e)) - .toSet(); - } - - @override - Map toJson() { - return {'blacklist': state.map((e) => e.toJson()).toList()}; - } } final blacklistProvider = - StateNotifierProvider>((ref) { - return BlackListNotifier(); -}); + AsyncNotifierProvider>( + () => BlackListNotifier(), +); From a799ca55bcb8833c1a2e89078df0460229ef5053 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 16 Jun 2024 20:58:54 +0600 Subject: [PATCH 04/24] chore: add encrypted text column support --- lib/main.dart | 2 + lib/models/database/database.dart | 6 ++- .../typeconverters/encrypted_text.dart | 39 +++++++++++++++++ lib/services/kv_store/encrypted_kv_store.dart | 43 +++++++++++++++++++ lib/services/kv_store/kv_store.dart | 35 +++++++++++++++ pubspec.lock | 24 +++++++++++ pubspec.yaml | 1 + 7 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 lib/models/database/typeconverters/encrypted_text.dart create mode 100644 lib/services/kv_store/encrypted_kv_store.dart diff --git a/lib/main.dart b/lib/main.dart index bdccadd4..09db495c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,6 +27,7 @@ import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/cli/cli.dart'; +import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; 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'; @@ -76,6 +77,7 @@ Future main(List rawArgs) async { } await KVStoreService.initialize(); + await EncryptedKvStoreService.initialize(); final hiveCacheDir = kIsWeb ? null : (await getApplicationSupportDirectory()).path; diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index ad09933d..ac0223fd 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -4,11 +4,14 @@ 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:spotify/spotify.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'; -import 'package:flutter/material.dart' hide Table; +import 'package:flutter/material.dart' hide Table, Key; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; import 'package:sqlite3/sqlite3.dart'; @@ -24,6 +27,7 @@ part 'tables/blacklist.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; +part 'typeconverters/encrypted_text.dart'; @DriftDatabase( tables: [ diff --git a/lib/models/database/typeconverters/encrypted_text.dart b/lib/models/database/typeconverters/encrypted_text.dart new file mode 100644 index 00000000..27921788 --- /dev/null +++ b/lib/models/database/typeconverters/encrypted_text.dart @@ -0,0 +1,39 @@ +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() { + return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64; + } +} + +class EncryptedTextConverter extends TypeConverter { + @override + DecryptedText fromSql(String fromDb) { + return DecryptedText.decrypted(fromDb); + } + + @override + String toSql(DecryptedText value) { + return value.encrypt(); + } +} diff --git a/lib/services/kv_store/encrypted_kv_store.dart b/lib/services/kv_store/encrypted_kv_store.dart new file mode 100644 index 00000000..d8f69690 --- /dev/null +++ b/lib/services/kv_store/encrypted_kv_store.dart @@ -0,0 +1,43 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:uuid/uuid.dart'; + +abstract class EncryptedKvStoreService { + static const _storage = FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + ); + + static late final String _encryptionKeySync; + + static Future initialize() async { + _encryptionKeySync = await encryptionKey; + } + + static String get encryptionKeySync => _encryptionKeySync; + + static Future get encryptionKey async { + 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 setEncryptionKey(String key) async { + try { + await _storage.write(key: 'encryption', value: key); + } catch (e) { + await KVStoreService.setEncryptionKey(key); + } + } +} diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index ae62a055..6b19c032 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'package:encrypt/encrypt.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart'; +import 'package:uuid/uuid.dart'; abstract class KVStoreService { static SharedPreferences? _sharedPreferences; @@ -43,4 +45,37 @@ abstract class KVStoreService { 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 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 setIVKey(IV iv) async { + await sharedPreferences.setString('iv', iv.base64); + } } diff --git a/pubspec.lock b/pubspec.lock index c5871a2a..70b0655c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + url: "https://pub.dev" + source: hosted + version: "1.5.3" async: dependency: "direct main" description: @@ -539,6 +547,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.13" + encrypt: + dependency: "direct main" + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" envied: dependency: "direct main" description: @@ -1670,6 +1686,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ddace46e..a923f5a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -124,6 +124,7 @@ dependencies: drift: ^2.18.0 sqlite3_flutter_libs: ^0.5.23 sqlite3: ^2.4.3 + encrypt: ^5.0.3 dev_dependencies: build_runner: ^2.4.11 From d18f74fd65486f25319d5804a191616a9220d7da Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 16 Jun 2024 22:33:23 +0600 Subject: [PATCH 05/24] refactor: use drift db based authentication --- lib/collections/routes.dart | 8 +- .../fallbacks/anonymous_fallback.dart | 2 +- lib/components/heart_button/heart_button.dart | 4 +- lib/components/track_tile/track_options.dart | 2 +- .../sections/header/header_actions.dart | 2 +- .../configurators/use_endless_playback.dart | 4 +- lib/models/database/database.dart | 14 +- lib/models/database/database.g.dart | 2051 ++++++++++------- .../database/tables/authentication.dart | 8 + .../typeconverters/encrypted_text.dart | 5 + .../database/typeconverters/string_list.dart | 2 +- lib/modules/desktop_login/login_form.dart | 7 +- lib/modules/home/sections/friends.dart | 4 +- lib/modules/home/sections/new_releases.dart | 4 +- .../local_folder/local_folder_item.dart | 2 +- lib/modules/library/user_albums.dart | 4 +- lib/modules/library/user_artists.dart | 4 +- lib/modules/library/user_playlists.dart | 4 +- lib/modules/player/player.dart | 2 +- lib/modules/player/player_actions.dart | 2 +- lib/modules/root/bottom_player.dart | 2 +- lib/modules/root/sidebar.dart | 4 +- lib/pages/artist/section/header.dart | 2 +- lib/pages/desktop_login/login_tutorial.dart | 9 +- lib/pages/lyrics/lyrics.dart | 4 +- lib/pages/lyrics/mini_lyrics.dart | 4 +- lib/pages/mobile_login/mobile_login.dart | 6 +- lib/pages/search/search.dart | 7 +- lib/pages/settings/sections/accounts.dart | 6 +- .../authentication.dart} | 190 +- .../custom_spotify_endpoint_provider.dart | 4 +- lib/provider/spotify/views/home.dart | 4 +- lib/provider/spotify/views/home_section.dart | 4 +- lib/provider/spotify_provider.dart | 6 +- .../user_preferences_provider.dart | 7 +- lib/services/kv_store/encrypted_kv_store.dart | 18 +- 36 files changed, 1408 insertions(+), 1004 deletions(-) create mode 100644 lib/models/database/tables/authentication.dart rename lib/provider/{authentication_provider.dart => authentication/authentication.dart} (51%) diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index b9e06c61..b3cba581 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -32,7 +32,7 @@ import 'package:spotube/pages/stats/playlists/playlists.dart'; import 'package:spotube/pages/stats/stats.dart'; import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/track/track.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/spotube_page_route.dart'; @@ -59,11 +59,9 @@ final routerProvider = Provider((ref) { path: "/", name: HomePage.name, redirect: (context, state) async { - final authNotifier = ref.read(authenticationProvider.notifier); - final json = await authNotifier.box.get(authNotifier.cacheKey); + final auth = await ref.read(authenticationProvider.future); - if (json?["cookie"] == null && - !KVStoreService.doneGettingStarted) { + if (auth == null && !KVStoreService.doneGettingStarted) { return "/getting-started"; } diff --git a/lib/components/fallbacks/anonymous_fallback.dart b/lib/components/fallbacks/anonymous_fallback.dart index 5ced6bb6..799297e3 100644 --- a/lib/components/fallbacks/anonymous_fallback.dart +++ b/lib/components/fallbacks/anonymous_fallback.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/settings/settings.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/utils/service_utils.dart'; class AnonymousFallback extends ConsumerWidget { diff --git a/lib/components/heart_button/heart_button.dart b/lib/components/heart_button/heart_button.dart index 8222b8e6..fa4318cc 100644 --- a/lib/components/heart_button/heart_button.dart +++ b/lib/components/heart_button/heart_button.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/heart_button/use_track_toggle_like.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HeartButton extends HookConsumerWidget { @@ -26,7 +26,7 @@ class HeartButton extends HookConsumerWidget { Widget build(BuildContext context, ref) { final auth = ref.watch(authenticationProvider); - if (auth == null) return const SizedBox.shrink(); + if (auth.asData?.value == null) return const SizedBox.shrink(); return IconButton( tooltip: tooltip, diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index fd3018ba..d54a0c15 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -20,7 +20,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index 3e0c4cc1..f20cd553 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index 97eb3f48..9b90b23d 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -2,7 +2,7 @@ import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -18,7 +18,7 @@ void useEndlessPlayback(WidgetRef ref) { useEffect( () { - if (!endlessPlayback || auth == null) return null; + if (!endlessPlayback || auth.asData?.value == null) return null; void listener(int index) async { try { diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index ac0223fd..56f72ee7 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -19,10 +19,11 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; part 'database.g.dart'; -part 'tables/preferences.dart'; -part 'tables/source_match.dart'; -part 'tables/skip_segment.dart'; +part 'tables/authentication.dart'; part 'tables/blacklist.dart'; +part 'tables/preferences.dart'; +part 'tables/skip_segment.dart'; +part 'tables/source_match.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; @@ -31,10 +32,11 @@ part 'typeconverters/encrypted_text.dart'; @DriftDatabase( tables: [ - PreferencesTable, - SourceMatchTable, - SkipSegmentTable, + AuthenticationTable, BlacklistTable, + PreferencesTable, + SkipSegmentTable, + SourceMatchTable, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 8c996d21..0ac7005e 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3,6 +3,533 @@ part of 'database.dart'; // ignore_for_file: type=lint +class $AuthenticationTableTable extends AuthenticationTable + with TableInfo<$AuthenticationTableTable, AuthenticationTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $AuthenticationTableTable(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 _cookieMeta = const VerificationMeta('cookie'); + @override + late final GeneratedColumnWithTypeConverter cookie = + GeneratedColumn('cookie', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $AuthenticationTableTable.$convertercookie); + static const VerificationMeta _accessTokenMeta = + const VerificationMeta('accessToken'); + @override + late final GeneratedColumnWithTypeConverter + accessToken = GeneratedColumn('access_token', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $AuthenticationTableTable.$converteraccessToken); + static const VerificationMeta _expirationMeta = + const VerificationMeta('expiration'); + @override + late final GeneratedColumn expiration = GeneratedColumn( + 'expiration', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [id, cookie, accessToken, expiration]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'authentication_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)); + } + context.handle(_cookieMeta, const VerificationResult.success()); + context.handle(_accessTokenMeta, const VerificationResult.success()); + if (data.containsKey('expiration')) { + context.handle( + _expirationMeta, + expiration.isAcceptableOrUnknown( + data['expiration']!, _expirationMeta)); + } else if (isInserting) { + context.missing(_expirationMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + AuthenticationTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthenticationTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + cookie: $AuthenticationTableTable.$convertercookie.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}cookie'])!), + accessToken: $AuthenticationTableTable.$converteraccessToken.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}access_token'])!), + expiration: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}expiration'])!, + ); + } + + @override + $AuthenticationTableTable createAlias(String alias) { + return $AuthenticationTableTable(attachedDatabase, alias); + } + + static TypeConverter $convertercookie = + EncryptedTextConverter(); + static TypeConverter $converteraccessToken = + EncryptedTextConverter(); +} + +class AuthenticationTableData extends DataClass + implements Insertable { + final int id; + final DecryptedText cookie; + final DecryptedText accessToken; + final DateTime expiration; + const AuthenticationTableData( + {required this.id, + required this.cookie, + required this.accessToken, + required this.expiration}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + { + map['cookie'] = Variable( + $AuthenticationTableTable.$convertercookie.toSql(cookie)); + } + { + map['access_token'] = Variable( + $AuthenticationTableTable.$converteraccessToken.toSql(accessToken)); + } + map['expiration'] = Variable(expiration); + return map; + } + + AuthenticationTableCompanion toCompanion(bool nullToAbsent) { + return AuthenticationTableCompanion( + id: Value(id), + cookie: Value(cookie), + accessToken: Value(accessToken), + expiration: Value(expiration), + ); + } + + factory AuthenticationTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthenticationTableData( + id: serializer.fromJson(json['id']), + cookie: serializer.fromJson(json['cookie']), + accessToken: serializer.fromJson(json['accessToken']), + expiration: serializer.fromJson(json['expiration']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'cookie': serializer.toJson(cookie), + 'accessToken': serializer.toJson(accessToken), + 'expiration': serializer.toJson(expiration), + }; + } + + AuthenticationTableData copyWith( + {int? id, + DecryptedText? cookie, + DecryptedText? accessToken, + DateTime? expiration}) => + AuthenticationTableData( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + @override + String toString() { + return (StringBuffer('AuthenticationTableData(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, cookie, accessToken, expiration); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthenticationTableData && + other.id == this.id && + other.cookie == this.cookie && + other.accessToken == this.accessToken && + other.expiration == this.expiration); +} + +class AuthenticationTableCompanion + extends UpdateCompanion { + final Value id; + final Value cookie; + final Value accessToken; + final Value expiration; + const AuthenticationTableCompanion({ + this.id = const Value.absent(), + this.cookie = const Value.absent(), + this.accessToken = const Value.absent(), + this.expiration = const Value.absent(), + }); + AuthenticationTableCompanion.insert({ + this.id = const Value.absent(), + required DecryptedText cookie, + required DecryptedText accessToken, + required DateTime expiration, + }) : cookie = Value(cookie), + accessToken = Value(accessToken), + expiration = Value(expiration); + static Insertable custom({ + Expression? id, + Expression? cookie, + Expression? accessToken, + Expression? expiration, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (cookie != null) 'cookie': cookie, + if (accessToken != null) 'access_token': accessToken, + if (expiration != null) 'expiration': expiration, + }); + } + + AuthenticationTableCompanion copyWith( + {Value? id, + Value? cookie, + Value? accessToken, + Value? expiration}) { + return AuthenticationTableCompanion( + id: id ?? this.id, + cookie: cookie ?? this.cookie, + accessToken: accessToken ?? this.accessToken, + expiration: expiration ?? this.expiration, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (cookie.present) { + map['cookie'] = Variable( + $AuthenticationTableTable.$convertercookie.toSql(cookie.value)); + } + if (accessToken.present) { + map['access_token'] = Variable($AuthenticationTableTable + .$converteraccessToken + .toSql(accessToken.value)); + } + if (expiration.present) { + map['expiration'] = Variable(expiration.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthenticationTableCompanion(') + ..write('id: $id, ') + ..write('cookie: $cookie, ') + ..write('accessToken: $accessToken, ') + ..write('expiration: $expiration') + ..write(')')) + .toString(); + } +} + +class $BlacklistTableTable extends BlacklistTable + with TableInfo<$BlacklistTableTable, BlacklistTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $BlacklistTableTable(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 _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _elementTypeMeta = + const VerificationMeta('elementType'); + @override + late final GeneratedColumnWithTypeConverter + elementType = GeneratedColumn('element_type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $BlacklistTableTable.$converterelementType); + static const VerificationMeta _elementIdMeta = + const VerificationMeta('elementId'); + @override + late final GeneratedColumn elementId = GeneratedColumn( + 'element_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name, elementType, elementId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'blacklist_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('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + context.handle(_elementTypeMeta, const VerificationResult.success()); + if (data.containsKey('element_id')) { + context.handle(_elementIdMeta, + elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); + } else if (isInserting) { + context.missing(_elementIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + BlacklistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BlacklistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + elementType: $BlacklistTableTable.$converterelementType.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}element_type'])!), + elementId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, + ); + } + + @override + $BlacklistTableTable createAlias(String alias) { + return $BlacklistTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 + $converterelementType = + const EnumNameConverter(BlacklistedType.values); +} + +class BlacklistTableData extends DataClass + implements Insertable { + final int id; + final String name; + final BlacklistedType elementType; + final String elementId; + const BlacklistTableData( + {required this.id, + required this.name, + required this.elementType, + required this.elementId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + { + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType)); + } + map['element_id'] = Variable(elementId); + return map; + } + + BlacklistTableCompanion toCompanion(bool nullToAbsent) { + return BlacklistTableCompanion( + id: Value(id), + name: Value(name), + elementType: Value(elementType), + elementId: Value(elementId), + ); + } + + factory BlacklistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BlacklistTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + elementType: $BlacklistTableTable.$converterelementType + .fromJson(serializer.fromJson(json['elementType'])), + elementId: serializer.fromJson(json['elementId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'elementType': serializer.toJson( + $BlacklistTableTable.$converterelementType.toJson(elementType)), + 'elementId': serializer.toJson(elementId), + }; + } + + BlacklistTableData copyWith( + {int? id, + String? name, + BlacklistedType? elementType, + String? elementId}) => + BlacklistTableData( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + @override + String toString() { + return (StringBuffer('BlacklistTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, elementType, elementId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BlacklistTableData && + other.id == this.id && + other.name == this.name && + other.elementType == this.elementType && + other.elementId == this.elementId); +} + +class BlacklistTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value elementType; + final Value elementId; + const BlacklistTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.elementType = const Value.absent(), + this.elementId = const Value.absent(), + }); + BlacklistTableCompanion.insert({ + this.id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) : name = Value(name), + elementType = Value(elementType), + elementId = Value(elementId); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? elementType, + Expression? elementId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (elementType != null) 'element_type': elementType, + if (elementId != null) 'element_id': elementId, + }); + } + + BlacklistTableCompanion copyWith( + {Value? id, + Value? name, + Value? elementType, + Value? elementId}) { + return BlacklistTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + elementType: elementType ?? this.elementType, + elementId: elementId ?? this.elementId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (elementType.present) { + map['element_type'] = Variable( + $BlacklistTableTable.$converterelementType.toSql(elementType.value)); + } + if (elementId.present) { + map['element_id'] = Variable(elementId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BlacklistTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('elementType: $elementType, ') + ..write('elementId: $elementId') + ..write(')')) + .toString(); + } +} + class $PreferencesTableTable extends PreferencesTable with TableInfo<$PreferencesTableTable, PreferencesTableData> { @override @@ -1204,6 +1731,293 @@ class PreferencesTableCompanion extends UpdateCompanion { } } +class $SkipSegmentTableTable extends SkipSegmentTable + with TableInfo<$SkipSegmentTableTable, SkipSegmentTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SkipSegmentTableTable(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 _startMeta = const VerificationMeta('start'); + @override + late final GeneratedColumn start = GeneratedColumn( + 'start', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _endMeta = const VerificationMeta('end'); + @override + late final GeneratedColumn end = GeneratedColumn( + 'end', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + 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 _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [id, start, end, trackId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'skip_segment_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('start')) { + context.handle( + _startMeta, start.isAcceptableOrUnknown(data['start']!, _startMeta)); + } else if (isInserting) { + context.missing(_startMeta); + } + if (data.containsKey('end')) { + context.handle( + _endMeta, end.isAcceptableOrUnknown(data['end']!, _endMeta)); + } else if (isInserting) { + context.missing(_endMeta); + } + if (data.containsKey('track_id')) { + context.handle(_trackIdMeta, + trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); + } else if (isInserting) { + context.missing(_trackIdMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SkipSegmentTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SkipSegmentTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + start: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}start'])!, + end: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}end'])!, + trackId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SkipSegmentTableTable createAlias(String alias) { + return $SkipSegmentTableTable(attachedDatabase, alias); + } +} + +class SkipSegmentTableData extends DataClass + implements Insertable { + final int id; + final int start; + final int end; + final String trackId; + final DateTime createdAt; + const SkipSegmentTableData( + {required this.id, + required this.start, + required this.end, + required this.trackId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['start'] = Variable(start); + map['end'] = Variable(end); + map['track_id'] = Variable(trackId); + map['created_at'] = Variable(createdAt); + return map; + } + + SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { + return SkipSegmentTableCompanion( + id: Value(id), + start: Value(start), + end: Value(end), + trackId: Value(trackId), + createdAt: Value(createdAt), + ); + } + + factory SkipSegmentTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SkipSegmentTableData( + id: serializer.fromJson(json['id']), + start: serializer.fromJson(json['start']), + end: serializer.fromJson(json['end']), + trackId: serializer.fromJson(json['trackId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'start': serializer.toJson(start), + 'end': serializer.toJson(end), + 'trackId': serializer.toJson(trackId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SkipSegmentTableData copyWith( + {int? id, + int? start, + int? end, + String? trackId, + DateTime? createdAt}) => + SkipSegmentTableData( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + @override + String toString() { + return (StringBuffer('SkipSegmentTableData(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, start, end, trackId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SkipSegmentTableData && + other.id == this.id && + other.start == this.start && + other.end == this.end && + other.trackId == this.trackId && + other.createdAt == this.createdAt); +} + +class SkipSegmentTableCompanion extends UpdateCompanion { + final Value id; + final Value start; + final Value end; + final Value trackId; + final Value createdAt; + const SkipSegmentTableCompanion({ + this.id = const Value.absent(), + this.start = const Value.absent(), + this.end = const Value.absent(), + this.trackId = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SkipSegmentTableCompanion.insert({ + this.id = const Value.absent(), + required int start, + required int end, + required String trackId, + this.createdAt = const Value.absent(), + }) : start = Value(start), + end = Value(end), + trackId = Value(trackId); + static Insertable custom({ + Expression? id, + Expression? start, + Expression? end, + Expression? trackId, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (start != null) 'start': start, + if (end != null) 'end': end, + if (trackId != null) 'track_id': trackId, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SkipSegmentTableCompanion copyWith( + {Value? id, + Value? start, + Value? end, + Value? trackId, + Value? createdAt}) { + return SkipSegmentTableCompanion( + id: id ?? this.id, + start: start ?? this.start, + end: end ?? this.end, + trackId: trackId ?? this.trackId, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (start.present) { + map['start'] = Variable(start.value); + } + if (end.present) { + map['end'] = Variable(end.value); + } + if (trackId.present) { + map['track_id'] = Variable(trackId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SkipSegmentTableCompanion(') + ..write('id: $id, ') + ..write('start: $start, ') + ..write('end: $end, ') + ..write('trackId: $trackId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + class $SourceMatchTableTable extends SourceMatchTable with TableInfo<$SourceMatchTableTable, SourceMatchTableData> { @override @@ -1502,581 +2316,288 @@ class SourceMatchTableCompanion extends UpdateCompanion { } } -class $SkipSegmentTableTable extends SkipSegmentTable - with TableInfo<$SkipSegmentTableTable, SkipSegmentTableData> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $SkipSegmentTableTable(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 _startMeta = const VerificationMeta('start'); - @override - late final GeneratedColumn start = GeneratedColumn( - 'start', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _endMeta = const VerificationMeta('end'); - @override - late final GeneratedColumn end = GeneratedColumn( - 'end', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - 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 _createdAtMeta = - const VerificationMeta('createdAt'); - @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - @override - List get $columns => [id, start, end, trackId, createdAt]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'skip_segment_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('start')) { - context.handle( - _startMeta, start.isAcceptableOrUnknown(data['start']!, _startMeta)); - } else if (isInserting) { - context.missing(_startMeta); - } - if (data.containsKey('end')) { - context.handle( - _endMeta, end.isAcceptableOrUnknown(data['end']!, _endMeta)); - } else if (isInserting) { - context.missing(_endMeta); - } - if (data.containsKey('track_id')) { - context.handle(_trackIdMeta, - trackId.isAcceptableOrUnknown(data['track_id']!, _trackIdMeta)); - } else if (isInserting) { - context.missing(_trackIdMeta); - } - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - SkipSegmentTableData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return SkipSegmentTableData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - start: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}start'])!, - end: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}end'])!, - trackId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}track_id'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - ); - } - - @override - $SkipSegmentTableTable createAlias(String alias) { - return $SkipSegmentTableTable(attachedDatabase, alias); - } -} - -class SkipSegmentTableData extends DataClass - implements Insertable { - final int id; - final int start; - final int end; - final String trackId; - final DateTime createdAt; - const SkipSegmentTableData( - {required this.id, - required this.start, - required this.end, - required this.trackId, - required this.createdAt}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['start'] = Variable(start); - map['end'] = Variable(end); - map['track_id'] = Variable(trackId); - map['created_at'] = Variable(createdAt); - return map; - } - - SkipSegmentTableCompanion toCompanion(bool nullToAbsent) { - return SkipSegmentTableCompanion( - id: Value(id), - start: Value(start), - end: Value(end), - trackId: Value(trackId), - createdAt: Value(createdAt), - ); - } - - factory SkipSegmentTableData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return SkipSegmentTableData( - id: serializer.fromJson(json['id']), - start: serializer.fromJson(json['start']), - end: serializer.fromJson(json['end']), - trackId: serializer.fromJson(json['trackId']), - createdAt: serializer.fromJson(json['createdAt']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'start': serializer.toJson(start), - 'end': serializer.toJson(end), - 'trackId': serializer.toJson(trackId), - 'createdAt': serializer.toJson(createdAt), - }; - } - - SkipSegmentTableData copyWith( - {int? id, - int? start, - int? end, - String? trackId, - DateTime? createdAt}) => - SkipSegmentTableData( - id: id ?? this.id, - start: start ?? this.start, - end: end ?? this.end, - trackId: trackId ?? this.trackId, - createdAt: createdAt ?? this.createdAt, - ); - @override - String toString() { - return (StringBuffer('SkipSegmentTableData(') - ..write('id: $id, ') - ..write('start: $start, ') - ..write('end: $end, ') - ..write('trackId: $trackId, ') - ..write('createdAt: $createdAt') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, start, end, trackId, createdAt); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is SkipSegmentTableData && - other.id == this.id && - other.start == this.start && - other.end == this.end && - other.trackId == this.trackId && - other.createdAt == this.createdAt); -} - -class SkipSegmentTableCompanion extends UpdateCompanion { - final Value id; - final Value start; - final Value end; - final Value trackId; - final Value createdAt; - const SkipSegmentTableCompanion({ - this.id = const Value.absent(), - this.start = const Value.absent(), - this.end = const Value.absent(), - this.trackId = const Value.absent(), - this.createdAt = const Value.absent(), - }); - SkipSegmentTableCompanion.insert({ - this.id = const Value.absent(), - required int start, - required int end, - required String trackId, - this.createdAt = const Value.absent(), - }) : start = Value(start), - end = Value(end), - trackId = Value(trackId); - static Insertable custom({ - Expression? id, - Expression? start, - Expression? end, - Expression? trackId, - Expression? createdAt, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (start != null) 'start': start, - if (end != null) 'end': end, - if (trackId != null) 'track_id': trackId, - if (createdAt != null) 'created_at': createdAt, - }); - } - - SkipSegmentTableCompanion copyWith( - {Value? id, - Value? start, - Value? end, - Value? trackId, - Value? createdAt}) { - return SkipSegmentTableCompanion( - id: id ?? this.id, - start: start ?? this.start, - end: end ?? this.end, - trackId: trackId ?? this.trackId, - createdAt: createdAt ?? this.createdAt, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (start.present) { - map['start'] = Variable(start.value); - } - if (end.present) { - map['end'] = Variable(end.value); - } - if (trackId.present) { - map['track_id'] = Variable(trackId.value); - } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('SkipSegmentTableCompanion(') - ..write('id: $id, ') - ..write('start: $start, ') - ..write('end: $end, ') - ..write('trackId: $trackId, ') - ..write('createdAt: $createdAt') - ..write(')')) - .toString(); - } -} - -class $BlacklistTableTable extends BlacklistTable - with TableInfo<$BlacklistTableTable, BlacklistTableData> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $BlacklistTableTable(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 _nameMeta = const VerificationMeta('name'); - @override - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _elementTypeMeta = - const VerificationMeta('elementType'); - @override - late final GeneratedColumnWithTypeConverter - elementType = GeneratedColumn('element_type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter( - $BlacklistTableTable.$converterelementType); - static const VerificationMeta _elementIdMeta = - const VerificationMeta('elementId'); - @override - late final GeneratedColumn elementId = GeneratedColumn( - 'element_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, name, elementType, elementId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'blacklist_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('name')) { - context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); - } else if (isInserting) { - context.missing(_nameMeta); - } - context.handle(_elementTypeMeta, const VerificationResult.success()); - if (data.containsKey('element_id')) { - context.handle(_elementIdMeta, - elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta)); - } else if (isInserting) { - context.missing(_elementIdMeta); - } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - BlacklistTableData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return BlacklistTableData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - elementType: $BlacklistTableTable.$converterelementType.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}element_type'])!), - elementId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}element_id'])!, - ); - } - - @override - $BlacklistTableTable createAlias(String alias) { - return $BlacklistTableTable(attachedDatabase, alias); - } - - static JsonTypeConverter2 - $converterelementType = - const EnumNameConverter(BlacklistedType.values); -} - -class BlacklistTableData extends DataClass - implements Insertable { - final int id; - final String name; - final BlacklistedType elementType; - final String elementId; - const BlacklistTableData( - {required this.id, - required this.name, - required this.elementType, - required this.elementId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - { - map['element_type'] = Variable( - $BlacklistTableTable.$converterelementType.toSql(elementType)); - } - map['element_id'] = Variable(elementId); - return map; - } - - BlacklistTableCompanion toCompanion(bool nullToAbsent) { - return BlacklistTableCompanion( - id: Value(id), - name: Value(name), - elementType: Value(elementType), - elementId: Value(elementId), - ); - } - - factory BlacklistTableData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return BlacklistTableData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - elementType: $BlacklistTableTable.$converterelementType - .fromJson(serializer.fromJson(json['elementType'])), - elementId: serializer.fromJson(json['elementId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'elementType': serializer.toJson( - $BlacklistTableTable.$converterelementType.toJson(elementType)), - 'elementId': serializer.toJson(elementId), - }; - } - - BlacklistTableData copyWith( - {int? id, - String? name, - BlacklistedType? elementType, - String? elementId}) => - BlacklistTableData( - id: id ?? this.id, - name: name ?? this.name, - elementType: elementType ?? this.elementType, - elementId: elementId ?? this.elementId, - ); - @override - String toString() { - return (StringBuffer('BlacklistTableData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('elementType: $elementType, ') - ..write('elementId: $elementId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, elementType, elementId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is BlacklistTableData && - other.id == this.id && - other.name == this.name && - other.elementType == this.elementType && - other.elementId == this.elementId); -} - -class BlacklistTableCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value elementType; - final Value elementId; - const BlacklistTableCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.elementType = const Value.absent(), - this.elementId = const Value.absent(), - }); - BlacklistTableCompanion.insert({ - this.id = const Value.absent(), - required String name, - required BlacklistedType elementType, - required String elementId, - }) : name = Value(name), - elementType = Value(elementType), - elementId = Value(elementId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? elementType, - Expression? elementId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (elementType != null) 'element_type': elementType, - if (elementId != null) 'element_id': elementId, - }); - } - - BlacklistTableCompanion copyWith( - {Value? id, - Value? name, - Value? elementType, - Value? elementId}) { - return BlacklistTableCompanion( - id: id ?? this.id, - name: name ?? this.name, - elementType: elementType ?? this.elementType, - elementId: elementId ?? this.elementId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (elementType.present) { - map['element_type'] = Variable( - $BlacklistTableTable.$converterelementType.toSql(elementType.value)); - } - if (elementId.present) { - map['element_id'] = Variable(elementId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('BlacklistTableCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('elementType: $elementType, ') - ..write('elementId: $elementId') - ..write(')')) - .toString(); - } -} - abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); + late final $AuthenticationTableTable authenticationTable = + $AuthenticationTableTable(this); + late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); late final $PreferencesTableTable preferencesTable = $PreferencesTableTable(this); - late final $SourceMatchTableTable sourceMatchTable = - $SourceMatchTableTable(this); late final $SkipSegmentTableTable skipSegmentTable = $SkipSegmentTableTable(this); - late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); - late final Index uniqTrackMatch = Index('uniq_track_match', - 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); + late final $SourceMatchTableTable sourceMatchTable = + $SourceMatchTableTable(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', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override List get allSchemaEntities => [ - preferencesTable, - sourceMatchTable, - skipSegmentTable, + authenticationTable, blacklistTable, - uniqTrackMatch, - uniqueBlacklist + preferencesTable, + skipSegmentTable, + sourceMatchTable, + uniqueBlacklist, + uniqTrackMatch ]; } +typedef $$AuthenticationTableTableInsertCompanionBuilder + = AuthenticationTableCompanion Function({ + Value id, + required DecryptedText cookie, + required DecryptedText accessToken, + required DateTime expiration, +}); +typedef $$AuthenticationTableTableUpdateCompanionBuilder + = AuthenticationTableCompanion Function({ + Value id, + Value cookie, + Value accessToken, + Value expiration, +}); + +class $$AuthenticationTableTableTableManager extends RootTableManager< + _$AppDatabase, + $AuthenticationTableTable, + AuthenticationTableData, + $$AuthenticationTableTableFilterComposer, + $$AuthenticationTableTableOrderingComposer, + $$AuthenticationTableTableProcessedTableManager, + $$AuthenticationTableTableInsertCompanionBuilder, + $$AuthenticationTableTableUpdateCompanionBuilder> { + $$AuthenticationTableTableTableManager( + _$AppDatabase db, $AuthenticationTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: $$AuthenticationTableTableFilterComposer( + ComposerState(db, table)), + orderingComposer: $$AuthenticationTableTableOrderingComposer( + ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$AuthenticationTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value cookie = const Value.absent(), + Value accessToken = const Value.absent(), + Value expiration = const Value.absent(), + }) => + AuthenticationTableCompanion( + id: id, + cookie: cookie, + accessToken: accessToken, + expiration: expiration, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required DecryptedText cookie, + required DecryptedText accessToken, + required DateTime expiration, + }) => + AuthenticationTableCompanion.insert( + id: id, + cookie: cookie, + accessToken: accessToken, + expiration: expiration, + ), + )); +} + +class $$AuthenticationTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $AuthenticationTableTable, + AuthenticationTableData, + $$AuthenticationTableTableFilterComposer, + $$AuthenticationTableTableOrderingComposer, + $$AuthenticationTableTableProcessedTableManager, + $$AuthenticationTableTableInsertCompanionBuilder, + $$AuthenticationTableTableUpdateCompanionBuilder> { + $$AuthenticationTableTableProcessedTableManager(super.$state); +} + +class $$AuthenticationTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $AuthenticationTableTable> { + $$AuthenticationTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get cookie => $state.composableBuilder( + column: $state.table.cookie, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get accessToken => $state.composableBuilder( + column: $state.table.accessToken, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get expiration => $state.composableBuilder( + column: $state.table.expiration, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$AuthenticationTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $AuthenticationTableTable> { + $$AuthenticationTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get cookie => $state.composableBuilder( + column: $state.table.cookie, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get accessToken => $state.composableBuilder( + column: $state.table.accessToken, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get expiration => $state.composableBuilder( + column: $state.table.expiration, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +typedef $$BlacklistTableTableInsertCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + required String name, + required BlacklistedType elementType, + required String elementId, +}); +typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion + Function({ + Value id, + Value name, + Value elementType, + Value elementId, +}); + +class $$BlacklistTableTableTableManager extends RootTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableTableManager( + _$AppDatabase db, $BlacklistTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$BlacklistTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$BlacklistTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$BlacklistTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value elementType = const Value.absent(), + Value elementId = const Value.absent(), + }) => + BlacklistTableCompanion( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required String name, + required BlacklistedType elementType, + required String elementId, + }) => + BlacklistTableCompanion.insert( + id: id, + name: name, + elementType: elementType, + elementId: elementId, + ), + )); +} + +class $$BlacklistTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $BlacklistTableTable, + BlacklistTableData, + $$BlacklistTableTableFilterComposer, + $$BlacklistTableTableOrderingComposer, + $$BlacklistTableTableProcessedTableManager, + $$BlacklistTableTableInsertCompanionBuilder, + $$BlacklistTableTableUpdateCompanionBuilder> { + $$BlacklistTableTableProcessedTableManager(super.$state); +} + +class $$BlacklistTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$BlacklistTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> { + $$BlacklistTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get name => $state.composableBuilder( + column: $state.table.name, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementType => $state.composableBuilder( + column: $state.table.elementType, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get elementId => $state.composableBuilder( + column: $state.table.elementId, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + typedef $$PreferencesTableTableInsertCompanionBuilder = PreferencesTableCompanion Function({ Value id, @@ -2560,6 +3081,145 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$SkipSegmentTableTableInsertCompanionBuilder + = SkipSegmentTableCompanion Function({ + Value id, + required int start, + required int end, + required String trackId, + Value createdAt, +}); +typedef $$SkipSegmentTableTableUpdateCompanionBuilder + = SkipSegmentTableCompanion Function({ + Value id, + Value start, + Value end, + Value trackId, + Value createdAt, +}); + +class $$SkipSegmentTableTableTableManager extends RootTableManager< + _$AppDatabase, + $SkipSegmentTableTable, + SkipSegmentTableData, + $$SkipSegmentTableTableFilterComposer, + $$SkipSegmentTableTableOrderingComposer, + $$SkipSegmentTableTableProcessedTableManager, + $$SkipSegmentTableTableInsertCompanionBuilder, + $$SkipSegmentTableTableUpdateCompanionBuilder> { + $$SkipSegmentTableTableTableManager( + _$AppDatabase db, $SkipSegmentTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$SkipSegmentTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$SkipSegmentTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$SkipSegmentTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value start = const Value.absent(), + Value end = const Value.absent(), + Value trackId = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SkipSegmentTableCompanion( + id: id, + start: start, + end: end, + trackId: trackId, + createdAt: createdAt, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required int start, + required int end, + required String trackId, + Value createdAt = const Value.absent(), + }) => + SkipSegmentTableCompanion.insert( + id: id, + start: start, + end: end, + trackId: trackId, + createdAt: createdAt, + ), + )); +} + +class $$SkipSegmentTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $SkipSegmentTableTable, + SkipSegmentTableData, + $$SkipSegmentTableTableFilterComposer, + $$SkipSegmentTableTableOrderingComposer, + $$SkipSegmentTableTableProcessedTableManager, + $$SkipSegmentTableTableInsertCompanionBuilder, + $$SkipSegmentTableTableUpdateCompanionBuilder> { + $$SkipSegmentTableTableProcessedTableManager(super.$state); +} + +class $$SkipSegmentTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $SkipSegmentTableTable> { + $$SkipSegmentTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get start => $state.composableBuilder( + column: $state.table.start, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get end => $state.composableBuilder( + column: $state.table.end, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get trackId => $state.composableBuilder( + column: $state.table.trackId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$SkipSegmentTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $SkipSegmentTableTable> { + $$SkipSegmentTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get start => $state.composableBuilder( + column: $state.table.start, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get end => $state.composableBuilder( + column: $state.table.end, + 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 createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + typedef $$SourceMatchTableTableInsertCompanionBuilder = SourceMatchTableCompanion Function({ Value id, @@ -2701,278 +3361,17 @@ class $$SourceMatchTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } -typedef $$SkipSegmentTableTableInsertCompanionBuilder - = SkipSegmentTableCompanion Function({ - Value id, - required int start, - required int end, - required String trackId, - Value createdAt, -}); -typedef $$SkipSegmentTableTableUpdateCompanionBuilder - = SkipSegmentTableCompanion Function({ - Value id, - Value start, - Value end, - Value trackId, - Value createdAt, -}); - -class $$SkipSegmentTableTableTableManager extends RootTableManager< - _$AppDatabase, - $SkipSegmentTableTable, - SkipSegmentTableData, - $$SkipSegmentTableTableFilterComposer, - $$SkipSegmentTableTableOrderingComposer, - $$SkipSegmentTableTableProcessedTableManager, - $$SkipSegmentTableTableInsertCompanionBuilder, - $$SkipSegmentTableTableUpdateCompanionBuilder> { - $$SkipSegmentTableTableTableManager( - _$AppDatabase db, $SkipSegmentTableTable table) - : super(TableManagerState( - db: db, - table: table, - filteringComposer: - $$SkipSegmentTableTableFilterComposer(ComposerState(db, table)), - orderingComposer: - $$SkipSegmentTableTableOrderingComposer(ComposerState(db, table)), - getChildManagerBuilder: (p) => - $$SkipSegmentTableTableProcessedTableManager(p), - getUpdateCompanionBuilder: ({ - Value id = const Value.absent(), - Value start = const Value.absent(), - Value end = const Value.absent(), - Value trackId = const Value.absent(), - Value createdAt = const Value.absent(), - }) => - SkipSegmentTableCompanion( - id: id, - start: start, - end: end, - trackId: trackId, - createdAt: createdAt, - ), - getInsertCompanionBuilder: ({ - Value id = const Value.absent(), - required int start, - required int end, - required String trackId, - Value createdAt = const Value.absent(), - }) => - SkipSegmentTableCompanion.insert( - id: id, - start: start, - end: end, - trackId: trackId, - createdAt: createdAt, - ), - )); -} - -class $$SkipSegmentTableTableProcessedTableManager - extends ProcessedTableManager< - _$AppDatabase, - $SkipSegmentTableTable, - SkipSegmentTableData, - $$SkipSegmentTableTableFilterComposer, - $$SkipSegmentTableTableOrderingComposer, - $$SkipSegmentTableTableProcessedTableManager, - $$SkipSegmentTableTableInsertCompanionBuilder, - $$SkipSegmentTableTableUpdateCompanionBuilder> { - $$SkipSegmentTableTableProcessedTableManager(super.$state); -} - -class $$SkipSegmentTableTableFilterComposer - extends FilterComposer<_$AppDatabase, $SkipSegmentTableTable> { - $$SkipSegmentTableTableFilterComposer(super.$state); - ColumnFilters get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get start => $state.composableBuilder( - column: $state.table.start, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get end => $state.composableBuilder( - column: $state.table.end, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get trackId => $state.composableBuilder( - column: $state.table.trackId, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get createdAt => $state.composableBuilder( - column: $state.table.createdAt, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); -} - -class $$SkipSegmentTableTableOrderingComposer - extends OrderingComposer<_$AppDatabase, $SkipSegmentTableTable> { - $$SkipSegmentTableTableOrderingComposer(super.$state); - ColumnOrderings get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get start => $state.composableBuilder( - column: $state.table.start, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get end => $state.composableBuilder( - column: $state.table.end, - 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 createdAt => $state.composableBuilder( - column: $state.table.createdAt, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); -} - -typedef $$BlacklistTableTableInsertCompanionBuilder = BlacklistTableCompanion - Function({ - Value id, - required String name, - required BlacklistedType elementType, - required String elementId, -}); -typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion - Function({ - Value id, - Value name, - Value elementType, - Value elementId, -}); - -class $$BlacklistTableTableTableManager extends RootTableManager< - _$AppDatabase, - $BlacklistTableTable, - BlacklistTableData, - $$BlacklistTableTableFilterComposer, - $$BlacklistTableTableOrderingComposer, - $$BlacklistTableTableProcessedTableManager, - $$BlacklistTableTableInsertCompanionBuilder, - $$BlacklistTableTableUpdateCompanionBuilder> { - $$BlacklistTableTableTableManager( - _$AppDatabase db, $BlacklistTableTable table) - : super(TableManagerState( - db: db, - table: table, - filteringComposer: - $$BlacklistTableTableFilterComposer(ComposerState(db, table)), - orderingComposer: - $$BlacklistTableTableOrderingComposer(ComposerState(db, table)), - getChildManagerBuilder: (p) => - $$BlacklistTableTableProcessedTableManager(p), - getUpdateCompanionBuilder: ({ - Value id = const Value.absent(), - Value name = const Value.absent(), - Value elementType = const Value.absent(), - Value elementId = const Value.absent(), - }) => - BlacklistTableCompanion( - id: id, - name: name, - elementType: elementType, - elementId: elementId, - ), - getInsertCompanionBuilder: ({ - Value id = const Value.absent(), - required String name, - required BlacklistedType elementType, - required String elementId, - }) => - BlacklistTableCompanion.insert( - id: id, - name: name, - elementType: elementType, - elementId: elementId, - ), - )); -} - -class $$BlacklistTableTableProcessedTableManager extends ProcessedTableManager< - _$AppDatabase, - $BlacklistTableTable, - BlacklistTableData, - $$BlacklistTableTableFilterComposer, - $$BlacklistTableTableOrderingComposer, - $$BlacklistTableTableProcessedTableManager, - $$BlacklistTableTableInsertCompanionBuilder, - $$BlacklistTableTableUpdateCompanionBuilder> { - $$BlacklistTableTableProcessedTableManager(super.$state); -} - -class $$BlacklistTableTableFilterComposer - extends FilterComposer<_$AppDatabase, $BlacklistTableTable> { - $$BlacklistTableTableFilterComposer(super.$state); - ColumnFilters get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get name => $state.composableBuilder( - column: $state.table.name, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnWithTypeConverterFilters - get elementType => $state.composableBuilder( - column: $state.table.elementType, - builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( - column, - joinBuilders: joinBuilders)); - - ColumnFilters get elementId => $state.composableBuilder( - column: $state.table.elementId, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); -} - -class $$BlacklistTableTableOrderingComposer - extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> { - $$BlacklistTableTableOrderingComposer(super.$state); - ColumnOrderings get id => $state.composableBuilder( - column: $state.table.id, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get name => $state.composableBuilder( - column: $state.table.name, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get elementType => $state.composableBuilder( - column: $state.table.elementType, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get elementId => $state.composableBuilder( - column: $state.table.elementId, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); -} - class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); - $$PreferencesTableTableTableManager get preferencesTable => - $$PreferencesTableTableTableManager(_db, _db.preferencesTable); - $$SourceMatchTableTableTableManager get sourceMatchTable => - $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); - $$SkipSegmentTableTableTableManager get skipSegmentTable => - $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); + $$AuthenticationTableTableTableManager get authenticationTable => + $$AuthenticationTableTableTableManager(_db, _db.authenticationTable); $$BlacklistTableTableTableManager get blacklistTable => $$BlacklistTableTableTableManager(_db, _db.blacklistTable); + $$PreferencesTableTableTableManager get preferencesTable => + $$PreferencesTableTableTableManager(_db, _db.preferencesTable); + $$SkipSegmentTableTableTableManager get skipSegmentTable => + $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); + $$SourceMatchTableTableTableManager get sourceMatchTable => + $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); } diff --git a/lib/models/database/tables/authentication.dart b/lib/models/database/tables/authentication.dart new file mode 100644 index 00000000..96041952 --- /dev/null +++ b/lib/models/database/tables/authentication.dart @@ -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()(); +} diff --git a/lib/models/database/typeconverters/encrypted_text.dart b/lib/models/database/typeconverters/encrypted_text.dart index 27921788..6afa8210 100644 --- a/lib/models/database/typeconverters/encrypted_text.dart +++ b/lib/models/database/typeconverters/encrypted_text.dart @@ -22,6 +22,11 @@ class DecryptedText { } String encrypt() { + _encrypter ??= Encrypter( + Salsa20( + Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync), + ), + ); return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64; } } diff --git a/lib/models/database/typeconverters/string_list.dart b/lib/models/database/typeconverters/string_list.dart index 5c30a997..466ae4c4 100644 --- a/lib/models/database/typeconverters/string_list.dart +++ b/lib/models/database/typeconverters/string_list.dart @@ -5,7 +5,7 @@ class StringListConverter extends TypeConverter, String> { @override List fromSql(String fromDb) { - return fromDb.split(","); + return fromDb.split(",").where((e) => e.isNotEmpty).toList(); } @override diff --git a/lib/modules/desktop_login/login_form.dart b/lib/modules/desktop_login/login_form.dart index 6091829c..e5d31215 100644 --- a/lib/modules/desktop_login/login_form.dart +++ b/lib/modules/desktop_login/login_form.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; class TokenLoginForm extends HookConsumerWidget { final void Function()? onDone; @@ -52,10 +52,7 @@ class TokenLoginForm extends HookConsumerWidget { final cookieHeader = "sp_dc=${directCodeController.text.trim()}"; - authenticationNotifier.setCredentials( - await AuthenticationCredentials.fromCookie( - cookieHeader), - ); + await authenticationNotifier.login(cookieHeader); if (context.mounted) { onDone?.call(); } diff --git a/lib/modules/home/sections/friends.dart b/lib/modules/home/sections/friends.dart index 85325f5a..d6bed6a8 100644 --- a/lib/modules/home/sections/friends.dart +++ b/lib/modules/home/sections/friends.dart @@ -8,7 +8,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/modules/home/sections/friends/friend_item.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/models/spotify_friends.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HomePageFriendsSection extends HookConsumerWidget { @@ -59,7 +59,7 @@ class HomePageFriendsSection extends HookConsumerWidget { if (friendsQuery.isLoading || friendsQuery.asData?.value.friends.isEmpty == true || - auth == null) { + auth.asData?.value == null) { return const SliverToBoxAdapter( child: SizedBox.shrink(), ); diff --git a/lib/modules/home/sections/new_releases.dart b/lib/modules/home/sections/new_releases.dart index 08b28138..e2b32741 100644 --- a/lib/modules/home/sections/new_releases.dart +++ b/lib/modules/home/sections/new_releases.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class HomeNewReleasesSection extends HookConsumerWidget { @@ -18,7 +18,7 @@ class HomeNewReleasesSection extends HookConsumerWidget { final albums = ref.watch(userArtistAlbumReleasesProvider); - if (auth == null || + if (auth.asData?.value == null || newReleases.isLoading || newReleases.asData?.value.items.isEmpty == true) { return const SizedBox.shrink(); diff --git a/lib/modules/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart index 9408d008..a5831fc2 100644 --- a/lib/modules/library/local_folder/local_folder_item.dart +++ b/lib/modules/library/local_folder/local_folder_item.dart @@ -46,7 +46,7 @@ class LocalFolderItem extends HookConsumerWidget { ...pathSegments.skip(pathSegments.length - 3).toList() ..removeLast(), ] - : pathSegments.take(pathSegments.length - 1).toList(); + : pathSegments.take(max(pathSegments.length - 1, 0)).toList(); final trackSnapshot = ref.watch( localTracksProvider.select( diff --git a/lib/modules/library/user_albums.dart b/lib/modules/library/user_albums.dart index 71e5b65a..c2c91293 100644 --- a/lib/modules/library/user_albums.dart +++ b/lib/modules/library/user_albums.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class UserAlbums extends HookConsumerWidget { @@ -46,7 +46,7 @@ class UserAlbums extends HookConsumerWidget { []; }, [albumsQuery.asData?.value, searchText.value]); - if (auth == null) { + if (auth.asData?.value == null) { return const AnonymousFallback(); } diff --git a/lib/modules/library/user_artists.dart b/lib/modules/library/user_artists.dart index dbdd8682..dd097080 100644 --- a/lib/modules/library/user_artists.dart +++ b/lib/modules/library/user_artists.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class UserArtists extends HookConsumerWidget { @@ -48,7 +48,7 @@ class UserArtists extends HookConsumerWidget { final controller = useScrollController(); - if (auth == null) { + if (auth.asData?.value == null) { return const AnonymousFallback(); } diff --git a/lib/modules/library/user_playlists.dart b/lib/modules/library/user_playlists.dart index e0c501bb..577f9655 100644 --- a/lib/modules/library/user_playlists.dart +++ b/lib/modules/library/user_playlists.dart @@ -17,7 +17,7 @@ import 'package:spotube/components/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -75,7 +75,7 @@ class UserPlaylists extends HookConsumerWidget { final controller = useScrollController(); - if (auth == null) { + if (auth.asData?.value == null) { return const AnonymousFallback(); } diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 6a8a3e52..66344792 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -24,7 +24,7 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/volume_provider.dart'; diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 41de7388..8fd434ad 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -13,7 +13,7 @@ import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/download_manager_provider.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/sleep_timer_provider.dart'; diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index 14784176..a77ab6fe 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -18,7 +18,7 @@ import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/modules/root/sidebar.dart b/lib/modules/root/sidebar.dart index 592a3d90..ef735798 100644 --- a/lib/modules/root/sidebar.dart +++ b/lib/modules/root/sidebar.dart @@ -20,7 +20,7 @@ import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/provider/download_manager_provider.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -269,7 +269,7 @@ class SidebarFooter extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (auth != null && data == null) + if (auth.asData?.value != null && data == null) const CircularProgressIndicator() else if (data != null) Flexible( diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index a30535dd..7d7fa8ef 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/models/database/database.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/primitive_utils.dart'; diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index d78143e4..ec62543c 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/links/hyper_link.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/home/home.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/utils/service_utils.dart'; class LoginTutorial extends ConsumerWidget { @@ -18,8 +18,7 @@ class LoginTutorial extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - ref.watch(authenticationProvider); - final authenticationNotifier = ref.watch(authenticationProvider.notifier); + final auth = ref.watch(authenticationProvider); final key = GlobalKey>(); final theme = Theme.of(context); @@ -53,7 +52,7 @@ class LoginTutorial extends ConsumerWidget { ), showBackButton: true, overrideDone: FilledButton( - onPressed: authenticationNotifier.isLoggedIn + onPressed: auth.asData?.value != null ? () { ServiceUtils.pushNamed(context, HomePage.name); } @@ -91,7 +90,7 @@ class LoginTutorial extends ConsumerWidget { bodyWidget: Text(context.l10n.step_3_steps, textAlign: TextAlign.left), ), - if (authenticationNotifier.isLoggedIn) + if (auth.asData?.value != null) PageViewModel( decoration: pageDecoration.copyWith( bodyAlignment: Alignment.center, diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index f75c715c..c484046b 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -17,7 +17,7 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -84,7 +84,7 @@ class LyricsPage extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); - if (auth == null) { + if (auth.asData?.value == null) { return Scaffold( appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null, body: const AnonymousFallback(), diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index 603f90d3..f9659538 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -14,7 +14,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -48,7 +48,7 @@ class MiniLyricsPage extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); - if (auth == null) { + if (auth.asData?.value == null) { return const Scaffold( appBar: PageWindowTitleBar(), body: AnonymousFallback(), diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart index 1f2df95a..290c2b2f 100644 --- a/lib/pages/mobile_login/mobile_login.dart +++ b/lib/pages/mobile_login/mobile_login.dart @@ -3,7 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/utils/platform.dart'; class WebViewLogin extends HookConsumerWidget { @@ -53,9 +53,7 @@ class WebViewLogin extends HookConsumerWidget { final cookieHeader = "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; - authenticationNotifier.setCredentials( - await AuthenticationCredentials.fromCookie(cookieHeader), - ); + await authenticationNotifier.login(cookieHeader); if (context.mounted) { // ignore: use_build_context_synchronously GoRouter.of(context).go("/"); diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 4f53f8e6..e28a5eff 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -20,7 +20,7 @@ import 'package:spotube/pages/search/sections/albums.dart'; import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/pages/search/sections/tracks.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; @@ -37,8 +37,7 @@ class SearchPage extends HookConsumerWidget { final searchTerm = ref.watch(searchTermStateProvider); final controller = useSearchController(); - ref.watch(authenticationProvider); - final authenticationNotifier = ref.watch(authenticationProvider.notifier); + final auth = ref.watch(authenticationProvider); final mediaQuery = MediaQuery.of(context); final searchTrack = ref.watch(searchProvider(SearchType.track)); @@ -91,7 +90,7 @@ class SearchPage extends HookConsumerWidget { appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar(automaticallyImplyLeading: true) : null, - body: !authenticationNotifier.isLoggedIn + body: auth.asData?.value == null ? const AnonymousFallback() : Column( children: [ diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index a007fbeb..1604f14b 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -9,7 +9,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/profile/profile.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -35,7 +35,7 @@ class SettingsAccountSection extends HookConsumerWidget { return SectionCardWithHeading( heading: context.l10n.account, children: [ - if (auth != null) + if (auth.asData?.value != null) ListTile( leading: const Icon(SpotubeIcons.user), title: const Text("User Profile"), @@ -53,7 +53,7 @@ class SettingsAccountSection extends HookConsumerWidget { ServiceUtils.pushNamed(context, ProfilePage.name); }, ), - if (auth == null) + if (auth.asData?.value == null) LayoutBuilder(builder: (context, constrains) { return ListTile( leading: Icon( diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication/authentication.dart similarity index 51% rename from lib/provider/authentication_provider.dart rename to lib/provider/authentication/authentication.dart index 52c7f281..3ea8693b 100644 --- a/lib/provider/authentication_provider.dart +++ b/lib/provider/authentication/authentication.dart @@ -4,22 +4,30 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; +import 'package:drift/drift.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart' hide X509Certificate; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/utils/platform.dart'; -class AuthenticationCredentials { - String cookie; - String accessToken; - DateTime expiration; - +extension ExpirationAuthenticationTableData on AuthenticationTableData { bool get isExpired => DateTime.now().isAfter(expiration); + String? getCookie(String key) => cookie.value + .split("; ") + .firstWhereOrNull((c) => c.trim().startsWith("$key=")) + ?.trim() + .split("=") + .last + .replaceAll(";", ""); +} + +class AuthenticationNotifier extends AsyncNotifier { static final Dio dio = () { final dio = Dio(); @@ -32,13 +40,68 @@ class AuthenticationCredentials { return dio; }(); - AuthenticationCredentials({ - required this.cookie, - required this.accessToken, - required this.expiration, - }); + @override + build() async { + final database = ref.watch(databaseProvider); - static Future fromCookie(String cookie) async { + final data = await (database.select(database.authenticationTable) + ..where((s) => s.id.equals(0))) + .getSingleOrNull(); + + Timer? refreshTimer; + + ref.listenSelf((prevData, newData) async { + if (newData.asData?.value == null) return; + + if (newData.asData!.value!.isExpired) { + await refreshCredentials(); + } + + // set the refresh timer + refreshTimer?.cancel(); + refreshTimer = Timer( + newData.asData!.value!.expiration.difference(DateTime.now()), + () => refreshCredentials(), + ); + }); + + final subscription = + database.select(database.authenticationTable).watch().listen( + (event) { + state = AsyncData(event.isEmpty ? null : event.first); + }, + ); + + ref.onDispose(() { + subscription.cancel(); + refreshTimer?.cancel(); + }); + + return data; + } + + Future refreshCredentials() async { + final database = ref.read(databaseProvider); + final refreshedCredentials = + await credentialsFromCookie(state.asData!.value!.cookie.value); + + await database + .update(database.authenticationTable) + .replace(refreshedCredentials); + } + + Future login(String cookie) async { + final database = ref.read(databaseProvider); + final refreshedCredentials = await credentialsFromCookie(cookie); + + await database + .into(database.authenticationTable) + .insert(refreshedCredentials); + } + + Future credentialsFromCookie( + String cookie, + ) async { try { final spDc = cookie .split("; ") @@ -65,9 +128,10 @@ class AuthenticationCredentials { ); } - return AuthenticationCredentials( - cookie: "${res.headers["set-cookie"]?.join(";")}; $spDc", - accessToken: body['accessToken'], + return AuthenticationTableCompanion.insert( + id: const Value(0), + cookie: DecryptedText("${res.headers["set-cookie"]?.join(";")}; $spDc"), + accessToken: DecryptedText(body['accessToken']), expiration: DateTime.fromMillisecondsSinceEpoch( body['accessTokenExpirationTimestampMs'], ), @@ -86,102 +150,20 @@ class AuthenticationCredentials { } } - /// Returns the cookie value - String? getCookie(String key) => cookie - .split("; ") - .firstWhereOrNull((c) => c.trim().startsWith("$key=")) - ?.trim() - .split("=") - .last - .replaceAll(";", ""); - - factory AuthenticationCredentials.fromJson(Map json) { - return AuthenticationCredentials( - cookie: json['cookie'] as String, - accessToken: json['accessToken'] as String, - expiration: DateTime.parse(json['expiration'] as String), - ); - } - - Map toJson() { - return { - 'cookie': cookie, - 'accessToken': accessToken, - 'expiration': expiration.toIso8601String(), - }; - } - - AuthenticationCredentials copyWith({ - String? cookie, - String? accessToken, - DateTime? expiration, - }) { - return AuthenticationCredentials( - cookie: cookie ?? this.cookie, - accessToken: accessToken ?? this.accessToken, - expiration: expiration ?? this.expiration, - ); - } -} - -class AuthenticationNotifier - extends PersistedStateNotifier { - bool get isLoggedIn => state != null; - - AuthenticationNotifier() : super(null, "authentication", encrypted: true); - - Timer? _refreshTimer; - - @override - FutureOr onInit() async { - super.onInit(); - if (isLoggedIn && state!.isExpired) { - await refreshCredentials(); - } - - addListener((state) { - _refreshTimer?.cancel(); - if (isLoggedIn && !state!.isExpired) { - _refreshTimer = Timer( - state.expiration.difference(DateTime.now()), - () => refreshCredentials(), - ); - } - }); - } - - void setCredentials(AuthenticationCredentials credentials) { - state = credentials; - } - Future logout() async { - state = null; + state = const AsyncData(null); + final database = ref.read(databaseProvider); + await (database.delete(database.authenticationTable) + ..where((s) => s.id.equals(0))) + .go(); if (kIsMobile) { WebStorageManager.instance().deleteAllData(); CookieManager.instance().deleteAllCookies(); } } - - Future refreshCredentials() async { - if (!isLoggedIn) { - return; - } - - state = await AuthenticationCredentials.fromCookie(state!.cookie); - } - - @override - FutureOr fromJson(Map json) { - return AuthenticationCredentials.fromJson(json); - } - - @override - Map toJson() { - return state?.toJson() ?? {}; - } } final authenticationProvider = - StateNotifierProvider( - (ref) => AuthenticationNotifier(), + AsyncNotifierProvider( + () => AuthenticationNotifier(), ); diff --git a/lib/provider/custom_spotify_endpoint_provider.dart b/lib/provider/custom_spotify_endpoint_provider.dart index 4634549a..ad0c389a 100644 --- a/lib/provider/custom_spotify_endpoint_provider.dart +++ b/lib/provider/custom_spotify_endpoint_provider.dart @@ -1,10 +1,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart'; final customSpotifyEndpointProvider = Provider((ref) { ref.watch(spotifyProvider); final auth = ref.watch(authenticationProvider); - return CustomSpotifyEndpoints(auth?.accessToken ?? ""); + return CustomSpotifyEndpoints(auth.asData?.value?.accessToken.value ?? ""); }); diff --git a/lib/provider/spotify/views/home.dart b/lib/provider/spotify/views/home.dart index 51586953..ad6a076a 100644 --- a/lib/provider/spotify/views/home.dart +++ b/lib/provider/spotify/views/home.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -8,7 +8,7 @@ final homeViewProvider = FutureProvider((ref) async { userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( - authenticationProvider.select((s) => s?.getCookie("sp_t")), + authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")), ); if (spTCookie == null) return null; diff --git a/lib/provider/spotify/views/home_section.dart b/lib/provider/spotify/views/home_section.dart index 04c4cbd6..5eb9183d 100644 --- a/lib/provider/spotify/views/home_section.dart +++ b/lib/provider/spotify/views/home_section.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/models/spotify/home_feed.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -11,7 +11,7 @@ final homeSectionViewProvider = userPreferencesProvider.select((s) => s.market), ); final spTCookie = ref.watch( - authenticationProvider.select((s) => s?.getCookie("sp_t")), + authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")), ); if (spTCookie == null) return null; diff --git a/lib/provider/spotify_provider.dart b/lib/provider/spotify_provider.dart index f8b6e044..5824cce0 100644 --- a/lib/provider/spotify_provider.dart +++ b/lib/provider/spotify_provider.dart @@ -2,14 +2,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/env.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/utils/primitive_utils.dart'; final spotifyProvider = Provider((ref) { final authState = ref.watch(authenticationProvider); final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets); - if (authState == null) { + if (authState.asData?.value == null) { return SpotifyApi( SpotifyApiCredentials( anonCred["clientId"], @@ -18,5 +18,5 @@ final spotifyProvider = Provider((ref) { ); } - return SpotifyApi.withAccessToken(authState.accessToken); + return SpotifyApi.withAccessToken(authState.asData!.value!.accessToken.value); }); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 8b96305f..a730c313 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -134,8 +134,11 @@ class UserPreferencesNotifier extends Notifier { void setLocalLibraryLocation(List localLibraryDirs) { //if (localLibraryDir.isEmpty) return; - setData(PreferencesTableCompanion( - localLibraryLocation: Value(localLibraryDirs))); + setData( + PreferencesTableCompanion( + localLibraryLocation: Value(localLibraryDirs), + ), + ); } void setLayoutMode(LayoutMode mode) { diff --git a/lib/services/kv_store/encrypted_kv_store.dart b/lib/services/kv_store/encrypted_kv_store.dart index d8f69690..ab4a750e 100644 --- a/lib/services/kv_store/encrypted_kv_store.dart +++ b/lib/services/kv_store/encrypted_kv_store.dart @@ -1,6 +1,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:uuid/uuid.dart'; +import 'package:spotube/utils/platform.dart'; abstract class EncryptedKvStoreService { static const _storage = FlutterSecureStorage( @@ -9,15 +10,21 @@ abstract class EncryptedKvStoreService { ), ); - static late final String _encryptionKeySync; + static String? _encryptionKeySync; static Future initialize() async { _encryptionKeySync = await encryptionKey; } - static String get encryptionKeySync => _encryptionKeySync; + static String get encryptionKeySync => _encryptionKeySync!; + + static bool get isUnsupportedPlatform => + kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak); static Future get encryptionKey async { + if (isUnsupportedPlatform) { + return KVStoreService.encryptionKey; + } try { final value = await _storage.read(key: 'encryption'); final key = const Uuid().v4(); @@ -34,10 +41,17 @@ abstract class EncryptedKvStoreService { } static Future 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; } } } From b9b7d5c8aad013224ba749a31dcc9627ffa76e9e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 17 Jun 2024 18:08:57 +0600 Subject: [PATCH 06/24] refactor: lastfm scrobbling to drift db --- .../heart_button/use_track_toggle_like.dart | 2 +- lib/models/database/database.dart | 2 + lib/models/database/database.g.dart | 380 ++++++++++++++++++ lib/models/database/tables/scrobbler.dart | 8 + lib/pages/lastfm_login/lastfm_login.dart | 2 +- lib/pages/settings/sections/accounts.dart | 4 +- .../proxy_playlist_provider.dart | 2 +- lib/provider/scrobbler/scrobbler.dart | 130 ++++++ lib/provider/scrobbler_provider.dart | 129 ------ 9 files changed, 525 insertions(+), 134 deletions(-) create mode 100644 lib/models/database/tables/scrobbler.dart create mode 100644 lib/provider/scrobbler/scrobbler.dart delete mode 100644 lib/provider/scrobbler_provider.dart diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart index 2a886feb..ba5cbee1 100644 --- a/lib/components/heart_button/use_track_toggle_like.dart +++ b/lib/components/heart_button/use_track_toggle_like.dart @@ -1,7 +1,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; typedef UseTrackToggleLike = ({ diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 56f72ee7..e387291a 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -22,6 +22,7 @@ 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'; @@ -35,6 +36,7 @@ part 'typeconverters/encrypted_text.dart'; AuthenticationTable, BlacklistTable, PreferencesTable, + ScrobblerTable, SkipSegmentTable, SourceMatchTable, ], diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 0ac7005e..6bcfbf21 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1731,6 +1731,260 @@ class PreferencesTableCompanion extends UpdateCompanion { } } +class $ScrobblerTableTable extends ScrobblerTable + with TableInfo<$ScrobblerTableTable, ScrobblerTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ScrobblerTableTable(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 _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _usernameMeta = + const VerificationMeta('username'); + @override + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _passwordHashMeta = + const VerificationMeta('passwordHash'); + @override + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, username, passwordHash]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'scrobbler_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('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('username')) { + context.handle(_usernameMeta, + username.isAcceptableOrUnknown(data['username']!, _usernameMeta)); + } else if (isInserting) { + context.missing(_usernameMeta); + } + if (data.containsKey('password_hash')) { + context.handle( + _passwordHashMeta, + passwordHash.isAcceptableOrUnknown( + data['password_hash']!, _passwordHashMeta)); + } else if (isInserting) { + context.missing(_passwordHashMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ScrobblerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScrobblerTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + passwordHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + ); + } + + @override + $ScrobblerTableTable createAlias(String alias) { + return $ScrobblerTableTable(attachedDatabase, alias); + } +} + +class ScrobblerTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String username; + final String passwordHash; + const ScrobblerTableData( + {required this.id, + required this.createdAt, + required this.username, + required this.passwordHash}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['username'] = Variable(username); + map['password_hash'] = Variable(passwordHash); + return map; + } + + ScrobblerTableCompanion toCompanion(bool nullToAbsent) { + return ScrobblerTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + username: Value(username), + passwordHash: Value(passwordHash), + ); + } + + factory ScrobblerTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScrobblerTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + username: serializer.fromJson(json['username']), + passwordHash: serializer.fromJson(json['passwordHash']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'username': serializer.toJson(username), + 'passwordHash': serializer.toJson(passwordHash), + }; + } + + ScrobblerTableData copyWith( + {int? id, + DateTime? createdAt, + String? username, + String? passwordHash}) => + ScrobblerTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + @override + String toString() { + return (StringBuffer('ScrobblerTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, username, passwordHash); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScrobblerTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.username == this.username && + other.passwordHash == this.passwordHash); +} + +class ScrobblerTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value username; + final Value passwordHash; + const ScrobblerTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.username = const Value.absent(), + this.passwordHash = const Value.absent(), + }); + ScrobblerTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) : username = Value(username), + passwordHash = Value(passwordHash); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? username, + Expression? passwordHash, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (username != null) 'username': username, + if (passwordHash != null) 'password_hash': passwordHash, + }); + } + + ScrobblerTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? username, + Value? passwordHash}) { + return ScrobblerTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } +} + class $SkipSegmentTableTable extends SkipSegmentTable with TableInfo<$SkipSegmentTableTable, SkipSegmentTableData> { @override @@ -2324,6 +2578,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); late final $PreferencesTableTable preferencesTable = $PreferencesTableTable(this); + late final $ScrobblerTableTable scrobblerTable = $ScrobblerTableTable(this); late final $SkipSegmentTableTable skipSegmentTable = $SkipSegmentTableTable(this); late final $SourceMatchTableTable sourceMatchTable = @@ -2340,6 +2595,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { authenticationTable, blacklistTable, preferencesTable, + scrobblerTable, skipSegmentTable, sourceMatchTable, uniqueBlacklist, @@ -3081,6 +3337,128 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$ScrobblerTableTableInsertCompanionBuilder = ScrobblerTableCompanion + Function({ + Value id, + Value createdAt, + required String username, + required String passwordHash, +}); +typedef $$ScrobblerTableTableUpdateCompanionBuilder = ScrobblerTableCompanion + Function({ + Value id, + Value createdAt, + Value username, + Value passwordHash, +}); + +class $$ScrobblerTableTableTableManager extends RootTableManager< + _$AppDatabase, + $ScrobblerTableTable, + ScrobblerTableData, + $$ScrobblerTableTableFilterComposer, + $$ScrobblerTableTableOrderingComposer, + $$ScrobblerTableTableProcessedTableManager, + $$ScrobblerTableTableInsertCompanionBuilder, + $$ScrobblerTableTableUpdateCompanionBuilder> { + $$ScrobblerTableTableTableManager( + _$AppDatabase db, $ScrobblerTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$ScrobblerTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$ScrobblerTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$ScrobblerTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + Value username = const Value.absent(), + Value passwordHash = const Value.absent(), + }) => + ScrobblerTableCompanion( + id: id, + createdAt: createdAt, + username: username, + passwordHash: passwordHash, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) => + ScrobblerTableCompanion.insert( + id: id, + createdAt: createdAt, + username: username, + passwordHash: passwordHash, + ), + )); +} + +class $$ScrobblerTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $ScrobblerTableTable, + ScrobblerTableData, + $$ScrobblerTableTableFilterComposer, + $$ScrobblerTableTableOrderingComposer, + $$ScrobblerTableTableProcessedTableManager, + $$ScrobblerTableTableInsertCompanionBuilder, + $$ScrobblerTableTableUpdateCompanionBuilder> { + $$ScrobblerTableTableProcessedTableManager(super.$state); +} + +class $$ScrobblerTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $ScrobblerTableTable> { + $$ScrobblerTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get username => $state.composableBuilder( + column: $state.table.username, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$ScrobblerTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $ScrobblerTableTable> { + $$ScrobblerTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get username => $state.composableBuilder( + column: $state.table.username, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + typedef $$SkipSegmentTableTableInsertCompanionBuilder = SkipSegmentTableCompanion Function({ Value id, @@ -3370,6 +3748,8 @@ class _$AppDatabaseManager { $$BlacklistTableTableTableManager(_db, _db.blacklistTable); $$PreferencesTableTableTableManager get preferencesTable => $$PreferencesTableTableTableManager(_db, _db.preferencesTable); + $$ScrobblerTableTableTableManager get scrobblerTable => + $$ScrobblerTableTableTableManager(_db, _db.scrobblerTable); $$SkipSegmentTableTableTableManager get skipSegmentTable => $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); $$SourceMatchTableTableTableManager get sourceMatchTable => diff --git a/lib/models/database/tables/scrobbler.dart b/lib/models/database/tables/scrobbler.dart new file mode 100644 index 00000000..481c441e --- /dev/null +++ b/lib/models/database/tables/scrobbler.dart @@ -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())(); +} diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index da2e4e13..8107e627 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; class LastFMLoginPage extends HookConsumerWidget { static const name = "lastfm_login"; diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 1604f14b..b06a67f6 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -119,7 +119,7 @@ class SettingsAccountSection extends HookConsumerWidget { ), ); }), - if (scrobbler == null) + if (scrobbler.asData?.value == null) ListTile( leading: const Icon(SpotubeIcons.lastFm), title: Text(context.l10n.login_with_lastfm), diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index d52073da..067d8d44 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -11,7 +11,7 @@ import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/player_listeners.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/provider/scrobbler/scrobbler.dart b/lib/provider/scrobbler/scrobbler.dart new file mode 100644 index 00000000..d0b41c56 --- /dev/null +++ b/lib/provider/scrobbler/scrobbler.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:scrobblenaut/scrobblenaut.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/env.dart'; +import 'package:spotube/extensions/artist_simple.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/services/logger/logger.dart'; + +class ScrobblerNotifier extends AsyncNotifier { + final StreamController _scrobbleController = + StreamController.broadcast(); + @override + build() async { + final database = ref.watch(databaseProvider); + + final loginInfo = await (database.select(database.scrobblerTable) + ..where((t) => t.id.equals(0))) + .getSingleOrNull(); + + final subscription = + database.select(database.scrobblerTable).watch().listen((event) async { + if (event.isNotEmpty) { + state = await AsyncValue.guard( + () async => Scrobblenaut( + lastFM: await LastFM.authenticateWithPasswordHash( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: event.first.username, + passwordHash: event.first.passwordHash, + ), + ), + ); + } else { + state = const AsyncValue.data(null); + } + }); + + final scrobblerSubscription = + _scrobbleController.stream.listen((track) async { + try { + await state.asData?.value?.track.scrobble( + artist: track.artists!.first.name!, + track: track.name!, + album: track.album!.name!, + chosenByUser: true, + duration: track.duration, + timestamp: DateTime.now().toUtc(), + trackNumber: track.trackNumber, + ); + } catch (e, stackTrace) { + AppLogger.reportError(e, stackTrace); + } + }); + + ref.onDispose(() { + subscription.cancel(); + scrobblerSubscription.cancel(); + }); + + if (loginInfo == null) { + return null; + } + + return Scrobblenaut( + lastFM: await LastFM.authenticateWithPasswordHash( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: loginInfo.username, + passwordHash: loginInfo.passwordHash, + ), + ); + } + + Future login( + String username, + String password, + ) async { + final database = ref.read(databaseProvider); + + final lastFm = await LastFM.authenticate( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: username, + password: password, + ); + + if (!lastFm.isAuth) throw Exception("Invalid credentials"); + + await database.into(database.scrobblerTable).insert( + ScrobblerTableCompanion.insert( + id: const Value(0), + username: username, + passwordHash: lastFm.passwordHash!, + ), + ); + } + + Future logout() async { + state = const AsyncValue.data(null); + final database = ref.read(databaseProvider); + await database.delete(database.scrobblerTable).go(); + } + + void scrobble(Track track) { + _scrobbleController.add(track); + } + + Future love(Track track) async { + await state.asData?.value?.track.love( + artist: track.artists!.asString(), + track: track.name!, + ); + } + + Future unlove(Track track) async { + await state.asData?.value?.track.unLove( + artist: track.artists!.asString(), + track: track.name!, + ); + } +} + +final scrobblerProvider = + AsyncNotifierProvider( + () => ScrobblerNotifier(), +); diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart deleted file mode 100644 index ab111ea4..00000000 --- a/lib/provider/scrobbler_provider.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:async'; - -import 'package:spotube/services/logger/logger.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:scrobblenaut/scrobblenaut.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/env.dart'; -import 'package:spotube/extensions/artist_simple.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; - -class ScrobblerState { - final String username; - final String passwordHash; - - final Scrobblenaut scrobblenaut; - - ScrobblerState({ - required this.username, - required this.passwordHash, - required this.scrobblenaut, - }); - - Map toJson() { - return { - 'username': username, - 'passwordHash': passwordHash, - }; - } -} - -class ScrobblerNotifier extends PersistedStateNotifier { - final Scrobblenaut? scrobblenaut; - - /// Directly scrobbling in set state of [ProxyPlaylistNotifier] - /// brings extra latency in playback - final StreamController _scrobbleController = - StreamController.broadcast(); - - ScrobblerNotifier() - : scrobblenaut = null, - super(null, "scrobbler", encrypted: true) { - _scrobbleController.stream.listen((track) async { - try { - await state?.scrobblenaut.track.scrobble( - artist: track.artists!.first.name!, - track: track.name!, - album: track.album!.name!, - chosenByUser: true, - duration: track.duration, - timestamp: DateTime.now().toUtc(), - trackNumber: track.trackNumber, - ); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - } - }); - } - - Future login( - String username, - String password, - ) async { - final lastFm = await LastFM.authenticate( - apiKey: Env.lastFmApiKey, - apiSecret: Env.lastFmApiSecret, - username: username, - password: password, - ); - if (!lastFm.isAuth) throw Exception("Invalid credentials"); - state = ScrobblerState( - username: username, - passwordHash: lastFm.passwordHash!, - scrobblenaut: Scrobblenaut(lastFM: lastFm), - ); - } - - Future logout() async { - state = null; - } - - void scrobble(Track track) { - _scrobbleController.add(track); - } - - Future love(Track track) async { - await state?.scrobblenaut.track.love( - artist: track.artists!.asString(), - track: track.name!, - ); - } - - Future unlove(Track track) async { - await state?.scrobblenaut.track.unLove( - artist: track.artists!.asString(), - track: track.name!, - ); - } - - @override - FutureOr fromJson(Map json) async { - if (json.isEmpty) { - return null; - } - - return ScrobblerState( - username: json['username'], - passwordHash: json['passwordHash'], - scrobblenaut: Scrobblenaut( - lastFM: await LastFM.authenticateWithPasswordHash( - apiKey: Env.lastFmApiKey, - apiSecret: Env.lastFmApiSecret, - username: json["username"], - passwordHash: json["passwordHash"], - ), - ), - ); - } - - @override - Map toJson() { - return state?.toJson() ?? {}; - } -} - -final scrobblerProvider = - StateNotifierProvider( - (ref) => ScrobblerNotifier(), -); From 5936f08a92182a4cc3a0e72c2c044d4e480d2158 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 17 Jun 2024 18:13:41 +0600 Subject: [PATCH 07/24] refactor(volumeProvider): use notifier and kvstore for persistence --- lib/provider/volume_provider.dart | 29 ++++++++++------------------- lib/services/kv_store/kv_store.dart | 4 ++++ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/provider/volume_provider.dart b/lib/provider/volume_provider.dart index 464b5e42..ddd38fd9 100644 --- a/lib/provider/volume_provider.dart +++ b/lib/provider/volume_provider.dart @@ -2,31 +2,22 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/services/kv_store/kv_store.dart'; -class VolumeProvider extends PersistedStateNotifier { - VolumeProvider() : super(1, 'volume'); +class VolumeProvider extends Notifier { + VolumeProvider(); + + @override + build() { + return KVStoreService.volume; + } Future setVolume(double volume) async { state = volume; await audioPlayer.setVolume(volume); - } - - @override - FutureOr onInit() async { - await audioPlayer.setVolume(state); - } - - @override - FutureOr fromJson(Map json) { - return json['volume'] as double? ?? 0.0; - } - - @override - Map toJson() { - return {'volume': state}; + KVStoreService.setVolume(volume); } } final volumeProvider = - StateNotifierProvider((ref) => VolumeProvider()); + NotifierProvider(() => VolumeProvider()); diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index 6b19c032..2707ea4d 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -78,4 +78,8 @@ abstract class KVStoreService { static Future setIVKey(IV iv) async { await sharedPreferences.setString('iv', iv.base64); } + + static double get volume => sharedPreferences.getDouble('volume') ?? 1.0; + static Future setVolume(double value) async => + await sharedPreferences.setDouble('volume', value); } From 59041a2948b7dcaade4bb8cc9328d06a2c25f897 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 20 Jun 2024 23:30:41 +0600 Subject: [PATCH 08/24] chore: use .value for scrobble encrypted text --- lib/models/database/database.g.dart | 67 +++++++++++++++------------ lib/provider/scrobbler/scrobbler.dart | 6 +-- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 6bcfbf21..6f899066 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1763,9 +1763,12 @@ class $ScrobblerTableTable extends ScrobblerTable static const VerificationMeta _passwordHashMeta = const VerificationMeta('passwordHash'); @override - late final GeneratedColumn passwordHash = GeneratedColumn( - 'password_hash', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumnWithTypeConverter + passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $ScrobblerTableTable.$converterpasswordHash); @override List get $columns => [id, createdAt, username, passwordHash]; @override @@ -1791,14 +1794,7 @@ class $ScrobblerTableTable extends ScrobblerTable } else if (isInserting) { context.missing(_usernameMeta); } - if (data.containsKey('password_hash')) { - context.handle( - _passwordHashMeta, - passwordHash.isAcceptableOrUnknown( - data['password_hash']!, _passwordHashMeta)); - } else if (isInserting) { - context.missing(_passwordHashMeta); - } + context.handle(_passwordHashMeta, const VerificationResult.success()); return context; } @@ -1814,8 +1810,9 @@ class $ScrobblerTableTable extends ScrobblerTable .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, username: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - passwordHash: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + passwordHash: $ScrobblerTableTable.$converterpasswordHash.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}password_hash'])!), ); } @@ -1823,6 +1820,9 @@ class $ScrobblerTableTable extends ScrobblerTable $ScrobblerTableTable createAlias(String alias) { return $ScrobblerTableTable(attachedDatabase, alias); } + + static TypeConverter $converterpasswordHash = + EncryptedTextConverter(); } class ScrobblerTableData extends DataClass @@ -1830,7 +1830,7 @@ class ScrobblerTableData extends DataClass final int id; final DateTime createdAt; final String username; - final String passwordHash; + final DecryptedText passwordHash; const ScrobblerTableData( {required this.id, required this.createdAt, @@ -1842,7 +1842,10 @@ class ScrobblerTableData extends DataClass map['id'] = Variable(id); map['created_at'] = Variable(createdAt); map['username'] = Variable(username); - map['password_hash'] = Variable(passwordHash); + { + map['password_hash'] = Variable( + $ScrobblerTableTable.$converterpasswordHash.toSql(passwordHash)); + } return map; } @@ -1862,7 +1865,7 @@ class ScrobblerTableData extends DataClass id: serializer.fromJson(json['id']), createdAt: serializer.fromJson(json['createdAt']), username: serializer.fromJson(json['username']), - passwordHash: serializer.fromJson(json['passwordHash']), + passwordHash: serializer.fromJson(json['passwordHash']), ); } @override @@ -1872,7 +1875,7 @@ class ScrobblerTableData extends DataClass 'id': serializer.toJson(id), 'createdAt': serializer.toJson(createdAt), 'username': serializer.toJson(username), - 'passwordHash': serializer.toJson(passwordHash), + 'passwordHash': serializer.toJson(passwordHash), }; } @@ -1880,7 +1883,7 @@ class ScrobblerTableData extends DataClass {int? id, DateTime? createdAt, String? username, - String? passwordHash}) => + DecryptedText? passwordHash}) => ScrobblerTableData( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, @@ -1914,7 +1917,7 @@ class ScrobblerTableCompanion extends UpdateCompanion { final Value id; final Value createdAt; final Value username; - final Value passwordHash; + final Value passwordHash; const ScrobblerTableCompanion({ this.id = const Value.absent(), this.createdAt = const Value.absent(), @@ -1925,7 +1928,7 @@ class ScrobblerTableCompanion extends UpdateCompanion { this.id = const Value.absent(), this.createdAt = const Value.absent(), required String username, - required String passwordHash, + required DecryptedText passwordHash, }) : username = Value(username), passwordHash = Value(passwordHash); static Insertable custom({ @@ -1946,7 +1949,7 @@ class ScrobblerTableCompanion extends UpdateCompanion { {Value? id, Value? createdAt, Value? username, - Value? passwordHash}) { + Value? passwordHash}) { return ScrobblerTableCompanion( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, @@ -1968,7 +1971,9 @@ class ScrobblerTableCompanion extends UpdateCompanion { map['username'] = Variable(username.value); } if (passwordHash.present) { - map['password_hash'] = Variable(passwordHash.value); + map['password_hash'] = Variable($ScrobblerTableTable + .$converterpasswordHash + .toSql(passwordHash.value)); } return map; } @@ -3342,14 +3347,14 @@ typedef $$ScrobblerTableTableInsertCompanionBuilder = ScrobblerTableCompanion Value id, Value createdAt, required String username, - required String passwordHash, + required DecryptedText passwordHash, }); typedef $$ScrobblerTableTableUpdateCompanionBuilder = ScrobblerTableCompanion Function({ Value id, Value createdAt, Value username, - Value passwordHash, + Value passwordHash, }); class $$ScrobblerTableTableTableManager extends RootTableManager< @@ -3376,7 +3381,7 @@ class $$ScrobblerTableTableTableManager extends RootTableManager< Value id = const Value.absent(), Value createdAt = const Value.absent(), Value username = const Value.absent(), - Value passwordHash = const Value.absent(), + Value passwordHash = const Value.absent(), }) => ScrobblerTableCompanion( id: id, @@ -3388,7 +3393,7 @@ class $$ScrobblerTableTableTableManager extends RootTableManager< Value id = const Value.absent(), Value createdAt = const Value.absent(), required String username, - required String passwordHash, + required DecryptedText passwordHash, }) => ScrobblerTableCompanion.insert( id: id, @@ -3429,10 +3434,12 @@ class $$ScrobblerTableTableFilterComposer builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnFilters get passwordHash => $state.composableBuilder( - column: $state.table.passwordHash, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnWithTypeConverterFilters + get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); } class $$ScrobblerTableTableOrderingComposer diff --git a/lib/provider/scrobbler/scrobbler.dart b/lib/provider/scrobbler/scrobbler.dart index d0b41c56..76559d69 100644 --- a/lib/provider/scrobbler/scrobbler.dart +++ b/lib/provider/scrobbler/scrobbler.dart @@ -30,7 +30,7 @@ class ScrobblerNotifier extends AsyncNotifier { apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: event.first.username, - passwordHash: event.first.passwordHash, + passwordHash: event.first.passwordHash.value, ), ), ); @@ -70,7 +70,7 @@ class ScrobblerNotifier extends AsyncNotifier { apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: loginInfo.username, - passwordHash: loginInfo.passwordHash, + passwordHash: loginInfo.passwordHash.value, ), ); } @@ -94,7 +94,7 @@ class ScrobblerNotifier extends AsyncNotifier { ScrobblerTableCompanion.insert( id: const Value(0), username: username, - passwordHash: lastFm.passwordHash!, + passwordHash: DecryptedText(lastFm.passwordHash!), ), ); } From f79fedefd48d4ac034960fcdc257e6be0a746022 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 23 Jun 2024 12:23:28 +0600 Subject: [PATCH 09/24] chore: create new audio player centric playback notifier with drift persistence --- lib/models/connect/connect.dart | 4 +- lib/models/connect/ws_event.dart | 12 +- lib/models/database/database.dart | 6 + lib/models/database/database.g.dart | 1305 +++++++++++++++++ .../database/tables/audio_player_state.dart | 27 + lib/models/database/typeconverters/map.dart | 15 + lib/modules/player/player_controls.dart | 27 +- lib/modules/player/use_progress.dart | 20 +- lib/pages/connect/control/control.dart | 23 +- lib/provider/audio_player/audio_player.dart | 225 +++ lib/provider/audio_player/state.dart | 42 + lib/provider/connect/connect.dart | 10 +- lib/provider/tray_manager/tray_menu.dart | 8 +- lib/services/audio_player/audio_player.dart | 18 +- .../audio_player/audio_player_impl.dart | 9 +- .../audio_players_streams_mixin.dart | 6 +- lib/services/audio_player/loop_mode.dart | 90 -- .../audio_services/mobile_audio_service.dart | 23 +- 18 files changed, 1694 insertions(+), 176 deletions(-) create mode 100644 lib/models/database/tables/audio_player_state.dart create mode 100644 lib/models/database/typeconverters/map.dart create mode 100644 lib/provider/audio_player/audio_player.dart create mode 100644 lib/provider/audio_player/state.dart delete mode 100644 lib/services/audio_player/loop_mode.dart diff --git a/lib/models/connect/connect.dart b/lib/models/connect/connect.dart index 28386050..0a06be32 100644 --- a/lib/models/connect/connect.dart +++ b/lib/models/connect/connect.dart @@ -4,9 +4,9 @@ import 'dart:async'; import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spotify/spotify.dart'; +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; part 'connect.freezed.dart'; part 'connect.g.dart'; diff --git a/lib/models/connect/ws_event.dart b/lib/models/connect/ws_event.dart index 2d7213b1..c3c29e76 100644 --- a/lib/models/connect/ws_event.dart +++ b/lib/models/connect/ws_event.dart @@ -183,7 +183,7 @@ class WebSocketEvent { if (type == WsEvent.loop) { await callback( WebSocketLoopEvent( - PlaybackLoopMode.fromString(data as String), + PlaylistMode.values.firstWhere((e) => e.name == data as String), ), ); } @@ -224,12 +224,16 @@ class WebSocketEvent { } } -class WebSocketLoopEvent extends WebSocketEvent { - WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data); +class WebSocketLoopEvent extends WebSocketEvent { + WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data); WebSocketLoopEvent.fromJson(Map json) : super( - WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String)); + WsEvent.loop, + PlaylistMode.values.firstWhere( + (e) => e.name == json["data"] as String, + ), + ); @override String toJson() { diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index e387291a..98dc22dc 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:encrypt/encrypt.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; @@ -25,11 +26,13 @@ part 'tables/preferences.dart'; part 'tables/scrobbler.dart'; part 'tables/skip_segment.dart'; part 'tables/source_match.dart'; +part 'tables/audio_player_state.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; part 'typeconverters/string_list.dart'; part 'typeconverters/encrypted_text.dart'; +part 'typeconverters/map.dart'; @DriftDatabase( tables: [ @@ -39,6 +42,9 @@ part 'typeconverters/encrypted_text.dart'; ScrobblerTable, SkipSegmentTable, SourceMatchTable, + AudioPlayerStateTable, + PlaylistTable, + PlaylistMediaTable, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 6f899066..ca9d6d97 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -2575,6 +2575,839 @@ class SourceMatchTableCompanion extends UpdateCompanion { } } +class $AudioPlayerStateTableTable extends AudioPlayerStateTable + with TableInfo<$AudioPlayerStateTableTable, AudioPlayerStateTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $AudioPlayerStateTableTable(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 _playingMeta = + const VerificationMeta('playing'); + @override + late final GeneratedColumn playing = GeneratedColumn( + 'playing', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); + static const VerificationMeta _volumeMeta = const VerificationMeta('volume'); + @override + late final GeneratedColumn volume = GeneratedColumn( + 'volume', aliasedName, false, + type: DriftSqlType.double, requiredDuringInsert: true); + static const VerificationMeta _loopModeMeta = + const VerificationMeta('loopMode'); + @override + late final GeneratedColumnWithTypeConverter loopMode = + GeneratedColumn('loop_mode', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $AudioPlayerStateTableTable.$converterloopMode); + static const VerificationMeta _shuffledMeta = + const VerificationMeta('shuffled'); + @override + late final GeneratedColumn shuffled = GeneratedColumn( + 'shuffled', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + @override + List get $columns => + [id, playing, volume, loopMode, shuffled]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'audio_player_state_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('playing')) { + context.handle(_playingMeta, + playing.isAcceptableOrUnknown(data['playing']!, _playingMeta)); + } else if (isInserting) { + context.missing(_playingMeta); + } + if (data.containsKey('volume')) { + context.handle(_volumeMeta, + volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta)); + } else if (isInserting) { + context.missing(_volumeMeta); + } + context.handle(_loopModeMeta, const VerificationResult.success()); + if (data.containsKey('shuffled')) { + context.handle(_shuffledMeta, + shuffled.isAcceptableOrUnknown(data['shuffled']!, _shuffledMeta)); + } else if (isInserting) { + context.missing(_shuffledMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + AudioPlayerStateTableData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AudioPlayerStateTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playing: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, + volume: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}volume'])!, + loopMode: $AudioPlayerStateTableTable.$converterloopMode.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!), + shuffled: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + ); + } + + @override + $AudioPlayerStateTableTable createAlias(String alias) { + return $AudioPlayerStateTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $converterloopMode = + const EnumNameConverter(PlaylistMode.values); +} + +class AudioPlayerStateTableData extends DataClass + implements Insertable { + final int id; + final bool playing; + final double volume; + final PlaylistMode loopMode; + final bool shuffled; + const AudioPlayerStateTableData( + {required this.id, + required this.playing, + required this.volume, + required this.loopMode, + required this.shuffled}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playing'] = Variable(playing); + map['volume'] = Variable(volume); + { + map['loop_mode'] = Variable( + $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode)); + } + map['shuffled'] = Variable(shuffled); + return map; + } + + AudioPlayerStateTableCompanion toCompanion(bool nullToAbsent) { + return AudioPlayerStateTableCompanion( + id: Value(id), + playing: Value(playing), + volume: Value(volume), + loopMode: Value(loopMode), + shuffled: Value(shuffled), + ); + } + + factory AudioPlayerStateTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AudioPlayerStateTableData( + id: serializer.fromJson(json['id']), + playing: serializer.fromJson(json['playing']), + volume: serializer.fromJson(json['volume']), + loopMode: $AudioPlayerStateTableTable.$converterloopMode + .fromJson(serializer.fromJson(json['loopMode'])), + shuffled: serializer.fromJson(json['shuffled']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playing': serializer.toJson(playing), + 'volume': serializer.toJson(volume), + 'loopMode': serializer.toJson( + $AudioPlayerStateTableTable.$converterloopMode.toJson(loopMode)), + 'shuffled': serializer.toJson(shuffled), + }; + } + + AudioPlayerStateTableData copyWith( + {int? id, + bool? playing, + double? volume, + PlaylistMode? loopMode, + bool? shuffled}) => + AudioPlayerStateTableData( + id: id ?? this.id, + playing: playing ?? this.playing, + volume: volume ?? this.volume, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + ); + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableData(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('volume: $volume, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, playing, volume, loopMode, shuffled); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AudioPlayerStateTableData && + other.id == this.id && + other.playing == this.playing && + other.volume == this.volume && + other.loopMode == this.loopMode && + other.shuffled == this.shuffled); +} + +class AudioPlayerStateTableCompanion + extends UpdateCompanion { + final Value id; + final Value playing; + final Value volume; + final Value loopMode; + final Value shuffled; + const AudioPlayerStateTableCompanion({ + this.id = const Value.absent(), + this.playing = const Value.absent(), + this.volume = const Value.absent(), + this.loopMode = const Value.absent(), + this.shuffled = const Value.absent(), + }); + AudioPlayerStateTableCompanion.insert({ + this.id = const Value.absent(), + required bool playing, + required double volume, + required PlaylistMode loopMode, + required bool shuffled, + }) : playing = Value(playing), + volume = Value(volume), + loopMode = Value(loopMode), + shuffled = Value(shuffled); + static Insertable custom({ + Expression? id, + Expression? playing, + Expression? volume, + Expression? loopMode, + Expression? shuffled, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playing != null) 'playing': playing, + if (volume != null) 'volume': volume, + if (loopMode != null) 'loop_mode': loopMode, + if (shuffled != null) 'shuffled': shuffled, + }); + } + + AudioPlayerStateTableCompanion copyWith( + {Value? id, + Value? playing, + Value? volume, + Value? loopMode, + Value? shuffled}) { + return AudioPlayerStateTableCompanion( + id: id ?? this.id, + playing: playing ?? this.playing, + volume: volume ?? this.volume, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playing.present) { + map['playing'] = Variable(playing.value); + } + if (volume.present) { + map['volume'] = Variable(volume.value); + } + if (loopMode.present) { + map['loop_mode'] = Variable( + $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode.value)); + } + if (shuffled.present) { + map['shuffled'] = Variable(shuffled.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AudioPlayerStateTableCompanion(') + ..write('id: $id, ') + ..write('playing: $playing, ') + ..write('volume: $volume, ') + ..write('loopMode: $loopMode, ') + ..write('shuffled: $shuffled') + ..write(')')) + .toString(); + } +} + +class $PlaylistTableTable extends PlaylistTable + with TableInfo<$PlaylistTableTable, PlaylistTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlaylistTableTable(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 _audioPlayerStateIdMeta = + const VerificationMeta('audioPlayerStateId'); + @override + late final GeneratedColumn audioPlayerStateId = GeneratedColumn( + 'audio_player_state_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES audio_player_state_table (id)')); + static const VerificationMeta _indexMeta = const VerificationMeta('index'); + @override + late final GeneratedColumn index = GeneratedColumn( + 'index', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [id, audioPlayerStateId, index]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'playlist_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('audio_player_state_id')) { + context.handle( + _audioPlayerStateIdMeta, + audioPlayerStateId.isAcceptableOrUnknown( + data['audio_player_state_id']!, _audioPlayerStateIdMeta)); + } else if (isInserting) { + context.missing(_audioPlayerStateIdMeta); + } + if (data.containsKey('index')) { + context.handle( + _indexMeta, index.isAcceptableOrUnknown(data['index']!, _indexMeta)); + } else if (isInserting) { + context.missing(_indexMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PlaylistTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PlaylistTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + audioPlayerStateId: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}audio_player_state_id'])!, + index: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}index'])!, + ); + } + + @override + $PlaylistTableTable createAlias(String alias) { + return $PlaylistTableTable(attachedDatabase, alias); + } +} + +class PlaylistTableData extends DataClass + implements Insertable { + final int id; + final int audioPlayerStateId; + final int index; + const PlaylistTableData( + {required this.id, + required this.audioPlayerStateId, + required this.index}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['audio_player_state_id'] = Variable(audioPlayerStateId); + map['index'] = Variable(index); + return map; + } + + PlaylistTableCompanion toCompanion(bool nullToAbsent) { + return PlaylistTableCompanion( + id: Value(id), + audioPlayerStateId: Value(audioPlayerStateId), + index: Value(index), + ); + } + + factory PlaylistTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PlaylistTableData( + id: serializer.fromJson(json['id']), + audioPlayerStateId: serializer.fromJson(json['audioPlayerStateId']), + index: serializer.fromJson(json['index']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'audioPlayerStateId': serializer.toJson(audioPlayerStateId), + 'index': serializer.toJson(index), + }; + } + + PlaylistTableData copyWith({int? id, int? audioPlayerStateId, int? index}) => + PlaylistTableData( + id: id ?? this.id, + audioPlayerStateId: audioPlayerStateId ?? this.audioPlayerStateId, + index: index ?? this.index, + ); + @override + String toString() { + return (StringBuffer('PlaylistTableData(') + ..write('id: $id, ') + ..write('audioPlayerStateId: $audioPlayerStateId, ') + ..write('index: $index') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, audioPlayerStateId, index); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PlaylistTableData && + other.id == this.id && + other.audioPlayerStateId == this.audioPlayerStateId && + other.index == this.index); +} + +class PlaylistTableCompanion extends UpdateCompanion { + final Value id; + final Value audioPlayerStateId; + final Value index; + const PlaylistTableCompanion({ + this.id = const Value.absent(), + this.audioPlayerStateId = const Value.absent(), + this.index = const Value.absent(), + }); + PlaylistTableCompanion.insert({ + this.id = const Value.absent(), + required int audioPlayerStateId, + required int index, + }) : audioPlayerStateId = Value(audioPlayerStateId), + index = Value(index); + static Insertable custom({ + Expression? id, + Expression? audioPlayerStateId, + Expression? index, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (audioPlayerStateId != null) + 'audio_player_state_id': audioPlayerStateId, + if (index != null) 'index': index, + }); + } + + PlaylistTableCompanion copyWith( + {Value? id, Value? audioPlayerStateId, Value? index}) { + return PlaylistTableCompanion( + id: id ?? this.id, + audioPlayerStateId: audioPlayerStateId ?? this.audioPlayerStateId, + index: index ?? this.index, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (audioPlayerStateId.present) { + map['audio_player_state_id'] = Variable(audioPlayerStateId.value); + } + if (index.present) { + map['index'] = Variable(index.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlaylistTableCompanion(') + ..write('id: $id, ') + ..write('audioPlayerStateId: $audioPlayerStateId, ') + ..write('index: $index') + ..write(')')) + .toString(); + } +} + +class $PlaylistMediaTableTable extends PlaylistMediaTable + with TableInfo<$PlaylistMediaTableTable, PlaylistMediaTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlaylistMediaTableTable(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 _playlistIdMeta = + const VerificationMeta('playlistId'); + @override + late final GeneratedColumn playlistId = GeneratedColumn( + 'playlist_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES playlist_table (id)')); + static const VerificationMeta _uriMeta = const VerificationMeta('uri'); + @override + late final GeneratedColumn uri = GeneratedColumn( + 'uri', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _extrasMeta = const VerificationMeta('extras'); + @override + late final GeneratedColumnWithTypeConverter?, String> + extras = GeneratedColumn('extras', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PlaylistMediaTableTable.$converterextrasn); + static const VerificationMeta _httpHeadersMeta = + const VerificationMeta('httpHeaders'); + @override + late final GeneratedColumnWithTypeConverter?, String> + httpHeaders = GeneratedColumn('http_headers', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PlaylistMediaTableTable.$converterhttpHeadersn); + @override + List get $columns => + [id, playlistId, uri, extras, httpHeaders]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'playlist_media_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('playlist_id')) { + context.handle( + _playlistIdMeta, + playlistId.isAcceptableOrUnknown( + data['playlist_id']!, _playlistIdMeta)); + } else if (isInserting) { + context.missing(_playlistIdMeta); + } + if (data.containsKey('uri')) { + context.handle( + _uriMeta, uri.isAcceptableOrUnknown(data['uri']!, _uriMeta)); + } else if (isInserting) { + context.missing(_uriMeta); + } + context.handle(_extrasMeta, const VerificationResult.success()); + context.handle(_httpHeadersMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PlaylistMediaTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PlaylistMediaTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + playlistId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}playlist_id'])!, + uri: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}uri'])!, + extras: $PlaylistMediaTableTable.$converterextrasn.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}extras'])), + httpHeaders: $PlaylistMediaTableTable.$converterhttpHeadersn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}http_headers'])), + ); + } + + @override + $PlaylistMediaTableTable createAlias(String alias) { + return $PlaylistMediaTableTable(attachedDatabase, alias); + } + + static TypeConverter, String> $converterextras = + const MapTypeConverter(); + static TypeConverter?, String?> $converterextrasn = + NullAwareTypeConverter.wrap($converterextras); + static TypeConverter, String> $converterhttpHeaders = + const MapTypeConverter(); + static TypeConverter?, String?> $converterhttpHeadersn = + NullAwareTypeConverter.wrap($converterhttpHeaders); +} + +class PlaylistMediaTableData extends DataClass + implements Insertable { + final int id; + final int playlistId; + final String uri; + final Map? extras; + final Map? httpHeaders; + const PlaylistMediaTableData( + {required this.id, + required this.playlistId, + required this.uri, + this.extras, + this.httpHeaders}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['playlist_id'] = Variable(playlistId); + map['uri'] = Variable(uri); + if (!nullToAbsent || extras != null) { + map['extras'] = Variable( + $PlaylistMediaTableTable.$converterextrasn.toSql(extras)); + } + if (!nullToAbsent || httpHeaders != null) { + map['http_headers'] = Variable( + $PlaylistMediaTableTable.$converterhttpHeadersn.toSql(httpHeaders)); + } + return map; + } + + PlaylistMediaTableCompanion toCompanion(bool nullToAbsent) { + return PlaylistMediaTableCompanion( + id: Value(id), + playlistId: Value(playlistId), + uri: Value(uri), + extras: + extras == null && nullToAbsent ? const Value.absent() : Value(extras), + httpHeaders: httpHeaders == null && nullToAbsent + ? const Value.absent() + : Value(httpHeaders), + ); + } + + factory PlaylistMediaTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PlaylistMediaTableData( + id: serializer.fromJson(json['id']), + playlistId: serializer.fromJson(json['playlistId']), + uri: serializer.fromJson(json['uri']), + extras: serializer.fromJson?>(json['extras']), + httpHeaders: + serializer.fromJson?>(json['httpHeaders']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'playlistId': serializer.toJson(playlistId), + 'uri': serializer.toJson(uri), + 'extras': serializer.toJson?>(extras), + 'httpHeaders': serializer.toJson?>(httpHeaders), + }; + } + + PlaylistMediaTableData copyWith( + {int? id, + int? playlistId, + String? uri, + Value?> extras = const Value.absent(), + Value?> httpHeaders = const Value.absent()}) => + PlaylistMediaTableData( + id: id ?? this.id, + playlistId: playlistId ?? this.playlistId, + uri: uri ?? this.uri, + extras: extras.present ? extras.value : this.extras, + httpHeaders: httpHeaders.present ? httpHeaders.value : this.httpHeaders, + ); + @override + String toString() { + return (StringBuffer('PlaylistMediaTableData(') + ..write('id: $id, ') + ..write('playlistId: $playlistId, ') + ..write('uri: $uri, ') + ..write('extras: $extras, ') + ..write('httpHeaders: $httpHeaders') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, playlistId, uri, extras, httpHeaders); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PlaylistMediaTableData && + other.id == this.id && + other.playlistId == this.playlistId && + other.uri == this.uri && + other.extras == this.extras && + other.httpHeaders == this.httpHeaders); +} + +class PlaylistMediaTableCompanion + extends UpdateCompanion { + final Value id; + final Value playlistId; + final Value uri; + final Value?> extras; + final Value?> httpHeaders; + const PlaylistMediaTableCompanion({ + this.id = const Value.absent(), + this.playlistId = const Value.absent(), + this.uri = const Value.absent(), + this.extras = const Value.absent(), + this.httpHeaders = const Value.absent(), + }); + PlaylistMediaTableCompanion.insert({ + this.id = const Value.absent(), + required int playlistId, + required String uri, + this.extras = const Value.absent(), + this.httpHeaders = const Value.absent(), + }) : playlistId = Value(playlistId), + uri = Value(uri); + static Insertable custom({ + Expression? id, + Expression? playlistId, + Expression? uri, + Expression? extras, + Expression? httpHeaders, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (playlistId != null) 'playlist_id': playlistId, + if (uri != null) 'uri': uri, + if (extras != null) 'extras': extras, + if (httpHeaders != null) 'http_headers': httpHeaders, + }); + } + + PlaylistMediaTableCompanion copyWith( + {Value? id, + Value? playlistId, + Value? uri, + Value?>? extras, + Value?>? httpHeaders}) { + return PlaylistMediaTableCompanion( + id: id ?? this.id, + playlistId: playlistId ?? this.playlistId, + uri: uri ?? this.uri, + extras: extras ?? this.extras, + httpHeaders: httpHeaders ?? this.httpHeaders, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (playlistId.present) { + map['playlist_id'] = Variable(playlistId.value); + } + if (uri.present) { + map['uri'] = Variable(uri.value); + } + if (extras.present) { + map['extras'] = Variable( + $PlaylistMediaTableTable.$converterextrasn.toSql(extras.value)); + } + if (httpHeaders.present) { + map['http_headers'] = Variable($PlaylistMediaTableTable + .$converterhttpHeadersn + .toSql(httpHeaders.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlaylistMediaTableCompanion(') + ..write('id: $id, ') + ..write('playlistId: $playlistId, ') + ..write('uri: $uri, ') + ..write('extras: $extras, ') + ..write('httpHeaders: $httpHeaders') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); @@ -2588,6 +3421,11 @@ abstract class _$AppDatabase extends GeneratedDatabase { $SkipSegmentTableTable(this); late final $SourceMatchTableTable sourceMatchTable = $SourceMatchTableTable(this); + late final $AudioPlayerStateTableTable audioPlayerStateTable = + $AudioPlayerStateTableTable(this); + late final $PlaylistTableTable playlistTable = $PlaylistTableTable(this); + late final $PlaylistMediaTableTable playlistMediaTable = + $PlaylistMediaTableTable(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', @@ -2603,6 +3441,9 @@ abstract class _$AppDatabase extends GeneratedDatabase { scrobblerTable, skipSegmentTable, sourceMatchTable, + audioPlayerStateTable, + playlistTable, + playlistMediaTable, uniqueBlacklist, uniqTrackMatch ]; @@ -3746,6 +4587,464 @@ class $$SourceMatchTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$AudioPlayerStateTableTableInsertCompanionBuilder + = AudioPlayerStateTableCompanion Function({ + Value id, + required bool playing, + required double volume, + required PlaylistMode loopMode, + required bool shuffled, +}); +typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder + = AudioPlayerStateTableCompanion Function({ + Value id, + Value playing, + Value volume, + Value loopMode, + Value shuffled, +}); + +class $$AudioPlayerStateTableTableTableManager extends RootTableManager< + _$AppDatabase, + $AudioPlayerStateTableTable, + AudioPlayerStateTableData, + $$AudioPlayerStateTableTableFilterComposer, + $$AudioPlayerStateTableTableOrderingComposer, + $$AudioPlayerStateTableTableProcessedTableManager, + $$AudioPlayerStateTableTableInsertCompanionBuilder, + $$AudioPlayerStateTableTableUpdateCompanionBuilder> { + $$AudioPlayerStateTableTableTableManager( + _$AppDatabase db, $AudioPlayerStateTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: $$AudioPlayerStateTableTableFilterComposer( + ComposerState(db, table)), + orderingComposer: $$AudioPlayerStateTableTableOrderingComposer( + ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$AudioPlayerStateTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value playing = const Value.absent(), + Value volume = const Value.absent(), + Value loopMode = const Value.absent(), + Value shuffled = const Value.absent(), + }) => + AudioPlayerStateTableCompanion( + id: id, + playing: playing, + volume: volume, + loopMode: loopMode, + shuffled: shuffled, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required bool playing, + required double volume, + required PlaylistMode loopMode, + required bool shuffled, + }) => + AudioPlayerStateTableCompanion.insert( + id: id, + playing: playing, + volume: volume, + loopMode: loopMode, + shuffled: shuffled, + ), + )); +} + +class $$AudioPlayerStateTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $AudioPlayerStateTableTable, + AudioPlayerStateTableData, + $$AudioPlayerStateTableTableFilterComposer, + $$AudioPlayerStateTableTableOrderingComposer, + $$AudioPlayerStateTableTableProcessedTableManager, + $$AudioPlayerStateTableTableInsertCompanionBuilder, + $$AudioPlayerStateTableTableUpdateCompanionBuilder> { + $$AudioPlayerStateTableTableProcessedTableManager(super.$state); +} + +class $$AudioPlayerStateTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $AudioPlayerStateTableTable> { + $$AudioPlayerStateTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get playing => $state.composableBuilder( + column: $state.table.playing, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get volume => $state.composableBuilder( + column: $state.table.volume, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get loopMode => $state.composableBuilder( + column: $state.table.loopMode, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get shuffled => $state.composableBuilder( + column: $state.table.shuffled, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ComposableFilter playlistTableRefs( + ComposableFilter Function($$PlaylistTableTableFilterComposer f) f) { + final $$PlaylistTableTableFilterComposer composer = $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $state.db.playlistTable, + getReferencedColumn: (t) => t.audioPlayerStateId, + builder: (joinBuilder, parentComposers) => + $$PlaylistTableTableFilterComposer(ComposerState($state.db, + $state.db.playlistTable, joinBuilder, parentComposers))); + return f(composer); + } +} + +class $$AudioPlayerStateTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $AudioPlayerStateTableTable> { + $$AudioPlayerStateTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get playing => $state.composableBuilder( + column: $state.table.playing, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get volume => $state.composableBuilder( + column: $state.table.volume, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get loopMode => $state.composableBuilder( + column: $state.table.loopMode, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get shuffled => $state.composableBuilder( + column: $state.table.shuffled, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + +typedef $$PlaylistTableTableInsertCompanionBuilder = PlaylistTableCompanion + Function({ + Value id, + required int audioPlayerStateId, + required int index, +}); +typedef $$PlaylistTableTableUpdateCompanionBuilder = PlaylistTableCompanion + Function({ + Value id, + Value audioPlayerStateId, + Value index, +}); + +class $$PlaylistTableTableTableManager extends RootTableManager< + _$AppDatabase, + $PlaylistTableTable, + PlaylistTableData, + $$PlaylistTableTableFilterComposer, + $$PlaylistTableTableOrderingComposer, + $$PlaylistTableTableProcessedTableManager, + $$PlaylistTableTableInsertCompanionBuilder, + $$PlaylistTableTableUpdateCompanionBuilder> { + $$PlaylistTableTableTableManager(_$AppDatabase db, $PlaylistTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$PlaylistTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$PlaylistTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$PlaylistTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value audioPlayerStateId = const Value.absent(), + Value index = const Value.absent(), + }) => + PlaylistTableCompanion( + id: id, + audioPlayerStateId: audioPlayerStateId, + index: index, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required int audioPlayerStateId, + required int index, + }) => + PlaylistTableCompanion.insert( + id: id, + audioPlayerStateId: audioPlayerStateId, + index: index, + ), + )); +} + +class $$PlaylistTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $PlaylistTableTable, + PlaylistTableData, + $$PlaylistTableTableFilterComposer, + $$PlaylistTableTableOrderingComposer, + $$PlaylistTableTableProcessedTableManager, + $$PlaylistTableTableInsertCompanionBuilder, + $$PlaylistTableTableUpdateCompanionBuilder> { + $$PlaylistTableTableProcessedTableManager(super.$state); +} + +class $$PlaylistTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $PlaylistTableTable> { + $$PlaylistTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get index => $state.composableBuilder( + column: $state.table.index, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + $$AudioPlayerStateTableTableFilterComposer get audioPlayerStateId { + final $$AudioPlayerStateTableTableFilterComposer composer = + $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.audioPlayerStateId, + referencedTable: $state.db.audioPlayerStateTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$AudioPlayerStateTableTableFilterComposer(ComposerState( + $state.db, + $state.db.audioPlayerStateTable, + joinBuilder, + parentComposers))); + return composer; + } + + ComposableFilter playlistMediaTableRefs( + ComposableFilter Function($$PlaylistMediaTableTableFilterComposer f) f) { + final $$PlaylistMediaTableTableFilterComposer composer = $state + .composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $state.db.playlistMediaTable, + getReferencedColumn: (t) => t.playlistId, + builder: (joinBuilder, parentComposers) => + $$PlaylistMediaTableTableFilterComposer(ComposerState( + $state.db, + $state.db.playlistMediaTable, + joinBuilder, + parentComposers))); + return f(composer); + } +} + +class $$PlaylistTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $PlaylistTableTable> { + $$PlaylistTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get index => $state.composableBuilder( + column: $state.table.index, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + $$AudioPlayerStateTableTableOrderingComposer get audioPlayerStateId { + final $$AudioPlayerStateTableTableOrderingComposer composer = + $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.audioPlayerStateId, + referencedTable: $state.db.audioPlayerStateTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$AudioPlayerStateTableTableOrderingComposer(ComposerState( + $state.db, + $state.db.audioPlayerStateTable, + joinBuilder, + parentComposers))); + return composer; + } +} + +typedef $$PlaylistMediaTableTableInsertCompanionBuilder + = PlaylistMediaTableCompanion Function({ + Value id, + required int playlistId, + required String uri, + Value?> extras, + Value?> httpHeaders, +}); +typedef $$PlaylistMediaTableTableUpdateCompanionBuilder + = PlaylistMediaTableCompanion Function({ + Value id, + Value playlistId, + Value uri, + Value?> extras, + Value?> httpHeaders, +}); + +class $$PlaylistMediaTableTableTableManager extends RootTableManager< + _$AppDatabase, + $PlaylistMediaTableTable, + PlaylistMediaTableData, + $$PlaylistMediaTableTableFilterComposer, + $$PlaylistMediaTableTableOrderingComposer, + $$PlaylistMediaTableTableProcessedTableManager, + $$PlaylistMediaTableTableInsertCompanionBuilder, + $$PlaylistMediaTableTableUpdateCompanionBuilder> { + $$PlaylistMediaTableTableTableManager( + _$AppDatabase db, $PlaylistMediaTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$PlaylistMediaTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: $$PlaylistMediaTableTableOrderingComposer( + ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$PlaylistMediaTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value playlistId = const Value.absent(), + Value uri = const Value.absent(), + Value?> extras = const Value.absent(), + Value?> httpHeaders = const Value.absent(), + }) => + PlaylistMediaTableCompanion( + id: id, + playlistId: playlistId, + uri: uri, + extras: extras, + httpHeaders: httpHeaders, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + required int playlistId, + required String uri, + Value?> extras = const Value.absent(), + Value?> httpHeaders = const Value.absent(), + }) => + PlaylistMediaTableCompanion.insert( + id: id, + playlistId: playlistId, + uri: uri, + extras: extras, + httpHeaders: httpHeaders, + ), + )); +} + +class $$PlaylistMediaTableTableProcessedTableManager + extends ProcessedTableManager< + _$AppDatabase, + $PlaylistMediaTableTable, + PlaylistMediaTableData, + $$PlaylistMediaTableTableFilterComposer, + $$PlaylistMediaTableTableOrderingComposer, + $$PlaylistMediaTableTableProcessedTableManager, + $$PlaylistMediaTableTableInsertCompanionBuilder, + $$PlaylistMediaTableTableUpdateCompanionBuilder> { + $$PlaylistMediaTableTableProcessedTableManager(super.$state); +} + +class $$PlaylistMediaTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $PlaylistMediaTableTable> { + $$PlaylistMediaTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get uri => $state.composableBuilder( + column: $state.table.uri, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters?, Map, + String> + get extras => $state.composableBuilder( + column: $state.table.extras, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters?, Map, + String> + get httpHeaders => $state.composableBuilder( + column: $state.table.httpHeaders, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + $$PlaylistTableTableFilterComposer get playlistId { + final $$PlaylistTableTableFilterComposer composer = $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playlistId, + referencedTable: $state.db.playlistTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$PlaylistTableTableFilterComposer(ComposerState($state.db, + $state.db.playlistTable, joinBuilder, parentComposers))); + return composer; + } +} + +class $$PlaylistMediaTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $PlaylistMediaTableTable> { + $$PlaylistMediaTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get uri => $state.composableBuilder( + column: $state.table.uri, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get extras => $state.composableBuilder( + column: $state.table.extras, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get httpHeaders => $state.composableBuilder( + column: $state.table.httpHeaders, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + $$PlaylistTableTableOrderingComposer get playlistId { + final $$PlaylistTableTableOrderingComposer composer = + $state.composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playlistId, + referencedTable: $state.db.playlistTable, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, parentComposers) => + $$PlaylistTableTableOrderingComposer(ComposerState($state.db, + $state.db.playlistTable, joinBuilder, parentComposers))); + return composer; + } +} + class _$AppDatabaseManager { final _$AppDatabase _db; _$AppDatabaseManager(this._db); @@ -3761,4 +5060,10 @@ class _$AppDatabaseManager { $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); $$SourceMatchTableTableTableManager get sourceMatchTable => $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); + $$AudioPlayerStateTableTableTableManager get audioPlayerStateTable => + $$AudioPlayerStateTableTableTableManager(_db, _db.audioPlayerStateTable); + $$PlaylistTableTableTableManager get playlistTable => + $$PlaylistTableTableTableManager(_db, _db.playlistTable); + $$PlaylistMediaTableTableTableManager get playlistMediaTable => + $$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable); } diff --git a/lib/models/database/tables/audio_player_state.dart b/lib/models/database/tables/audio_player_state.dart new file mode 100644 index 00000000..45f5ffd9 --- /dev/null +++ b/lib/models/database/tables/audio_player_state.dart @@ -0,0 +1,27 @@ +part of '../database.dart'; + +class AudioPlayerStateTable extends Table { + IntColumn get id => integer().autoIncrement()(); + BoolColumn get playing => boolean()(); + RealColumn get volume => real()(); + TextColumn get loopMode => textEnum()(); + BoolColumn get shuffled => boolean()(); +} + +class PlaylistTable extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get audioPlayerStateId => + integer().references(AudioPlayerStateTable, #id)(); + IntColumn get index => integer()(); +} + +class PlaylistMediaTable extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get playlistId => integer().references(PlaylistTable, #id)(); + + TextColumn get uri => text()(); + TextColumn get extras => + text().nullable().map(const MapTypeConverter())(); + TextColumn get httpHeaders => + text().nullable().map(const MapTypeConverter())(); +} diff --git a/lib/models/database/typeconverters/map.dart b/lib/models/database/typeconverters/map.dart new file mode 100644 index 00000000..0b0ff7e0 --- /dev/null +++ b/lib/models/database/typeconverters/map.dart @@ -0,0 +1,15 @@ +part of '../database.dart'; + +class MapTypeConverter extends TypeConverter, String> { + const MapTypeConverter(); + + @override + fromSql(String fromDb) { + return json.decode(fromDb) as Map; + } + + @override + toSql(value) { + return json.encode(value); + } +} diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index ba69560c..a1a3ffcf 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -12,7 +13,6 @@ import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; class PlayerControls extends HookConsumerWidget { final PaletteGenerator? palette; @@ -234,38 +234,29 @@ class PlayerControls extends HookConsumerWidget { ? null : playlistNotifier.next, ), - StreamBuilder( + StreamBuilder( stream: audioPlayer.loopModeStream, builder: (context, snapshot) { - final loopMode = snapshot.data ?? PlaybackLoopMode.none; + final loopMode = snapshot.data ?? PlaylistMode.none; return IconButton( - tooltip: loopMode == PlaybackLoopMode.one + tooltip: loopMode == PlaylistMode.single ? context.l10n.loop_track - : loopMode == PlaybackLoopMode.all + : loopMode == PlaylistMode.loop ? context.l10n.repeat_playlist : null, icon: Icon( - loopMode == PlaybackLoopMode.one + loopMode == PlaylistMode.single ? SpotubeIcons.repeatOne : SpotubeIcons.repeat, ), - style: loopMode == PlaybackLoopMode.one || - loopMode == PlaybackLoopMode.all + style: loopMode == PlaylistMode.single || + loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, onPressed: playlist.isFetching == true ? null : () async { - audioPlayer.setLoopMode( - switch (loopMode) { - PlaybackLoopMode.all => - PlaybackLoopMode.one, - PlaybackLoopMode.one => - PlaybackLoopMode.none, - PlaybackLoopMode.none => - PlaybackLoopMode.all, - }, - ); + await audioPlayer.setLoopMode(loopMode); }, ); }), diff --git a/lib/modules/player/use_progress.dart b/lib/modules/player/use_progress.dart index 15a979af..eaea638e 100644 --- a/lib/modules/player/use_progress.dart +++ b/lib/modules/player/use_progress.dart @@ -1,4 +1,3 @@ -import 'package:async/async.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -19,26 +18,13 @@ import 'package:spotube/services/audio_player/audio_player.dart'; final sliderValue = position.value.inSeconds; useEffect(() { - final durationOperation = - CancelableOperation.fromFuture(audioPlayer.duration); - durationOperation.then((value) { - if (value != null) { - duration.value = value; - } - }); + duration.value = audioPlayer.duration; final durationSubscription = audioPlayer.durationStream.listen((event) { duration.value = event; }); - final positionOperation = - CancelableOperation.fromFuture(audioPlayer.position); - - positionOperation.then((value) { - if (value != null) { - position.value = value; - } - }); + position.value = audioPlayer.position; var lastPosition = position.value; @@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart'; }); return () { - positionOperation.cancel(); positionSubscription.cancel(); - durationOperation.cancel(); durationSubscription.cancel(); }; }, []); diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index eb2c48c5..d27b7867 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -16,7 +16,7 @@ import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/connect.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/utils/service_utils.dart'; class RemotePlayerQueue extends ConsumerWidget { @@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget { : connectNotifier.next, ), IconButton( - tooltip: loopMode == PlaybackLoopMode.one + tooltip: loopMode == PlaylistMode.single ? context.l10n.loop_track - : loopMode == PlaybackLoopMode.all + : loopMode == PlaylistMode.loop ? context.l10n.repeat_playlist : null, icon: Icon( - loopMode == PlaybackLoopMode.one + loopMode == PlaylistMode.single ? SpotubeIcons.repeatOne : SpotubeIcons.repeat, ), - style: loopMode == PlaybackLoopMode.one || - loopMode == PlaybackLoopMode.all + style: loopMode == PlaylistMode.single || + loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, onPressed: playlist.activeTrack == null @@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget { : () async { connectNotifier.setLoopMode( switch (loopMode) { - PlaybackLoopMode.all => - PlaybackLoopMode.one, - PlaybackLoopMode.one => - PlaybackLoopMode.none, - PlaybackLoopMode.none => - PlaybackLoopMode.all, + PlaylistMode.loop => + PlaylistMode.single, + PlaylistMode.single => + PlaylistMode.none, + PlaylistMode.none => PlaylistMode.loop, }, ); }, diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart new file mode 100644 index 00000000..747c78e6 --- /dev/null +++ b/lib/provider/audio_player/audio_player.dart @@ -0,0 +1,225 @@ +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class AudioPlayerNotifier extends Notifier { + Future _syncSavedState() async { + final database = ref.read(databaseProvider); + + var playerState = + await database.select(database.audioPlayerStateTable).getSingleOrNull(); + + if (playerState == null) { + await database.into(database.audioPlayerStateTable).insert( + AudioPlayerStateTableCompanion.insert( + playing: audioPlayer.isPlaying, + volume: audioPlayer.volume, + loopMode: audioPlayer.loopMode, + shuffled: audioPlayer.isShuffled, + id: const Value(0), + ), + ); + + playerState = + await database.select(database.audioPlayerStateTable).getSingle(); + } else { + await audioPlayer.setVolume(playerState.volume); + await audioPlayer.setLoopMode(playerState.loopMode); + await audioPlayer.setShuffle(playerState.shuffled); + } + + var playlist = + await database.select(database.playlistTable).getSingleOrNull(); + var medias = await database.select(database.playlistMediaTable).get(); + + if (playlist == null) { + await database.into(database.playlistTable).insert( + PlaylistTableCompanion.insert( + audioPlayerStateId: 0, + index: audioPlayer.playlist.index, + id: const Value(0), + ), + ); + + playlist = await database.select(database.playlistTable).getSingle(); + } + + if (medias.isEmpty && audioPlayer.playlist.medias.isNotEmpty) { + await database.batch((batch) { + batch.insertAll( + database.playlistMediaTable, + [ + for (final media in audioPlayer.playlist.medias) + PlaylistMediaTableCompanion.insert( + playlistId: playlist!.id, + uri: media.uri, + extras: Value(media.extras), + httpHeaders: Value(media.httpHeaders), + ), + ], + ); + }); + } else { + await audioPlayer.openPlaylist( + medias + .map((media) => Media( + media.uri, + extras: media.extras, + httpHeaders: media.httpHeaders, + )) + .toList(), + initialIndex: playlist.index, + ); + } + } + + Future _updatePlayerState( + AudioPlayerStateTableCompanion companion, + ) async { + final database = ref.read(databaseProvider); + + await (database.update(database.audioPlayerStateTable) + ..where((tb) => tb.id.equals(0))) + .write(companion); + } + + Future _updatePlaylist( + Playlist playlist, + ) async { + final database = ref.read(databaseProvider); + + await database.batch((batch) { + batch.update( + database.playlistTable, + PlaylistTableCompanion(index: Value(playlist.index)), + where: (tb) => tb.id.equals(0), + ); + + batch.deleteAll(database.playlistMediaTable); + + if (playlist.medias.isEmpty) return; + batch.insertAll( + database.playlistMediaTable, + [ + for (final media in playlist.medias) + PlaylistMediaTableCompanion.insert( + playlistId: 0, + uri: media.uri, + extras: Value(media.extras), + httpHeaders: Value(media.httpHeaders), + ), + ], + ); + }); + } + + @override + build() { + final subscriptions = [ + audioPlayer.playingStream.listen((playing) async { + state = state.copyWith(playing: playing); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + playing: Value(playing), + ), + ); + }), + audioPlayer.volumeStream.listen((volume) async { + state = state.copyWith(volume: volume); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + volume: Value(volume), + ), + ); + }), + audioPlayer.loopModeStream.listen((loopMode) async { + state = state.copyWith(loopMode: loopMode); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + loopMode: Value(loopMode), + ), + ); + }), + audioPlayer.shuffledStream.listen((shuffled) async { + state = state.copyWith(shuffled: shuffled); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + shuffled: Value(shuffled), + ), + ); + }), + audioPlayer.playlistStream.listen((playlist) async { + state = state.copyWith(playlist: playlist); + + await _updatePlaylist(playlist); + }), + ]; + + _syncSavedState(); + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + + return AudioPlayerState( + loopMode: audioPlayer.loopMode, + playing: audioPlayer.isPlaying, + playlist: audioPlayer.playlist, + shuffled: audioPlayer.isShuffled, + volume: audioPlayer.volume, + ); + } + + // Tracks related methods + + Future addTrack(Track track) async { + await audioPlayer.addTrack(SpotubeMedia(track)); + } + + Future addTracks(Iterable tracks) async { + for (final track in tracks) { + await addTrack(track); + } + } + + Future removeTrack(Track track) async { + final index = state.tracks.indexWhere((element) => element == track); + + if (index == -1) return; + + await audioPlayer.removeTrack(index); + } + + Future removeTracks(Iterable tracks) async { + for (final track in tracks) { + await removeTrack(track); + } + } + + Future load( + List track, { + required int initialIndex, + bool autoPlay = false, + }) async { + await audioPlayer.openPlaylist( + track.map((t) => SpotubeMedia(t)).toList(), + initialIndex: initialIndex, + autoPlay: autoPlay, + ); + } +} + +final audioPlayerProvider = NotifierProvider( + () => AudioPlayerNotifier(), +); \ No newline at end of file diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart new file mode 100644 index 00000000..3c874011 --- /dev/null +++ b/lib/provider/audio_player/state.dart @@ -0,0 +1,42 @@ +import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class AudioPlayerState { + final bool playing; + final double volume; + final PlaylistMode loopMode; + final bool shuffled; + final Playlist playlist; + + final List tracks; + + AudioPlayerState({ + required this.playing, + required this.volume, + required this.loopMode, + required this.shuffled, + required this.playlist, + List? tracks, + }) : tracks = tracks ?? + playlist.medias + .map((media) => SpotubeMedia.fromMedia(media).track) + .toList(); + + AudioPlayerState copyWith({ + bool? playing, + double? volume, + PlaylistMode? loopMode, + bool? shuffled, + Playlist? playlist, + }) { + return AudioPlayerState( + playing: playing ?? this.playing, + volume: volume ?? this.volume, + loopMode: loopMode ?? this.loopMode, + shuffled: shuffled ?? this.shuffled, + playlist: playlist ?? this.playlist, + tracks: playlist == null ? tracks : null, + ); + } +} diff --git a/lib/provider/connect/connect.dart b/lib/provider/connect/connect.dart index feb9fbd2..c6014445 100644 --- a/lib/provider/connect/connect.dart +++ b/lib/provider/connect/connect.dart @@ -1,13 +1,13 @@ import 'dart:convert'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -27,8 +27,8 @@ final shuffleProvider = StateProvider( (ref) => false, ); -final loopModeProvider = StateProvider( - (ref) => PlaybackLoopMode.none, +final loopModeProvider = StateProvider( + (ref) => PlaylistMode.none, ); final queueProvider = StateProvider( @@ -158,7 +158,7 @@ class ConnectNotifier extends AsyncNotifier { emit(WebSocketShuffleEvent(value)); } - Future setLoopMode(PlaybackLoopMode value) async { + Future setLoopMode(PlaylistMode value) async { emit(WebSocketLoopEvent(value)); } diff --git a/lib/provider/tray_manager/tray_menu.dart b/lib/provider/tray_manager/tray_menu.dart index cb793707..35aca4f5 100644 --- a/lib/provider/tray_manager/tray_menu.dart +++ b/lib/provider/tray_manager/tray_menu.dart @@ -3,11 +3,11 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; -final audioPlayerLoopMode = StreamProvider((ref) { +final audioPlayerLoopMode = StreamProvider((ref) { return audioPlayer.loopModeStream; }); @@ -23,7 +23,7 @@ final trayMenuProvider = Provider((ref) { final isPlaybackPlaying = ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null)); final isLoopOne = - ref.watch(audioPlayerLoopMode).asData?.value == PlaybackLoopMode.one; + ref.watch(audioPlayerLoopMode).asData?.value == PlaylistMode.single; final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false; final isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false; @@ -75,7 +75,7 @@ final trayMenuProvider = Provider((ref) { checked: isLoopOne, onClick: (menuItem) { audioPlayer.setLoopMode( - isLoopOne ? PlaybackLoopMode.none : PlaybackLoopMode.one, + isLoopOne ? PlaylistMode.none : PlaylistMode.single, ); }, ), diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index df23039c..713d518b 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,16 +1,16 @@ import 'dart:io'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/local_track.dart'; import 'package:spotube/services/audio_player/custom_player.dart'; import 'dart:async'; import 'package:media_kit/media_kit.dart' as mk; -import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -66,15 +66,19 @@ abstract class AudioPlayerInterface { bool get mkSupportedPlatform => _mkSupportedPlatform; - Future get duration async { + Duration get duration { return _mkPlayer.state.duration; } - Future get position async { + Playlist get playlist { + return _mkPlayer.state.playlist; + } + + Duration get position { return _mkPlayer.state.position; } - Future get bufferedPosition async { + Duration get bufferedPosition { return _mkPlayer.state.buffer; } @@ -111,8 +115,8 @@ abstract class AudioPlayerInterface { return _mkPlayer.shuffled; } - PlaybackLoopMode get loopMode { - return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode); + PlaylistMode get loopMode { + return _mkPlayer.state.playlistMode; } /// Returns the current volume of the player, between 0 and 1 diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index 58868aed..82c8c906 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -65,7 +65,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface } String? get nextSource { - if (loopMode == PlaybackLoopMode.all && + if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == _mkPlayer.state.playlist.medias.length - 1) { return sources.first; @@ -77,8 +77,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface } String? get previousSource { - if (loopMode == PlaybackLoopMode.all && - _mkPlayer.state.playlist.index == 0) { + if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) { return sources.last; } @@ -125,8 +124,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface await _mkPlayer.setShuffle(shuffle); } - Future setLoopMode(PlaybackLoopMode loop) async { - await _mkPlayer.setPlaylistMode(loop.toPlaylistMode()); + Future setLoopMode(PlaylistMode loop) async { + await _mkPlayer.setPlaylistMode(loop); } Future setAudioNormalization(bool normalize) async { diff --git a/lib/services/audio_player/audio_players_streams_mixin.dart b/lib/services/audio_player/audio_players_streams_mixin.dart index f6fe0630..03ce0d5d 100644 --- a/lib/services/audio_player/audio_players_streams_mixin.dart +++ b/lib/services/audio_player/audio_players_streams_mixin.dart @@ -71,12 +71,12 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface { // } } - Stream get loopModeStream { + Stream get loopModeStream { // if (mkSupportedPlatform) { - return _mkPlayer.stream.playlistMode.map(PlaybackLoopMode.fromPlaylistMode); + return _mkPlayer.stream.playlistMode; // } else { // return _justAudio!.loopModeStream - // .map(PlaybackLoopMode.fromLoopMode) + // .map(PlaylistMode.fromLoopMode) // ; // } } diff --git a/lib/services/audio_player/loop_mode.dart b/lib/services/audio_player/loop_mode.dart deleted file mode 100644 index 78da43ba..00000000 --- a/lib/services/audio_player/loop_mode.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:media_kit/media_kit.dart'; - -/// An unified loop mode for both [LoopMode] and [PlaylistMode] -enum PlaybackLoopMode { - all, - one, - none; - - // static PlaybackLoopMode fromLoopMode(LoopMode loopMode) { - // switch (loopMode) { - // case LoopMode.all: - // return PlaybackLoopMode.all; - // case LoopMode.one: - // return PlaybackLoopMode.one; - // case LoopMode.off: - // return PlaybackLoopMode.none; - // } - // } - - // LoopMode toLoopMode() { - // switch (this) { - // case PlaybackLoopMode.all: - // return LoopMode.all; - // case PlaybackLoopMode.one: - // return LoopMode.one; - // case PlaybackLoopMode.none: - // return LoopMode.off; - // } - // } - - static PlaybackLoopMode fromPlaylistMode(PlaylistMode mode) { - switch (mode) { - case PlaylistMode.single: - return PlaybackLoopMode.one; - case PlaylistMode.loop: - return PlaybackLoopMode.all; - case PlaylistMode.none: - return PlaybackLoopMode.none; - } - } - - PlaylistMode toPlaylistMode() { - switch (this) { - case PlaybackLoopMode.all: - return PlaylistMode.loop; - case PlaybackLoopMode.one: - return PlaylistMode.single; - case PlaybackLoopMode.none: - return PlaylistMode.none; - } - } - - static PlaybackLoopMode fromAudioServiceRepeatMode( - AudioServiceRepeatMode mode) { - switch (mode) { - case AudioServiceRepeatMode.all: - case AudioServiceRepeatMode.group: - return PlaybackLoopMode.all; - case AudioServiceRepeatMode.one: - return PlaybackLoopMode.one; - case AudioServiceRepeatMode.none: - return PlaybackLoopMode.none; - } - } - - AudioServiceRepeatMode toAudioServiceRepeatMode() { - switch (this) { - case PlaybackLoopMode.all: - return AudioServiceRepeatMode.all; - case PlaybackLoopMode.one: - return AudioServiceRepeatMode.one; - case PlaybackLoopMode.none: - return AudioServiceRepeatMode.none; - } - } - - static PlaybackLoopMode fromString(String? value) { - switch (value) { - case 'all': - return PlaybackLoopMode.all; - case 'one': - return PlaybackLoopMode.one; - case 'none': - return PlaybackLoopMode.none; - default: - return PlaybackLoopMode.none; - } - } -} diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index 62cc8552..3dbae18f 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -5,7 +5,7 @@ import 'package:audio_session/audio_session.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/audio_player/loop_mode.dart'; +import 'package:media_kit/media_kit.dart' hide Track; class MobileAudioService extends BaseAudioHandler { AudioSession? session; @@ -91,9 +91,13 @@ class MobileAudioService extends BaseAudioHandler { @override Future setRepeatMode(AudioServiceRepeatMode repeatMode) async { super.setRepeatMode(repeatMode); - audioPlayer.setLoopMode( - PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode), - ); + audioPlayer.setLoopMode(switch (repeatMode) { + AudioServiceRepeatMode.all || + AudioServiceRepeatMode.group => + PlaylistMode.loop, + AudioServiceRepeatMode.one => PlaylistMode.single, + _ => PlaylistMode.none, + }); } @override @@ -120,7 +124,6 @@ class MobileAudioService extends BaseAudioHandler { } Future _transformEvent() async { - final position = (await audioPlayer.position) ?? Duration.zero; return PlaybackState( controls: [ MediaControl.skipToPrevious, @@ -133,12 +136,16 @@ class MobileAudioService extends BaseAudioHandler { }, androidCompactActionIndices: const [0, 1, 2], playing: audioPlayer.isPlaying, - updatePosition: position, - bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero, + updatePosition: audioPlayer.position, + bufferedPosition: audioPlayer.bufferedPosition, shuffleMode: audioPlayer.isShuffled == true ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, - repeatMode: (audioPlayer.loopMode).toAudioServiceRepeatMode(), + repeatMode: switch (audioPlayer.loopMode) { + PlaylistMode.loop => AudioServiceRepeatMode.all, + PlaylistMode.single => AudioServiceRepeatMode.one, + _ => AudioServiceRepeatMode.none, + }, processingState: playlist.isFetching == true ? AudioProcessingState.loading : AudioProcessingState.ready, From a83dd64476486bdad88b0c0f2afb56e2b90e7f0f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 20:52:40 +0600 Subject: [PATCH 10/24] refactor: replace all instances of proxy playlist --- lib/collections/intents.dart | 8 +- lib/components/track_tile/track_options.dart | 14 +- lib/components/track_tile/track_tile.dart | 11 +- .../sections/body/track_view_body.dart | 6 +- .../sections/body/track_view_options.dart | 4 +- .../sections/header/header_actions.dart | 8 +- .../sections/header/header_buttons.dart | 6 +- .../configurators/use_endless_playback.dart | 16 +- lib/main.dart | 4 +- lib/models/connect/connect.dart | 2 +- lib/models/connect/ws_event.dart | 6 +- lib/models/database/database.g.dart | 128 ++++++----- .../database/tables/audio_player_state.dart | 2 +- lib/modules/album/album_card.dart | 8 +- lib/modules/player/player.dart | 21 +- lib/modules/player/player_actions.dart | 8 +- lib/modules/player/player_controls.dart | 24 +- lib/modules/player/player_overlay.dart | 16 +- lib/modules/player/player_queue.dart | 15 +- lib/modules/player/player_track_details.dart | 4 +- lib/modules/player/sibling_tracks_sheet.dart | 17 +- lib/modules/playlist/playlist_card.dart | 10 +- lib/modules/root/bottom_player.dart | 6 +- lib/pages/artist/section/header.dart | 2 +- lib/pages/artist/section/top_tracks.dart | 6 +- lib/pages/library/local_folder.dart | 8 +- .../playlist_generate_result.dart | 13 +- lib/pages/lyrics/lyrics.dart | 6 +- lib/pages/lyrics/mini_lyrics.dart | 11 +- lib/pages/lyrics/plain_lyrics.dart | 4 +- lib/pages/lyrics/synced_lyrics.dart | 6 +- lib/pages/root/root_app.dart | 8 +- lib/pages/search/sections/tracks.dart | 6 +- lib/pages/track/track.dart | 6 +- lib/provider/audio_player/audio_player.dart | 137 +++++++++-- .../audio_player_streams.dart} | 87 ++++--- lib/provider/audio_player/state.dart | 66 +++++- lib/provider/connect/connect.dart | 13 +- lib/provider/discord_provider.dart | 4 +- .../proxy_playlist/proxy_playlist.dart | 101 -------- .../proxy_playlist_provider.dart | 215 ------------------ lib/provider/server/active_sourced_track.dart | 4 +- lib/provider/server/routes/connect.dart | 28 +-- lib/provider/server/routes/playback.dart | 6 +- lib/provider/server/sourced_track.dart | 7 +- .../skip_segments.dart | 0 lib/provider/tray_manager/tray_menu.dart | 10 +- .../user_preferences_provider.dart | 5 +- .../audio_services/audio_services.dart | 4 +- .../audio_services/mobile_audio_service.dart | 20 +- .../audio_services/windows_audio_service.dart | 12 +- 51 files changed, 515 insertions(+), 624 deletions(-) rename lib/provider/{proxy_playlist/player_listeners.dart => audio_player/audio_player_streams.dart} (51%) delete mode 100644 lib/provider/proxy_playlist/proxy_playlist.dart delete mode 100644 lib/provider/proxy_playlist/proxy_playlist_provider.dart rename lib/provider/{proxy_playlist => skip_segments}/skip_segments.dart (100%) diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 6d6e643e..1a44a846 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -11,7 +11,7 @@ import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/library/library.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/search/search.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; @@ -96,8 +96,8 @@ class SeekIntent extends Intent { class SeekAction extends Action { @override invoke(intent) async { - final playlist = intent.ref.read(proxyPlaylistProvider); - if (playlist.isFetching) { + final playlist = intent.ref.read(audioPlayerProvider.notifier); + if (playlist.isFetching()) { DirectionalFocusAction().invoke( DirectionalFocusIntent( intent.forward ? TraversalDirection.right : TraversalDirection.left, @@ -105,7 +105,7 @@ class SeekAction extends Action { ); return null; } - final position = (await audioPlayer.position ?? Duration.zero).inSeconds; + final position = audioPlayer.position.inSeconds; await audioPlayer.seek( Duration( seconds: intent.forward ? position + 5 : position - 5, diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index d54a0c15..c6cfdd35 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -24,7 +24,7 @@ import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify_provider.dart'; @@ -96,8 +96,8 @@ class TrackOptions extends HookConsumerWidget { WidgetRef ref, Track track, ) async { - final playback = ref.read(proxyPlaylistProvider.notifier); - final playlist = ref.read(proxyPlaylistProvider); + final playback = ref.read(audioPlayerProvider.notifier); + final playlist = ref.read(audioPlayerProvider); final spotify = ref.read(spotifyProvider); final query = "${track.name} Radio"; final pages = @@ -160,8 +160,8 @@ class TrackOptions extends HookConsumerWidget { final router = GoRouter.of(context); final ThemeData(:colorScheme) = Theme.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playback = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playback = ref.watch(audioPlayerProvider.notifier); final auth = ref.watch(authenticationProvider); ref.watch(downloadManagerProvider); final downloadManager = ref.watch(downloadManagerProvider.notifier); @@ -364,7 +364,7 @@ class TrackOptions extends HookConsumerWidget { : context.l10n.save_as_favorite, ), ), - if (auth != null && !isLocalTrack) ...[ + if (auth.asData?.value != null && !isLocalTrack) ...[ PopSheetEntry( value: TrackOptionValue.startRadio, leading: const Icon(SpotubeIcons.radio), @@ -376,7 +376,7 @@ class TrackOptions extends HookConsumerWidget { title: Text(context.l10n.add_to_playlist), ), ], - if (userPlaylist && auth != null && !isLocalTrack) + if (userPlaylist && auth.asData?.value != null && !isLocalTrack) PopSheetEntry( value: TrackOptionValue.removeFromPlaylist, leading: const Icon(SpotubeIcons.removeFilled), diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index e2e7e293..cdc18d9b 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -17,8 +17,9 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; class TrackTile extends HookConsumerWidget { /// [index] will not be shown if null @@ -30,7 +31,7 @@ class TrackTile extends HookConsumerWidget { final VoidCallback? onLongPress; final bool userPlaylist; final String? playlistId; - final ProxyPlaylist playlist; + final AudioPlayerState playlist; final List? leadingActions; @@ -160,7 +161,11 @@ class TrackTile extends HookConsumerWidget { child: Skeleton.ignore( child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: (isPlaying && playlist.isFetching) || + child: (isPlaying && + ref + .watch(audioPlayerProvider + .notifier) + .isFetching()) || isLoading.value ? const SizedBox( width: 26, diff --git a/lib/components/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart index 0c3cca4e..a6089cc3 100644 --- a/lib/components/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/tracks_view/sections/body/track_view_body.dart @@ -18,7 +18,7 @@ import 'package:spotube/components/tracks_view/track_view_provider.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -27,8 +27,8 @@ class TrackViewBodySection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final props = InheritedTrackView.of(context); final trackViewState = ref.watch(trackViewProvider(props.tracks)); diff --git a/lib/components/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart index 1accba34..98ddca25 100644 --- a/lib/components/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class TrackViewBodyOptions extends HookConsumerWidget { @@ -24,7 +24,7 @@ class TrackViewBodyOptions extends HookConsumerWidget { ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final audioSource = ref.watch(userPreferencesProvider.select((s) => s.audioSource)); diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index f20cd553..6769ed52 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; class TrackViewHeaderActions extends HookConsumerWidget { const TrackViewHeaderActions({super.key}); @@ -20,8 +20,8 @@ class TrackViewHeaderActions extends HookConsumerWidget { Widget build(BuildContext context, ref) { final props = InheritedTrackView.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final isActive = playlist.collections.contains(props.collectionId); @@ -73,7 +73,7 @@ class TrackViewHeaderActions extends HookConsumerWidget { } }, ), - if (props.onHeart != null && auth != null) + if (props.onHeart != null && auth.asData?.value != null) HeartButton( isLiked: props.isLiked, icon: isUserPlaylist ? SpotubeIcons.trash : null, diff --git a/lib/components/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart index aa660f01..aabca20f 100644 --- a/lib/components/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/tracks_view/sections/header/header_buttons.dart @@ -13,7 +13,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class TrackViewHeaderButtons extends HookConsumerWidget { @@ -28,8 +28,8 @@ class TrackViewHeaderButtons extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final props = InheritedTrackView.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.watch(playbackHistoryProvider.notifier); final isActive = playlist.collections.contains(props.collectionId); diff --git a/lib/hooks/configurators/use_endless_playback.dart b/lib/hooks/configurators/use_endless_playback.dart index 9b90b23d..e2fb1e6e 100644 --- a/lib/hooks/configurators/use_endless_playback.dart +++ b/lib/hooks/configurators/use_endless_playback.dart @@ -3,15 +3,15 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; void useEndlessPlayback(WidgetRef ref) { final auth = ref.watch(authenticationProvider); - final playback = ref.watch(proxyPlaylistProvider.notifier); - final playlist = ref.watch(proxyPlaylistProvider); + final playback = ref.watch(audioPlayerProvider.notifier); + final playlist = ref.watch(audioPlayerProvider.select((s) => s.playlist)); final spotify = ref.watch(spotifyProvider); final endlessPlayback = ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback)); @@ -22,7 +22,7 @@ void useEndlessPlayback(WidgetRef ref) { void listener(int index) async { try { - final playlist = ref.read(proxyPlaylistProvider); + final playlist = ref.read(audioPlayerProvider); if (index != playlist.tracks.length - 1) return; final track = playlist.tracks.last; @@ -56,7 +56,7 @@ void useEndlessPlayback(WidgetRef ref) { await playback.addTracks( tracks.toList() ..removeWhere((e) { - final playlist = ref.read(proxyPlaylistProvider); + final playlist = ref.read(audioPlayerProvider); final isDuplicate = playlist.tracks.any((t) => t.id == e.id); return e.id == track.id || isDuplicate; }), @@ -69,9 +69,9 @@ void useEndlessPlayback(WidgetRef ref) { // Sometimes user can change settings for which the currentIndexChanged // might not be called. So we need to check if the current track is the // last track and if it is then we need to call the listener manually. - if (playlist.active == playlist.tracks.length - 1 && + if (playlist.index == playlist.medias.length - 1 && audioPlayer.isPlaying) { - listener(playlist.active!); + listener(playlist.index); } final subscription = @@ -82,7 +82,7 @@ void useEndlessPlayback(WidgetRef ref) { [ spotify, playback, - playlist.tracks, + playlist.medias, endlessPlayback, auth, ], diff --git a/lib/main.dart b/lib/main.dart index 09db495c..9b92a21d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart'; import 'package:spotube/hooks/configurators/use_deep_linking.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; +import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; @@ -113,9 +114,10 @@ class Spotube extends HookConsumerWidget { ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); final router = ref.watch(routerProvider); - ref.listen(serverProvider, (_, __) {}); + ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); + ref.listen(serverProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); useDisableBatteryOptimizations(); diff --git a/lib/models/connect/connect.dart b/lib/models/connect/connect.dart index 0a06be32..a70520ad 100644 --- a/lib/models/connect/connect.dart +++ b/lib/models/connect/connect.dart @@ -6,7 +6,7 @@ import 'dart:convert'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotify/spotify.dart' hide Playlist; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; +import 'package:spotube/provider/audio_player/state.dart'; part 'connect.freezed.dart'; part 'connect.g.dart'; diff --git a/lib/models/connect/ws_event.dart b/lib/models/connect/ws_event.dart index c3c29e76..d1047646 100644 --- a/lib/models/connect/ws_event.dart +++ b/lib/models/connect/ws_event.dart @@ -325,12 +325,12 @@ class WebSocketErrorEvent extends WebSocketEvent { WebSocketErrorEvent(String data) : super(WsEvent.error, data); } -class WebSocketQueueEvent extends WebSocketEvent { - WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data); +class WebSocketQueueEvent extends WebSocketEvent { + WebSocketQueueEvent(AudioPlayerState data) : super(WsEvent.queue, data); factory WebSocketQueueEvent.fromJson(Map json) => WebSocketQueueEvent( - ProxyPlaylist.fromJsonRaw(json), + AudioPlayerState.fromJson(json), ); } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index ca9d6d97..37cc930c 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -2599,11 +2599,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("playing" IN (0, 1))')); - static const VerificationMeta _volumeMeta = const VerificationMeta('volume'); - @override - late final GeneratedColumn volume = GeneratedColumn( - 'volume', aliasedName, false, - type: DriftSqlType.double, requiredDuringInsert: true); static const VerificationMeta _loopModeMeta = const VerificationMeta('loopMode'); @override @@ -2621,9 +2616,17 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("shuffled" IN (0, 1))')); + static const VerificationMeta _collectionsMeta = + const VerificationMeta('collections'); + @override + late final GeneratedColumnWithTypeConverter, String> + collections = GeneratedColumn('collections', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $AudioPlayerStateTableTable.$convertercollections); @override List get $columns => - [id, playing, volume, loopMode, shuffled]; + [id, playing, loopMode, shuffled, collections]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2644,12 +2647,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable } else if (isInserting) { context.missing(_playingMeta); } - if (data.containsKey('volume')) { - context.handle(_volumeMeta, - volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta)); - } else if (isInserting) { - context.missing(_volumeMeta); - } context.handle(_loopModeMeta, const VerificationResult.success()); if (data.containsKey('shuffled')) { context.handle(_shuffledMeta, @@ -2657,6 +2654,7 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable } else if (isInserting) { context.missing(_shuffledMeta); } + context.handle(_collectionsMeta, const VerificationResult.success()); return context; } @@ -2671,13 +2669,14 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable .read(DriftSqlType.int, data['${effectivePrefix}id'])!, playing: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}playing'])!, - volume: attachedDatabase.typeMapping - .read(DriftSqlType.double, data['${effectivePrefix}volume'])!, loopMode: $AudioPlayerStateTableTable.$converterloopMode.fromSql( attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}loop_mode'])!), shuffled: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}shuffled'])!, + collections: $AudioPlayerStateTableTable.$convertercollections.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}collections'])!), ); } @@ -2688,32 +2687,37 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable static JsonTypeConverter2 $converterloopMode = const EnumNameConverter(PlaylistMode.values); + static TypeConverter, String> $convertercollections = + const StringListConverter(); } class AudioPlayerStateTableData extends DataClass implements Insertable { final int id; final bool playing; - final double volume; final PlaylistMode loopMode; final bool shuffled; + final List collections; const AudioPlayerStateTableData( {required this.id, required this.playing, - required this.volume, required this.loopMode, - required this.shuffled}); + required this.shuffled, + required this.collections}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); map['playing'] = Variable(playing); - map['volume'] = Variable(volume); { map['loop_mode'] = Variable( $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode)); } map['shuffled'] = Variable(shuffled); + { + map['collections'] = Variable( + $AudioPlayerStateTableTable.$convertercollections.toSql(collections)); + } return map; } @@ -2721,9 +2725,9 @@ class AudioPlayerStateTableData extends DataClass return AudioPlayerStateTableCompanion( id: Value(id), playing: Value(playing), - volume: Value(volume), loopMode: Value(loopMode), shuffled: Value(shuffled), + collections: Value(collections), ); } @@ -2733,10 +2737,10 @@ class AudioPlayerStateTableData extends DataClass return AudioPlayerStateTableData( id: serializer.fromJson(json['id']), playing: serializer.fromJson(json['playing']), - volume: serializer.fromJson(json['volume']), loopMode: $AudioPlayerStateTableTable.$converterloopMode .fromJson(serializer.fromJson(json['loopMode'])), shuffled: serializer.fromJson(json['shuffled']), + collections: serializer.fromJson>(json['collections']), ); } @override @@ -2745,103 +2749,103 @@ class AudioPlayerStateTableData extends DataClass return { 'id': serializer.toJson(id), 'playing': serializer.toJson(playing), - 'volume': serializer.toJson(volume), 'loopMode': serializer.toJson( $AudioPlayerStateTableTable.$converterloopMode.toJson(loopMode)), 'shuffled': serializer.toJson(shuffled), + 'collections': serializer.toJson>(collections), }; } AudioPlayerStateTableData copyWith( {int? id, bool? playing, - double? volume, PlaylistMode? loopMode, - bool? shuffled}) => + bool? shuffled, + List? collections}) => AudioPlayerStateTableData( id: id ?? this.id, playing: playing ?? this.playing, - volume: volume ?? this.volume, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, ); @override String toString() { return (StringBuffer('AudioPlayerStateTableData(') ..write('id: $id, ') ..write('playing: $playing, ') - ..write('volume: $volume, ') ..write('loopMode: $loopMode, ') - ..write('shuffled: $shuffled') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, playing, volume, loopMode, shuffled); + int get hashCode => Object.hash(id, playing, loopMode, shuffled, collections); @override bool operator ==(Object other) => identical(this, other) || (other is AudioPlayerStateTableData && other.id == this.id && other.playing == this.playing && - other.volume == this.volume && other.loopMode == this.loopMode && - other.shuffled == this.shuffled); + other.shuffled == this.shuffled && + other.collections == this.collections); } class AudioPlayerStateTableCompanion extends UpdateCompanion { final Value id; final Value playing; - final Value volume; final Value loopMode; final Value shuffled; + final Value> collections; const AudioPlayerStateTableCompanion({ this.id = const Value.absent(), this.playing = const Value.absent(), - this.volume = const Value.absent(), this.loopMode = const Value.absent(), this.shuffled = const Value.absent(), + this.collections = const Value.absent(), }); AudioPlayerStateTableCompanion.insert({ this.id = const Value.absent(), required bool playing, - required double volume, required PlaylistMode loopMode, required bool shuffled, + required List collections, }) : playing = Value(playing), - volume = Value(volume), loopMode = Value(loopMode), - shuffled = Value(shuffled); + shuffled = Value(shuffled), + collections = Value(collections); static Insertable custom({ Expression? id, Expression? playing, - Expression? volume, Expression? loopMode, Expression? shuffled, + Expression? collections, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (playing != null) 'playing': playing, - if (volume != null) 'volume': volume, if (loopMode != null) 'loop_mode': loopMode, if (shuffled != null) 'shuffled': shuffled, + if (collections != null) 'collections': collections, }); } AudioPlayerStateTableCompanion copyWith( {Value? id, Value? playing, - Value? volume, Value? loopMode, - Value? shuffled}) { + Value? shuffled, + Value>? collections}) { return AudioPlayerStateTableCompanion( id: id ?? this.id, playing: playing ?? this.playing, - volume: volume ?? this.volume, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, + collections: collections ?? this.collections, ); } @@ -2854,9 +2858,6 @@ class AudioPlayerStateTableCompanion if (playing.present) { map['playing'] = Variable(playing.value); } - if (volume.present) { - map['volume'] = Variable(volume.value); - } if (loopMode.present) { map['loop_mode'] = Variable( $AudioPlayerStateTableTable.$converterloopMode.toSql(loopMode.value)); @@ -2864,6 +2865,11 @@ class AudioPlayerStateTableCompanion if (shuffled.present) { map['shuffled'] = Variable(shuffled.value); } + if (collections.present) { + map['collections'] = Variable($AudioPlayerStateTableTable + .$convertercollections + .toSql(collections.value)); + } return map; } @@ -2872,9 +2878,9 @@ class AudioPlayerStateTableCompanion return (StringBuffer('AudioPlayerStateTableCompanion(') ..write('id: $id, ') ..write('playing: $playing, ') - ..write('volume: $volume, ') ..write('loopMode: $loopMode, ') - ..write('shuffled: $shuffled') + ..write('shuffled: $shuffled, ') + ..write('collections: $collections') ..write(')')) .toString(); } @@ -4591,17 +4597,17 @@ typedef $$AudioPlayerStateTableTableInsertCompanionBuilder = AudioPlayerStateTableCompanion Function({ Value id, required bool playing, - required double volume, required PlaylistMode loopMode, required bool shuffled, + required List collections, }); typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder = AudioPlayerStateTableCompanion Function({ Value id, Value playing, - Value volume, Value loopMode, Value shuffled, + Value> collections, }); class $$AudioPlayerStateTableTableTableManager extends RootTableManager< @@ -4627,30 +4633,30 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager< getUpdateCompanionBuilder: ({ Value id = const Value.absent(), Value playing = const Value.absent(), - Value volume = const Value.absent(), Value loopMode = const Value.absent(), Value shuffled = const Value.absent(), + Value> collections = const Value.absent(), }) => AudioPlayerStateTableCompanion( id: id, playing: playing, - volume: volume, loopMode: loopMode, shuffled: shuffled, + collections: collections, ), getInsertCompanionBuilder: ({ Value id = const Value.absent(), required bool playing, - required double volume, required PlaylistMode loopMode, required bool shuffled, + required List collections, }) => AudioPlayerStateTableCompanion.insert( id: id, playing: playing, - volume: volume, loopMode: loopMode, shuffled: shuffled, + collections: collections, ), )); } @@ -4681,11 +4687,6 @@ class $$AudioPlayerStateTableTableFilterComposer builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnFilters get volume => $state.composableBuilder( - column: $state.table.volume, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnWithTypeConverterFilters get loopMode => $state.composableBuilder( column: $state.table.loopMode, @@ -4698,6 +4699,13 @@ class $$AudioPlayerStateTableTableFilterComposer builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnWithTypeConverterFilters, List, String> + get collections => $state.composableBuilder( + column: $state.table.collections, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + ComposableFilter playlistTableRefs( ComposableFilter Function($$PlaylistTableTableFilterComposer f) f) { final $$PlaylistTableTableFilterComposer composer = $state.composerBuilder( @@ -4725,11 +4733,6 @@ class $$AudioPlayerStateTableTableOrderingComposer builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get volume => $state.composableBuilder( - column: $state.table.volume, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get loopMode => $state.composableBuilder( column: $state.table.loopMode, builder: (column, joinBuilders) => @@ -4739,6 +4742,11 @@ class $$AudioPlayerStateTableTableOrderingComposer column: $state.table.shuffled, builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get collections => $state.composableBuilder( + column: $state.table.collections, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); } typedef $$PlaylistTableTableInsertCompanionBuilder = PlaylistTableCompanion diff --git a/lib/models/database/tables/audio_player_state.dart b/lib/models/database/tables/audio_player_state.dart index 45f5ffd9..3e49cf6f 100644 --- a/lib/models/database/tables/audio_player_state.dart +++ b/lib/models/database/tables/audio_player_state.dart @@ -3,9 +3,9 @@ part of '../database.dart'; class AudioPlayerStateTable extends Table { IntColumn get id => integer().autoIncrement()(); BoolColumn get playing => boolean()(); - RealColumn get volume => real()(); TextColumn get loopMode => textEnum()(); BoolColumn get shuffled => boolean()(); + TextColumn get collections => text().map(const StringListConverter())(); } class PlaylistTable extends Table { diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index a071ac04..f9f70c66 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -12,7 +12,7 @@ import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/pages/album/album.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -30,10 +30,10 @@ class AlbumCard extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.read(playbackHistoryProvider.notifier); bool isPlaylistPlaying = useMemoized( @@ -59,7 +59,7 @@ class AlbumCard extends HookConsumerWidget { ), margin: const EdgeInsets.symmetric(horizontal: 10), isPlaying: isPlaylistPlaying, - isLoading: (isPlaylistPlaying && playlist.isFetching == true) || + isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || updating.value, title: album.name!, description: diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 66344792..d75df796 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -25,7 +25,7 @@ import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; @@ -47,7 +47,7 @@ class PlayerView extends HookConsumerWidget { final auth = ref.watch(authenticationProvider); final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider); final currentActiveTrack = - ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack)); + ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); final currentTrack = sourcedCurrentTrack ?? currentActiveTrack; final isLocalTrack = currentTrack is LocalTrack; final mediaQuery = MediaQuery.of(context); @@ -309,15 +309,13 @@ class PlayerView extends HookConsumerWidget { builder: (context) => Consumer( builder: (context, ref, _) { final playlist = ref.watch( - proxyPlaylistProvider, - ); - final playlistNotifier = - ref.read( - proxyPlaylistProvider - .notifier, + audioPlayerProvider, ); + final playlistNotifier = ref + .read(audioPlayerProvider + .notifier); return PlayerQueue - .fromProxyPlaylistNotifier( + .fromAudioPlayerNotifier( floating: false, playlist: playlist, notifier: playlistNotifier, @@ -328,8 +326,9 @@ class PlayerView extends HookConsumerWidget { } : null), ), - if (auth != null) const SizedBox(width: 10), - if (auth != null) + if (auth.asData?.value != null) + const SizedBox(width: 10), + if (auth.asData?.value != null) Expanded( child: OutlinedButton.icon( label: Text(context.l10n.lyrics), diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 8fd434ad..8a7b3e83 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -14,7 +14,7 @@ import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/sleep_timer_provider.dart'; class PlayerActions extends HookConsumerWidget { @@ -33,7 +33,7 @@ class PlayerActions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final isLocalTrack = playlist.activeTrack is LocalTrack; ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); @@ -129,7 +129,9 @@ class PlayerActions extends HookConsumerWidget { ? () => downloader.addToQueue(playlist.activeTrack!) : null, ), - if (playlist.activeTrack != null && !isLocalTrack && auth != null) + if (playlist.activeTrack != null && + !isLocalTrack && + auth.asData?.value != null) TrackHeartButton(track: playlist.activeTrack!), AdaptivePopSheetList( offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)), diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index a1a3ffcf..c5ef82d6 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerControls extends HookConsumerWidget { @@ -43,8 +43,8 @@ class PlayerControls extends HookConsumerWidget { SeekIntent: SeekAction(), }, []); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; @@ -132,7 +132,7 @@ class PlayerControls extends HookConsumerWidget { // than total duration. Keeping it resolved value: progress.value.toDouble(), secondaryTrackValue: bufferProgress, - onChanged: playlist.isFetching == true + onChanged: playlistNotifier.isFetching() ? null : (v) { progress.value = v; @@ -183,7 +183,7 @@ class PlayerControls extends HookConsumerWidget { : context.l10n.shuffle_playlist, icon: const Icon(SpotubeIcons.shuffle), style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null : () { if (shuffled) { @@ -198,15 +198,15 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.previous_track, icon: const Icon(SpotubeIcons.skipBack), style: buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.previous, + : audioPlayer.skipToPrevious, ), IconButton( tooltip: playing ? context.l10n.pause_playback : context.l10n.resume_playback, - icon: playlist.isFetching == true + icon: playlistNotifier.isFetching() ? SizedBox( height: 20, width: 20, @@ -219,7 +219,7 @@ class PlayerControls extends HookConsumerWidget { playing ? SpotubeIcons.pause : SpotubeIcons.play, ), style: resumePauseStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null : Actions.handler( context, @@ -230,9 +230,9 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.next_track, icon: const Icon(SpotubeIcons.skipForward), style: buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.next, + : audioPlayer.skipToNext, ), StreamBuilder( stream: audioPlayer.loopModeStream, @@ -253,7 +253,7 @@ class PlayerControls extends HookConsumerWidget { loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, - onPressed: playlist.isFetching == true + onPressed: playlistNotifier.isFetching() ? null : () async { await audioPlayer.setLoopMode(loopMode); diff --git a/lib/modules/player/player_overlay.dart b/lib/modules/player/player_overlay.dart index 084de425..c1b285ee 100644 --- a/lib/modules/player/player_overlay.dart +++ b/lib/modules/player/player_overlay.dart @@ -11,7 +11,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/modules/player/player.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerOverlay extends HookConsumerWidget { @@ -24,8 +24,8 @@ class PlayerOverlay extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); - final playlist = ref.watch(proxyPlaylistProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); final canShow = playlist.activeTrack != null; final playing = @@ -127,14 +127,14 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipBack, color: textColor, ), - onPressed: playlist.isFetching + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.previous, + : audioPlayer.skipToPrevious, ), Consumer( builder: (context, ref, _) { return IconButton( - icon: playlist.isFetching + icon: playlistNotifier.isFetching() ? const SizedBox( height: 20, width: 20, @@ -158,9 +158,9 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipForward, color: textColor, ), - onPressed: playlist.isFetching + onPressed: playlistNotifier.isFetching() ? null - : playlistNotifier.next, + : audioPlayer.skipToNext, ), ], ), diff --git a/lib/modules/player/player_queue.dart b/lib/modules/player/player_queue.dart index cf16e9a3..2431d82e 100644 --- a/lib/modules/player/player_queue.dart +++ b/lib/modules/player/player_queue.dart @@ -18,12 +18,12 @@ import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; class PlayerQueue extends HookConsumerWidget { final bool floating; - final ProxyPlaylist playlist; + final AudioPlayerState playlist; final Future Function(Track track) onJump; final Future Function(String trackId) onRemove; @@ -40,10 +40,10 @@ class PlayerQueue extends HookConsumerWidget { super.key, }); - PlayerQueue.fromProxyPlaylistNotifier({ + PlayerQueue.fromAudioPlayerNotifier({ this.floating = true, required this.playlist, - required ProxyPlaylistNotifier notifier, + required AudioPlayerNotifier notifier, super.key, }) : onJump = notifier.jumpToTrack, onRemove = notifier.removeTrack, @@ -93,11 +93,10 @@ class PlayerQueue extends HookConsumerWidget { ); useEffect(() { - if (playlist.active == null) return null; + if (playlist.activeTrack == null) return null; - if (playlist.active! < 0) return; controller.scrollToIndex( - playlist.active!, + playlist.playlist.index, preferPosition: AutoScrollPosition.middle, ); return null; diff --git a/lib/modules/player/player_track_details.dart b/lib/modules/player/player_track_details.dart index da58e3b1..d722830e 100644 --- a/lib/modules/player/player_track_details.dart +++ b/lib/modules/player/player_track_details.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; class PlayerTrackDetails extends HookConsumerWidget { @@ -21,7 +21,7 @@ class PlayerTrackDetails extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); - final playback = ref.watch(proxyPlaylistProvider); + final playback = ref.watch(audioPlayerProvider); return Row( children: [ diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index a6136e62..8592f1e3 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -15,7 +15,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/models/database/database.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -53,7 +53,8 @@ class SiblingTracksSheet extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final preferences = ref.watch(userPreferencesProvider); final isSearching = useState(false); @@ -129,13 +130,13 @@ class SiblingTracksSheet extends HookConsumerWidget { ]); final siblings = useMemoized( - () => playlist.isFetching == false + () => playlistNotifier.isFetching() ? [ (activeTrack as SourcedTrack).sourceInfo, ...activeTrack.siblings, ] : [], - [playlist.isFetching, activeTrack], + [activeTrack], ); final borderRadius = floating @@ -175,12 +176,12 @@ class SiblingTracksSheet extends HookConsumerWidget { Text(" • ${sourceInfo.artist}"), ], ), - enabled: playlist.isFetching != true, - selected: playlist.isFetching != true && + enabled: !playlistNotifier.isFetching(), + selected: !playlistNotifier.isFetching() && sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, selectedTileColor: theme.popupMenuTheme.color, onTap: () { - if (playlist.isFetching == false && + if (!playlistNotifier.isFetching() && sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { activeTrackNotifier.swapSibling(sourceInfo); Navigator.of(context).pop(); @@ -188,7 +189,7 @@ class SiblingTracksSheet extends HookConsumerWidget { }, ); }, - [playlist.isFetching, activeTrack, siblings], + [activeTrack, siblings], ); final mediaQuery = MediaQuery.of(context); diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index 7c11eca6..c4164701 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -9,7 +9,7 @@ import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -22,8 +22,8 @@ class PlaylistCard extends HookConsumerWidget { }); @override Widget build(BuildContext context, ref) { - final playlistQueue = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistQueue = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.read(playbackHistoryProvider.notifier); final playing = @@ -65,8 +65,8 @@ class PlaylistCard extends HookConsumerWidget { placeholder: ImagePlaceholder.collection, ), isPlaying: isPlaylistPlaying, - isLoading: - (isPlaylistPlaying && playlistQueue.isFetching) || updating.value, + isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || + updating.value, isOwner: playlist.owner?.id == me.asData?.value.id && me.asData?.value.id != null, onTap: () { diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index a77ab6fe..e7dbacd2 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -19,7 +19,7 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/volume_provider.dart'; @@ -33,7 +33,7 @@ class BottomPlayer extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final auth = ref.watch(authenticationProvider); - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); @@ -91,7 +91,7 @@ class BottomPlayer extends HookConsumerWidget { children: [ PlayerActions( extraActions: [ - if (auth != null) + if (auth.asData?.value != null) IconButton( tooltip: context.l10n.mini_player, icon: const Icon(SpotubeIcons.miniPlayer), diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 7d7fa8ef..713e0d26 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -135,7 +135,7 @@ class ArtistPageHeader extends HookConsumerWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (auth != null) + if (auth.asData?.value != null) Consumer( builder: (context, ref, _) { final isFollowingQuery = ref diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index c9397c7b..d52ed470 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class ArtistPageTopTracks extends HookConsumerWidget { @@ -21,8 +21,8 @@ class ArtistPageTopTracks extends HookConsumerWidget { final theme = Theme.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final topTracksQuery = ref.watch(artistTopTracksProvider(artistId)); final isPlaylistPlaying = playlist.containsTracks( diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 830e8a5d..16891bc1 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -17,7 +17,7 @@ import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/service_utils.dart'; class LocalLibraryPage extends HookConsumerWidget { @@ -32,8 +32,8 @@ class LocalLibraryPage extends HookConsumerWidget { List tracks, { LocalTrack? currentTrack, }) async { - final playlist = ref.read(proxyPlaylistProvider); - final playback = ref.read(proxyPlaylistProvider.notifier); + final playlist = ref.read(audioPlayerProvider); + final playback = ref.read(audioPlayerProvider.notifier); currentTrack ??= tracks.first; final isPlaylistPlaying = playlist.containsTracks(tracks); if (!isPlaylistPlaying) { @@ -52,7 +52,7 @@ class LocalLibraryPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final sortBy = useState(SortBy.none); - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final trackSnapshot = ref.watch(localTracksProvider); final isPlaylistPlaying = playlist.containsTracks( trackSnapshot.asData?.value.values.flattened.toList() ?? []); diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 90838300..3bdc3b52 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart'; import 'package:spotube/pages/playlist/playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class PlaylistGenerateResultPage extends HookConsumerWidget { @@ -28,7 +28,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final router = GoRouter.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final generatedPlaylist = ref.watch(generatePlaylistProvider(state)); @@ -81,9 +81,12 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ? null : () async { await playlistNotifier.load( - generatedPlaylist.asData!.value.where( - (e) => selectedTracks.value.contains(e.id!), - ), + generatedPlaylist.asData!.value + .where( + (e) => selectedTracks.value + .contains(e.id!), + ) + .toList(), autoPlay: true, ); }, diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index c484046b..18ce6e28 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -18,7 +18,7 @@ import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -30,7 +30,7 @@ class LyricsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); String albumArt = useMemoized( () => (playlist.activeTrack?.album?.images).asUrlString( index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1, @@ -62,7 +62,7 @@ class LyricsPage extends HookConsumerWidget { const Spacer(), Consumer( builder: (context, ref, child) { - final playback = ref.watch(proxyPlaylistProvider); + final playback = ref.watch(audioPlayerProvider); final lyric = ref.watch(syncedLyricsProvider(playback.activeTrack)); final providerName = lyric.asData?.value.provider; diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index f9659538..d9222059 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -15,7 +15,7 @@ import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -31,7 +31,7 @@ class MiniLyricsPage extends HookConsumerWidget { final update = useForceUpdate(); final wasMaximized = useRef(false); - final playlistQueue = ref.watch(proxyPlaylistProvider); + final playlistQueue = ref.watch(audioPlayerProvider); final areaActive = useState(false); final hoverMode = useState(true); @@ -230,14 +230,13 @@ class MiniLyricsPage extends HookConsumerWidget { builder: (context) { return Consumer(builder: (context, ref, _) { final playlist = - ref.watch(proxyPlaylistProvider); + ref.watch(audioPlayerProvider); - return PlayerQueue - .fromProxyPlaylistNotifier( + return PlayerQueue.fromAudioPlayerNotifier( floating: true, playlist: playlist, notifier: ref - .read(proxyPlaylistProvider.notifier), + .read(audioPlayerProvider.notifier), ); }); }, diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 5340e8fd..7c571d5f 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class PlainLyrics extends HookConsumerWidget { @@ -27,7 +27,7 @@ class PlainLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final lyricsQuery = ref.watch(syncedLyricsProvider(playlist.activeTrack)); final mediaQuery = MediaQuery.of(context); final textTheme = Theme.of(context).textTheme; diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 8a2dd356..3294bab5 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -12,7 +12,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/modules/lyrics/use_synced_lyrics.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -32,7 +32,7 @@ class SyncedLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final mediaQuery = MediaQuery.of(context); final controller = useAutoScrollController(); @@ -54,7 +54,7 @@ class SyncedLyrics extends HookConsumerWidget { final textTheme = Theme.of(context).textTheme; ref.listen( - proxyPlaylistProvider.select((s) => s.activeTrack), + audioPlayerProvider.select((s) => s.activeTrack), (previous, next) { controller.scrollToIndex(0); ref.read(syncedLyricsDelayProvider.notifier).state = 0; diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 93a84f0a..322a8731 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -16,7 +16,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/pages/home/home.dart'; import 'package:spotube/provider/download_manager_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_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'; @@ -201,11 +201,11 @@ class RootApp extends HookConsumerWidget { ), child: Consumer( builder: (context, ref, _) { - final playlist = ref.watch(proxyPlaylistProvider); + final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = - ref.read(proxyPlaylistProvider.notifier); + ref.read(audioPlayerProvider.notifier); - return PlayerQueue.fromProxyPlaylistNotifier( + return PlayerQueue.fromAudioPlayerNotifier( floating: true, playlist: playlist, notifier: playlistNotifier, diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index 1bde2872..6ec8f685 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -8,7 +8,7 @@ import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; class SearchTracksSection extends HookConsumerWidget { @@ -24,8 +24,8 @@ class SearchTracksSection extends HookConsumerWidget { ref.watch(searchProvider(SearchType.track).notifier); final tracks = searchTrack.asData?.value.items.cast() ?? []; - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); - final playlist = ref.watch(proxyPlaylistProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); final theme = Theme.of(context); return Column( diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index 1e9b2067..dc4defc8 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -34,8 +34,8 @@ class TrackPage extends HookConsumerWidget { final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final mediaQuery = MediaQuery.of(context); - final playlist = ref.watch(proxyPlaylistProvider); - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final isActive = playlist.activeTrack?.id == trackId; diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 747c78e6..258e15d8 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -1,13 +1,22 @@ +import 'dart:math'; + import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/discord_provider.dart'; +import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class AudioPlayerNotifier extends Notifier { + BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier); + Future _syncSavedState() async { final database = ref.read(databaseProvider); @@ -18,9 +27,9 @@ class AudioPlayerNotifier extends Notifier { await database.into(database.audioPlayerStateTable).insert( AudioPlayerStateTableCompanion.insert( playing: audioPlayer.isPlaying, - volume: audioPlayer.volume, loopMode: audioPlayer.loopMode, shuffled: audioPlayer.isShuffled, + collections: [], id: const Value(0), ), ); @@ -28,7 +37,6 @@ class AudioPlayerNotifier extends Notifier { playerState = await database.select(database.audioPlayerStateTable).getSingle(); } else { - await audioPlayer.setVolume(playerState.volume); await audioPlayer.setLoopMode(playerState.loopMode); await audioPlayer.setShuffle(playerState.shuffled); } @@ -130,15 +138,6 @@ class AudioPlayerNotifier extends Notifier { ), ); }), - audioPlayer.volumeStream.listen((volume) async { - state = state.copyWith(volume: volume); - - await _updatePlayerState( - AudioPlayerStateTableCompanion( - volume: Value(volume), - ), - ); - }), audioPlayer.loopModeStream.listen((loopMode) async { state = state.copyWith(loopMode: loopMode); @@ -177,49 +176,141 @@ class AudioPlayerNotifier extends Notifier { playing: audioPlayer.isPlaying, playlist: audioPlayer.playlist, shuffled: audioPlayer.isShuffled, - volume: audioPlayer.volume, + collections: [], ); } + // Collection related methods + Future addCollections(List collectionIds) async { + state = state.copyWith(collections: [ + ...state.collections, + ...collectionIds, + ]); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + collections: Value(state.collections), + ), + ); + } + + Future addCollection(String collectionId) async { + await addCollections([collectionId]); + } + + Future removeCollections(List collectionIds) async { + state = state.copyWith( + collections: state.collections + .where((element) => !collectionIds.contains(element)) + .toList(), + ); + + await _updatePlayerState( + AudioPlayerStateTableCompanion( + collections: Value(state.collections), + ), + ); + } + + Future removeCollection(String collectionId) async { + await removeCollections([collectionId]); + } + // Tracks related methods + Future addTracksAtFirst(Iterable tracks) async { + if (state.tracks.length == 1) { + return addTracks(tracks); + } + + tracks = _blacklist.filter(tracks).toList() as List; + + for (int i = 0; i < tracks.length; i++) { + final track = tracks.elementAt(i); + + await audioPlayer.addTrackAt( + SpotubeMedia(track), + max(state.playlist.index, 0) + i + 1, + ); + } + } + Future addTrack(Track track) async { + if (_blacklist.contains(track)) return; await audioPlayer.addTrack(SpotubeMedia(track)); } Future addTracks(Iterable tracks) async { + tracks = _blacklist.filter(tracks).toList() as List; for (final track in tracks) { - await addTrack(track); + await audioPlayer.addTrack(SpotubeMedia(track)); } } - Future removeTrack(Track track) async { - final index = state.tracks.indexWhere((element) => element == track); + Future removeTrack(String trackId) async { + final index = state.tracks.indexWhere((element) => element.id == trackId); if (index == -1) return; await audioPlayer.removeTrack(index); } - Future removeTracks(Iterable tracks) async { - for (final track in tracks) { - await removeTrack(track); + Future removeTracks(Iterable trackIds) async { + for (final trackId in trackIds) { + await removeTrack(trackId); } } Future load( - List track, { - required int initialIndex, + List tracks, { + int initialIndex = 0, bool autoPlay = false, }) async { + tracks = _blacklist.filter(tracks).toList() as List; + + // Giving the initial track a boost so MediaKit won't skip + // because of timeout + final intendedActiveTrack = tracks.elementAt(initialIndex); + if (intendedActiveTrack is! LocalTrack) { + await ref.read(sourcedTrackProvider(intendedActiveTrack).future); + } + await audioPlayer.openPlaylist( - track.map((t) => SpotubeMedia(t)).toList(), + tracks.asMediaList(), initialIndex: initialIndex, autoPlay: autoPlay, ); } + + Future jumpToTrack(Track track) async { + final index = + state.tracks.toList().indexWhere((element) => element.id == track.id); + if (index == -1) return; + await audioPlayer.jumpTo(index); + } + + Future moveTrack(int oldIndex, int newIndex) async { + if (oldIndex == newIndex || + newIndex < 0 || + oldIndex < 0 || + newIndex > state.tracks.length - 1 || + oldIndex > state.tracks.length - 1) return; + + await audioPlayer.moveTrack(oldIndex, newIndex); + } + + bool isFetching() { + if (state.activeTrack == null) return false; + return ref.read(sourcedTrackProvider(state.activeTrack!)).isLoading; + } + + Future stop() async { + await audioPlayer.stop(); + ref.read(discordProvider.notifier).clear(); + } } -final audioPlayerProvider = NotifierProvider( +final audioPlayerProvider = + NotifierProvider( () => AudioPlayerNotifier(), -); \ No newline at end of file +); diff --git a/lib/provider/proxy_playlist/player_listeners.dart b/lib/provider/audio_player/audio_player_streams.dart similarity index 51% rename from lib/provider/proxy_playlist/player_listeners.dart rename to lib/provider/audio_player/audio_player_streams.dart index 2c1423a5..d5473dd5 100644 --- a/lib/provider/proxy_playlist/player_listeners.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -1,19 +1,53 @@ -// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - import 'dart:async'; -import 'package:spotube/services/logger/logger.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/provider/discord_provider.dart'; +import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/palette_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/proxy_playlist/skip_segments.dart'; +import 'package:spotube/provider/skip_segments/skip_segments.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/server/sourced_track.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_services/audio_services.dart'; +import 'package:spotube/services/logger/logger.dart'; + +class AudioPlayerStreamListeners { + final Ref ref; + late final AudioServices notificationService; + AudioPlayerStreamListeners(this.ref) { + AudioServices.create(ref, ref.read(audioPlayerProvider.notifier)).then( + (value) => notificationService = value, + ); + + final subscriptions = [ + subscribeToPlaylist(), + subscribeToSkipSponsor(), + subscribeToScrobbleChanged(), + subscribeToPosition(), + subscribeToPlayerError(), + ]; + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + } + + ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); + UserPreferences get preferences => ref.read(userPreferencesProvider); + Discord get discord => ref.read(discordProvider); + AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider); + PlaybackHistoryNotifier get history => + ref.read(playbackHistoryProvider.notifier); -extension ProxyPlaylistListeners on ProxyPlaylistNotifier { Future updatePalette() async { final palette = ref.read(paletteProvider); if (!preferences.albumColorSync) { @@ -21,11 +55,12 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { return; } return Future.microtask(() async { - if (playlist.activeTrack == null) return; + final activeTrack = ref.read(audioPlayerProvider).activeTrack; + if (activeTrack == null) return; final palette = await PaletteGenerator.fromImageProvider( UniversalImage.imageProvider( - (playlist.activeTrack?.album?.images).asUrlString( + (activeTrack.album?.images).asUrlString( placeholder: ImagePlaceholder.albumArt, ), height: 50, @@ -38,15 +73,8 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { StreamSubscription subscribeToPlaylist() { return audioPlayer.playlistStream.listen((mpvPlaylist) { - state = playlist.copyWith( - tracks: mpvPlaylist.medias - .map((media) => SpotubeMedia.fromMedia(media).track) - .toSet(), - active: mpvPlaylist.index, - ); - - notificationService.addTrack(playlist.activeTrack!); - discord.updatePresence(playlist.activeTrack!); + notificationService.addTrack(audioPlayerState.activeTrack!); + discord.updatePresence(audioPlayerState.activeTrack!); updatePalette(); }); } @@ -72,18 +100,18 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { String? lastScrobbled; return audioPlayer.positionStream.listen((position) { try { - final uid = playlist.activeTrack is LocalTrack - ? (playlist.activeTrack as LocalTrack).path - : playlist.activeTrack?.id; + final uid = audioPlayerState.activeTrack is LocalTrack + ? (audioPlayerState.activeTrack as LocalTrack).path + : audioPlayerState.activeTrack?.id; - if (playlist.activeTrack == null || + if (audioPlayerState.activeTrack == null || lastScrobbled == uid || position.inSeconds < 30) { return; } - scrobbler.scrobble(playlist.activeTrack!); - history.addTrack(playlist.activeTrack!); + scrobbler.scrobble(audioPlayerState.activeTrack!); + history.addTrack(audioPlayerState.activeTrack!); lastScrobbled = uid; } catch (e, stack) { AppLogger.reportError(e, stack); @@ -95,9 +123,13 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { String lastTrack = ""; // used to prevent multiple calls to the same track return audioPlayer.positionStream.listen((event) async { if (event < const Duration(seconds: 3) || - playlist.active == null || - playlist.active == playlist.tracks.length - 1) return; - final nextTrack = playlist.tracks.elementAt(playlist.active! + 1); + audioPlayerState.playlist.index == -1 || + audioPlayerState.playlist.index == + audioPlayerState.tracks.length - 1) { + return; + } + final nextTrack = audioPlayerState.tracks + .elementAt(audioPlayerState.playlist.index + 1); if (lastTrack == nextTrack.id || nextTrack is LocalTrack) return; @@ -113,3 +145,6 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier { return audioPlayer.errorStream.listen((event) {}); } } + +final audioPlayerStreamListenersProvider = + Provider(AudioPlayerStreamListeners.new); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 3c874011..685ce112 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -4,39 +4,97 @@ import 'package:spotube/services/audio_player/audio_player.dart'; class AudioPlayerState { final bool playing; - final double volume; final PlaylistMode loopMode; final bool shuffled; final Playlist playlist; final List tracks; + final List collections; AudioPlayerState({ required this.playing, - required this.volume, required this.loopMode, required this.shuffled, required this.playlist, + required this.collections, List? tracks, }) : tracks = tracks ?? playlist.medias .map((media) => SpotubeMedia.fromMedia(media).track) .toList(); + factory AudioPlayerState.fromJson(Map json) { + return AudioPlayerState( + playing: json['playing'], + loopMode: PlaylistMode.values.firstWhere( + (e) => e.name == json['loopMode'], + orElse: () => audioPlayer.loopMode, + ), + shuffled: json['shuffled'], + playlist: Playlist( + json['playlist']['medias'] + .map((media) => Media( + media['uri'], + extras: media['extras'], + httpHeaders: media['httpHeaders'], + )) + .toList(), + index: json['playlist']['index'], + ), + collections: List.from(json['collections']), + ); + } + + Map toJson() { + return { + 'playing': playing, + 'loopMode': loopMode.name, + 'shuffled': shuffled, + 'playlist': { + 'medias': playlist.medias + .map((media) => { + 'uri': media.uri, + 'extras': media.extras, + 'httpHeaders': media.httpHeaders, + }) + .toList(), + 'index': playlist.index, + }, + 'collections': collections, + }; + } + AudioPlayerState copyWith({ bool? playing, - double? volume, PlaylistMode? loopMode, bool? shuffled, Playlist? playlist, + List? collections, }) { return AudioPlayerState( playing: playing ?? this.playing, - volume: volume ?? this.volume, loopMode: loopMode ?? this.loopMode, shuffled: shuffled ?? this.shuffled, playlist: playlist ?? this.playlist, + collections: collections ?? this.collections, tracks: playlist == null ? tracks : null, ); } + + Track? get activeTrack { + if (playlist.index == -1) return null; + return tracks.elementAtOrNull(playlist.index); + } + + bool containsTrack(Track track) { + return tracks.any((t) => t.id == track.id); + } + + bool containsTracks(List tracks) { + return tracks.every(containsTrack); + } + + bool containsCollection(String collectionId) { + return collections.contains(collectionId); + } } diff --git a/lib/provider/connect/connect.dart b/lib/provider/connect/connect.dart index c6014445..28eb131b 100644 --- a/lib/provider/connect/connect.dart +++ b/lib/provider/connect/connect.dart @@ -1,13 +1,14 @@ import 'dart:convert'; import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotube/provider/audio_player/state.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/connect/clients.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -31,8 +32,14 @@ final loopModeProvider = StateProvider( (ref) => PlaylistMode.none, ); -final queueProvider = StateProvider( - (ref) => ProxyPlaylist({}), +final queueProvider = StateProvider( + (ref) => AudioPlayerState( + playing: audioPlayer.isPlaying, + loopMode: audioPlayer.loopMode, + shuffled: audioPlayer.isShuffled, + playlist: audioPlayer.playlist, + collections: [], + ), ); final volumeProvider = StateProvider( diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index f90db54a..29c53762 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/extensions/artist_simple.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/platform.dart'; @@ -55,7 +55,7 @@ final discordProvider = ChangeNotifierProvider( (ref) { final isEnabled = ref.watch(userPreferencesProvider.select((s) => s.discordPresence)); - final playback = ref.read(proxyPlaylistProvider); + final playback = ref.read(audioPlayerProvider); final discord = Discord(isEnabled); if (playback.activeTrack != null) { diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart deleted file mode 100644 index 9f371b7a..00000000 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/models/local_track.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; - -class ProxyPlaylist { - final Set tracks; - final Set collections; - final int? active; - - ProxyPlaylist(this.tracks, [this.active, this.collections = const {}]); - - factory ProxyPlaylist.fromJson( - Map json, - ) { - return ProxyPlaylist( - List.castFrom>( - json['tracks'] ?? >[], - ).map((t) => _makeAppropriateTrack(t)).toSet(), - json['active'] as int?, - json['collections'] == null - ? {} - : (json['collections'] as List).toSet().cast(), - ); - } - - factory ProxyPlaylist.fromJsonRaw(Map json) => ProxyPlaylist( - json['tracks'] == null - ? {} - : (json['tracks'] as List).map((t) => Track.fromJson(t)).toSet(), - json['active'] as int?, - json['collections'] == null - ? {} - : (json['collections'] as List).toSet().cast(), - ); - - Track? get activeTrack => - active == null || active == -1 ? null : tracks.elementAtOrNull(active!); - - bool get isFetching => activeTrack == null && tracks.isNotEmpty; - - bool containsCollection(String collection) { - return collections.contains(collection); - } - - bool containsTrack(TrackSimple track) { - return tracks.firstWhereOrNull((element) { - if (element is LocalTrack && track is LocalTrack) { - return element.path == track.path; - } - - return element.id == track.id; - }) != - null; - } - - bool containsTracks(Iterable tracks) { - if (tracks.isEmpty) return false; - return tracks.every(containsTrack); - } - - static Track _makeAppropriateTrack(Map track) { - if (track.containsKey("path")) { - return LocalTrack.fromJson(track); - } else { - return Track.fromJson(track); - } - } - - /// To make sure proper instance method is used for JSON serialization - /// Otherwise default super.toJson() is used - static Map _makeAppropriateTrackJson(Track track) { - return switch (track) { - // ignore: unnecessary_cast - LocalTrack() => (track as LocalTrack).toJson(), - // ignore: unnecessary_cast - SourcedTrack() => (track as SourcedTrack).toJson(), - _ => track.toJson(), - }; - } - - Map toJson() { - return { - 'tracks': tracks.map(_makeAppropriateTrackJson).toList(), - 'active': active, - 'collections': collections.toList(), - }; - } - - ProxyPlaylist copyWith({ - Set? tracks, - int? active, - Set? collections, - }) { - return ProxyPlaylist( - tracks ?? this.tracks, - active ?? this.active, - collections ?? this.collections, - ); - } -} diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart deleted file mode 100644 index 067d8d44..00000000 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; -import 'package:spotube/models/local_track.dart'; - -import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/palette_provider.dart'; -import 'package:spotube/provider/proxy_playlist/player_listeners.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/scrobbler/scrobbler.dart'; -import 'package:spotube/provider/server/sourced_track.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; - -import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/audio_services/audio_services.dart'; -import 'package:spotube/provider/discord_provider.dart'; - -import 'package:spotube/utils/persisted_state_notifier.dart'; - -class ProxyPlaylistNotifier extends PersistedStateNotifier { - final Ref ref; - late final AudioServices notificationService; - - ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); - UserPreferences get preferences => ref.read(userPreferencesProvider); - ProxyPlaylist get playlist => state; - BlackListNotifier get blacklist => ref.read(blacklistProvider.notifier); - Discord get discord => ref.read(discordProvider); - PlaybackHistoryNotifier get history => - ref.read(playbackHistoryProvider.notifier); - - List _subscriptions = []; - - ProxyPlaylistNotifier(this.ref) : super(ProxyPlaylist({}), "playlist") { - AudioServices.create(ref, this).then( - (value) => notificationService = value, - ); - - _subscriptions = [ - // These are subscription methods from player_listeners.dart - subscribeToPlaylist(), - subscribeToSkipSponsor(), - subscribeToPosition(), - subscribeToScrobbleChanged(), - ]; - } - // Basic methods for adding or removing tracks to playlist - - Future addTrack(Track track) async { - if (blacklist.contains(track)) return; - await audioPlayer.addTrack(SpotubeMedia(track)); - } - - Future addTracks(Iterable tracks) async { - tracks = blacklist.filter(tracks).toList() as List; - for (final track in tracks) { - await audioPlayer.addTrack(SpotubeMedia(track)); - } - } - - void addCollection(String collectionId) { - state = state.copyWith(collections: { - ...state.collections, - collectionId, - }); - } - - void removeCollection(String collectionId) { - state = state.copyWith(collections: { - ...state.collections..remove(collectionId), - }); - } - - Future removeTrack(String trackId) async { - final trackIndex = - state.tracks.toList().indexWhere((element) => element.id == trackId); - if (trackIndex == -1) return; - await audioPlayer.removeTrack(trackIndex); - } - - Future removeTracks(Iterable tracksIds) async { - final tracks = state.tracks.map((t) => t.id!).toList(); - - for (final track in tracks) { - final index = tracks.indexOf(track); - if (index == -1) continue; - await audioPlayer.removeTrack(index); - } - } - - Future load( - Iterable tracks, { - int initialIndex = 0, - bool autoPlay = false, - }) async { - tracks = blacklist.filter(tracks).toList() as List; - - state = state.copyWith(collections: {}); - - // Giving the initial track a boost so MediaKit won't skip - // because of timeout - final intendedActiveTrack = tracks.elementAt(initialIndex); - if (intendedActiveTrack is! LocalTrack) { - await ref.read(sourcedTrackProvider(intendedActiveTrack).future); - } - - await audioPlayer.openPlaylist( - tracks.asMediaList(), - initialIndex: initialIndex, - autoPlay: autoPlay, - ); - } - - Future jumpTo(int index) async { - await audioPlayer.jumpTo(index); - } - - Future jumpToTrack(Track track) async { - final index = - state.tracks.toList().indexWhere((element) => element.id == track.id); - if (index == -1) return; - await jumpTo(index); - } - - Future moveTrack(int oldIndex, int newIndex) async { - if (oldIndex == newIndex || - newIndex < 0 || - oldIndex < 0 || - newIndex > state.tracks.length - 1 || - oldIndex > state.tracks.length - 1) return; - - await audioPlayer.moveTrack(oldIndex, newIndex); - } - - Future addTracksAtFirst(Iterable tracks) async { - if (state.tracks.length == 1) { - return addTracks(tracks); - } - - tracks = blacklist.filter(tracks).toList() as List; - - for (int i = 0; i < tracks.length; i++) { - final track = tracks.elementAt(i); - - await audioPlayer.addTrackAt( - SpotubeMedia(track), - (state.active ?? 0) + i + 1, - ); - } - } - - Future next() async { - await audioPlayer.skipToNext(); - } - - Future previous() async { - await audioPlayer.skipToPrevious(); - } - - Future stop() async { - state = ProxyPlaylist({}); - await audioPlayer.stop(); - discord.clear(); - } - - @override - set state(state) { - super.state = state; - if (state.tracks.isEmpty && ref.read(paletteProvider) != null) { - ref.read(paletteProvider.notifier).state = null; - } else { - updatePalette(); - } - } - - @override - onInit() async { - if (state.tracks.isEmpty) return null; - final oldCollections = state.collections; - await load( - state.tracks, - initialIndex: max(state.active ?? 0, 0), - autoPlay: false, - ); - state = state.copyWith(collections: oldCollections); - } - - @override - FutureOr fromJson(Map json) { - return ProxyPlaylist.fromJson(json); - } - - @override - Map toJson() { - final json = state.toJson(); - return json; - } - - @override - void dispose() { - for (final subscription in _subscriptions) { - subscription.cancel(); - } - super.dispose(); - } -} - -final proxyPlaylistProvider = - StateNotifierProvider( - (ref) => ProxyPlaylistNotifier(ref), -); diff --git a/lib/provider/server/active_sourced_track.dart b/lib/provider/server/active_sourced_track.dart index 410b788c..685896ec 100644 --- a/lib/provider/server/active_sourced_track.dart +++ b/lib/provider/server/active_sourced_track.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -28,7 +28,7 @@ class ActiveSourcedTrackNotifier extends Notifier { state = newTrack; await audioPlayer.pause(); - final playbackNotifier = ref.read(proxyPlaylistProvider.notifier); + final playbackNotifier = ref.read(audioPlayerProvider.notifier); final oldActiveIndex = audioPlayer.currentIndex; await playbackNotifier.addTracksAtFirst([newTrack]); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart index eee3365e..a2fa70b8 100644 --- a/lib/provider/server/routes/connect.dart +++ b/lib/provider/server/routes/connect.dart @@ -9,7 +9,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; @@ -38,8 +38,8 @@ class ServerConnectRoutes { }); } - ProxyPlaylistNotifier get playbackNotifier => - ref.read(proxyPlaylistProvider.notifier); + AudioPlayerNotifier get audioPlayerNotifier => + ref.read(audioPlayerProvider.notifier); PlaybackHistoryNotifier get historyNotifier => ref.read(playbackHistoryProvider.notifier); Stream get connectClientStream => @@ -57,7 +57,7 @@ class ServerConnectRoutes { _connectClientStreamController.add(origin); ref.listen( - proxyPlaylistProvider, + audioPlayerProvider, (previous, next) { channel.sink.addEvent(WebSocketQueueEvent(next)); }, @@ -67,10 +67,10 @@ class ServerConnectRoutes { // because audioPlayer events doesn't fireImmediately channel.sink.addEvent(WebSocketPlayingEvent(audioPlayer.isPlaying)); channel.sink.addEvent( - WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero), + WebSocketPositionEvent(audioPlayer.position), ); channel.sink.addEvent( - WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero), + WebSocketDurationEvent(audioPlayer.duration), ); channel.sink.addEvent(WebSocketShuffleEvent(audioPlayer.isShuffled)); channel.sink.addEvent(WebSocketLoopEvent(audioPlayer.loopMode)); @@ -116,14 +116,14 @@ class ServerConnectRoutes { ); event.onLoad((event) async { - await playbackNotifier.load( + await audioPlayerNotifier.load( event.data.tracks, autoPlay: true, initialIndex: event.data.initialIndex ?? 0, ); if (event.data.collectionId == null) return; - playbackNotifier.addCollection(event.data.collectionId!); + audioPlayerNotifier.addCollection(event.data.collectionId!); if (event.data.collection is AlbumSimple) { historyNotifier .addAlbums([event.data.collection as AlbumSimple]); @@ -146,15 +146,15 @@ class ServerConnectRoutes { }); event.onNext((event) async { - await playbackNotifier.next(); + await audioPlayer.skipToNext(); }); event.onPrevious((event) async { - await playbackNotifier.previous(); + await audioPlayer.skipToPrevious(); }); event.onJump((event) async { - await playbackNotifier.jumpTo(event.data); + await audioPlayer.jumpTo(event.data); }); event.onSeek((event) async { @@ -170,15 +170,15 @@ class ServerConnectRoutes { }); event.onAddTrack((event) async { - await playbackNotifier.addTrack(event.data); + await audioPlayerNotifier.addTrack(event.data); }); event.onRemoveTrack((event) async { - await playbackNotifier.removeTrack(event.data); + await audioPlayerNotifier.removeTrack(event.data); }); event.onReorder((event) async { - await playbackNotifier.moveTrack( + await audioPlayerNotifier.moveTrack( event.data.oldIndex, event.data.newIndex, ); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 679f58b1..f29aecf4 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -2,8 +2,8 @@ import 'package:dio/dio.dart' hide Response; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelf/shelf.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -12,7 +12,7 @@ import 'package:spotube/services/logger/logger.dart'; class ServerPlaybackRoutes { final Ref ref; UserPreferences get userPreferences => ref.read(userPreferencesProvider); - ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider); + AudioPlayerState get playlist => ref.read(audioPlayerProvider); final Dio dio; ServerPlaybackRoutes(this.ref) : dio = Dio(); diff --git a/lib/provider/server/sourced_track.dart b/lib/provider/server/sourced_track.dart index 82c7ddcd..37c889b0 100644 --- a/lib/provider/server/sourced_track.dart +++ b/lib/provider/server/sourced_track.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; final sourcedTrackProvider = @@ -12,10 +12,9 @@ final sourcedTrackProvider = } ref.listen( - proxyPlaylistProvider, + audioPlayerProvider.select((value) => value.tracks), (old, next) { - if (next.tracks.isEmpty || - next.tracks.none((element) => element.id == track.id)) { + if (next.isEmpty || next.none((element) => element.id == track.id)) { ref.invalidateSelf(); } }, diff --git a/lib/provider/proxy_playlist/skip_segments.dart b/lib/provider/skip_segments/skip_segments.dart similarity index 100% rename from lib/provider/proxy_playlist/skip_segments.dart rename to lib/provider/skip_segments/skip_segments.dart diff --git a/lib/provider/tray_manager/tray_menu.dart b/lib/provider/tray_manager/tray_menu.dart index 35aca4f5..42a3f948 100644 --- a/lib/provider/tray_manager/tray_menu.dart +++ b/lib/provider/tray_manager/tray_menu.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:tray_manager/tray_manager.dart'; @@ -19,9 +19,9 @@ final audioPlayerPlaying = StreamProvider((ref) { }); final trayMenuProvider = Provider((ref) { - final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final isPlaybackPlaying = - ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null)); + ref.watch(audioPlayerProvider.select((s) => s.activeTrack != null)); final isLoopOne = ref.watch(audioPlayerLoopMode).asData?.value == PlaylistMode.single; final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false; @@ -56,14 +56,14 @@ final trayMenuProvider = Provider((ref) { label: "Next", disabled: !isPlaybackPlaying, onClick: (menuItem) { - playlistNotifier.next(); + audioPlayer.skipToNext(); }, ), MenuItem( label: "Previous", disabled: !isPlaybackPlaying, onClick: (menuItem) { - playlistNotifier.previous(); + audioPlayer.skipToPrevious(); }, ), MenuItem.submenu( diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index a730c313..a421e7d0 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -6,10 +6,9 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/palette_provider.dart'; -import 'package:spotube/provider/proxy_playlist/player_listeners.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/platform.dart'; @@ -115,7 +114,7 @@ class UserPreferencesNotifier extends Notifier { if (!sync) { ref.read(paletteProvider.notifier).state = null; } else { - ref.read(proxyPlaylistProvider.notifier).updatePalette(); + ref.read(audioPlayerStreamListenersProvider).updatePalette(); } } diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index f42d6c4b..63e43c4d 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -17,7 +17,7 @@ class AudioServices { static Future create( Ref ref, - ProxyPlaylistNotifier playback, + AudioPlayerNotifier playback, ) async { final mobile = kIsMobile || kIsMacOS || kIsLinux ? await AudioService.init( diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index 3dbae18f..cdd16138 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -2,19 +2,19 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:media_kit/media_kit.dart' hide Track; class MobileAudioService extends BaseAudioHandler { AudioSession? session; - final ProxyPlaylistNotifier playlistNotifier; + final AudioPlayerNotifier audioPlayerNotifier; // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - ProxyPlaylist get playlist => playlistNotifier.state; + AudioPlayerState get playlist => audioPlayerNotifier.state; - MobileAudioService(this.playlistNotifier) { + MobileAudioService(this.audioPlayerNotifier) { AudioSession.instance.then((s) { session = s; session?.configure(const AudioSessionConfiguration.music()); @@ -102,24 +102,24 @@ class MobileAudioService extends BaseAudioHandler { @override Future stop() async { - await playlistNotifier.stop(); + await audioPlayerNotifier.stop(); } @override Future skipToNext() async { - await playlistNotifier.next(); + await audioPlayer.skipToNext(); await super.skipToNext(); } @override Future skipToPrevious() async { - await playlistNotifier.previous(); + await audioPlayer.skipToPrevious(); await super.skipToPrevious(); } @override Future onTaskRemoved() async { - await playlistNotifier.stop(); + await audioPlayerNotifier.stop(); return super.onTaskRemoved(); } @@ -146,7 +146,7 @@ class MobileAudioService extends BaseAudioHandler { PlaylistMode.single => AudioServiceRepeatMode.one, _ => AudioServiceRepeatMode.none, }, - processingState: playlist.isFetching == true + processingState: audioPlayer.isBuffering ? AudioProcessingState.loading : AudioProcessingState.ready, ); diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index a3ee31e1..0b3113fc 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -5,18 +5,18 @@ import 'package:smtc_windows/smtc_windows.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; class WindowsAudioService { final SMTCWindows smtc; final Ref ref; - final ProxyPlaylistNotifier playlistNotifier; + final AudioPlayerNotifier audioPlayerNotifier; final subscriptions = []; - WindowsAudioService(this.ref, this.playlistNotifier) + WindowsAudioService(this.ref, this.audioPlayerNotifier) : smtc = SMTCWindows(enabled: false) { smtc.setPlaybackStatus(PlaybackStatus.Stopped); final buttonStream = smtc.buttonPressStream.listen((event) { @@ -28,13 +28,13 @@ class WindowsAudioService { audioPlayer.pause(); break; case PressedButton.next: - playlistNotifier.next(); + audioPlayer.skipToNext(); break; case PressedButton.previous: - playlistNotifier.previous(); + audioPlayer.skipToPrevious(); break; case PressedButton.stop: - playlistNotifier.stop(); + audioPlayerNotifier.stop(); break; default: break; From 75173e5096209104763059f812961982bd3755e6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 21:01:09 +0600 Subject: [PATCH 11/24] refactor: use provider based is track loading implementation --- lib/collections/intents.dart | 6 +- lib/components/track_tile/track_tile.dart | 347 +++++++++--------- lib/modules/album/album_card.dart | 6 +- lib/modules/player/player_controls.dart | 22 +- lib/modules/player/player_overlay.dart | 9 +- lib/modules/player/sibling_tracks_sheet.dart | 11 +- lib/modules/playlist/playlist_card.dart | 5 +- lib/provider/audio_player/audio_player.dart | 5 - .../audio_player/querying_track_info.dart | 12 + 9 files changed, 216 insertions(+), 207 deletions(-) create mode 100644 lib/provider/audio_player/querying_track_info.dart diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 1a44a846..ac0451ac 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -11,7 +11,7 @@ import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/library/library.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/search/search.dart'; -import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; @@ -96,8 +96,8 @@ class SeekIntent extends Intent { class SeekAction extends Action { @override invoke(intent) async { - final playlist = intent.ref.read(audioPlayerProvider.notifier); - if (playlist.isFetching()) { + final isFetchingActiveTrack = intent.ref.read(queryingTrackInfoProvider); + if (isFetchingActiveTrack) { DirectionalFocusAction().invoke( DirectionalFocusIntent( intent.forward ? TraversalDirection.right : TraversalDirection.left, diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index cdc18d9b..0e8d2cd0 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -17,7 +17,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; @@ -84,191 +84,190 @@ class TrackTile extends HookConsumerWidget { }, child: HoverBuilder( permanentState: isSelected || constrains.smAndDown ? true : null, - builder: (context, isHovering) { - return ListTile( - selected: isSelected, - onTap: () async { - try { - isLoading.value = true; - await onTap?.call(); - } finally { - if (context.mounted) { - isLoading.value = false; - } + builder: (context, isHovering) => ListTile( + selected: isSelected, + onTap: () async { + try { + isLoading.value = true; + await onTap?.call(); + } finally { + if (context.mounted) { + isLoading.value = false; } - }, - onLongPress: onLongPress, - enabled: !isBlackListed, - contentPadding: EdgeInsets.zero, - tileColor: - isBlackListed ? theme.colorScheme.errorContainer : null, - horizontalTitleGap: 12, - leadingAndTrailingTextStyle: theme.textTheme.bodyMedium, - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ...?leadingActions, - if (index != null && onChanged == null && constrains.mdAndUp) - SizedBox( - width: 50, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - '${(index ?? 0) + 1}', - maxLines: 1, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), + } + }, + onLongPress: onLongPress, + enabled: !isBlackListed, + contentPadding: EdgeInsets.zero, + tileColor: isBlackListed ? theme.colorScheme.errorContainer : null, + horizontalTitleGap: 12, + leadingAndTrailingTextStyle: theme.textTheme.bodyMedium, + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...?leadingActions, + if (index != null && onChanged == null && constrains.mdAndUp) + SizedBox( + width: 50, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + '${(index ?? 0) + 1}', + maxLines: 1, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, ), - ) - else if (constrains.smAndDown) - const SizedBox(width: 16), - if (onChanged != null) - Checkbox( - value: selected, - onChanged: onChanged, ), - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: AspectRatio( - aspectRatio: 1, - child: UniversalImage( - path: (track.album?.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - ), - fit: BoxFit.cover, - ), - ), - ), - Positioned.fill( - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: isHovering - ? Colors.black.withOpacity(0.4) - : Colors.transparent, - ), - ), - ), - Positioned.fill( - child: Center( - child: IconTheme( - data: theme.iconTheme - .copyWith(size: 26, color: Colors.white), - child: Skeleton.ignore( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: (isPlaying && - ref - .watch(audioPlayerProvider - .notifier) - .isFetching()) || - isLoading.value - ? const SizedBox( - width: 26, - height: 26, - child: CircularProgressIndicator( - strokeWidth: 1.5, - color: Colors.white, - ), - ) - : isPlaying - ? Icon( - SpotubeIcons.pause, - color: theme.colorScheme.primary, - ) - : !isHovering - ? const SizedBox.shrink() - : const Icon(SpotubeIcons.play), - ), - ), - ), - ), - ), - ], + ) + else if (constrains.smAndDown) + const SizedBox(width: 16), + if (onChanged != null) + Checkbox( + value: selected, + onChanged: onChanged, ), - ], - ), - title: Row( - children: [ - Expanded( - flex: 6, - child: switch (track) { - LocalTrack() => Text( - track.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - _ => LinkText( - track.name!, - "/track/${track.id}", - push: true, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - }, - ), - if (constrains.mdAndUp) ...[ - const SizedBox(width: 8), - Expanded( - flex: 4, - child: switch (track) { - LocalTrack() => Text( - track.album!.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: AspectRatio( + aspectRatio: 1, + child: UniversalImage( + path: (track.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, ), - _ => Align( - alignment: Alignment.centerLeft, - child: LinkText( - track.album!.name!, - "/album/${track.album?.id}", - extra: track.album, - push: true, - overflow: TextOverflow.ellipsis, + fit: BoxFit.cover, + ), + ), + ), + Positioned.fill( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: isHovering + ? Colors.black.withOpacity(0.4) + : Colors.transparent, + ), + ), + ), + Positioned.fill( + child: Center( + child: IconTheme( + data: theme.iconTheme + .copyWith(size: 26, color: Colors.white), + child: Skeleton.ignore( + child: Consumer( + builder: (context, ref, _) { + final isFetchingActiveTrack = + ref.watch(queryingTrackInfoProvider); + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: (isPlaying && isFetchingActiveTrack) || + isLoading.value + ? const SizedBox( + width: 26, + height: 26, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: Colors.white, + ), + ) + : isPlaying + ? Icon( + SpotubeIcons.pause, + color: theme.colorScheme.primary, + ) + : !isHovering + ? const SizedBox.shrink() + : const Icon(SpotubeIcons.play), + ); + }, ), - ) - }, + ), + ), + ), ), ], - ], - ), - subtitle: Align( - alignment: Alignment.centerLeft, - child: track is LocalTrack - ? Text( - track.artists?.asString() ?? '', - ) - : ClipRect( - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 40), - child: ArtistLink(artists: track.artists ?? []), - ), + ), + ], + ), + title: Row( + children: [ + Expanded( + flex: 6, + child: switch (track) { + LocalTrack() => Text( + track.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ + _ => LinkText( + track.name!, + "/track/${track.id}", + push: true, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + }, + ), + if (constrains.mdAndUp) ...[ const SizedBox(width: 8), - Text( - Duration(milliseconds: track.durationMs ?? 0) - .toHumanReadableString(padZero: false), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - TrackOptions( - track: track, - playlistId: playlistId, - userPlaylist: userPlaylist, - showMenuCbRef: showOptionCbRef, + Expanded( + flex: 4, + child: switch (track) { + LocalTrack() => Text( + track.album!.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + _ => Align( + alignment: Alignment.centerLeft, + child: LinkText( + track.album!.name!, + "/album/${track.album?.id}", + extra: track.album, + push: true, + overflow: TextOverflow.ellipsis, + ), + ) + }, ), ], - ), - ); - }, + ], + ), + subtitle: Align( + alignment: Alignment.centerLeft, + child: track is LocalTrack + ? Text( + track.artists?.asString() ?? '', + ) + : ClipRect( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 40), + child: ArtistLink(artists: track.artists ?? []), + ), + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 8), + Text( + Duration(milliseconds: track.durationMs ?? 0) + .toHumanReadableString(padZero: false), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + TrackOptions( + track: track, + playlistId: playlistId, + userPlaylist: userPlaylist, + showMenuCbRef: showOptionCbRef, + ), + ], + ), + ), ), ); }); diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index f9f70c66..de7aa5f8 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -10,6 +10,7 @@ import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/pages/album/album.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -35,6 +36,7 @@ class AlbumCard extends HookConsumerWidget { useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final historyNotifier = ref.read(playbackHistoryProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); bool isPlaylistPlaying = useMemoized( () => playlist.containsCollection(album.id!), @@ -59,8 +61,8 @@ class AlbumCard extends HookConsumerWidget { ), margin: const EdgeInsets.symmetric(horizontal: 10), isPlaying: isPlaylistPlaying, - isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || - updating.value, + isLoading: + (isPlaylistPlaying && isFetchingActiveTrack) || updating.value, title: album.name!, description: "${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}", diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index c5ef82d6..25080b66 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerControls extends HookConsumerWidget { @@ -43,8 +43,7 @@ class PlayerControls extends HookConsumerWidget { SeekIntent: SeekAction(), }, []); - ref.watch(audioPlayerProvider); - final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; @@ -132,7 +131,7 @@ class PlayerControls extends HookConsumerWidget { // than total duration. Keeping it resolved value: progress.value.toDouble(), secondaryTrackValue: bufferProgress, - onChanged: playlistNotifier.isFetching() + onChanged: isFetchingActiveTrack ? null : (v) { progress.value = v; @@ -183,7 +182,7 @@ class PlayerControls extends HookConsumerWidget { : context.l10n.shuffle_playlist, icon: const Icon(SpotubeIcons.shuffle), style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : () { if (shuffled) { @@ -198,7 +197,7 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.previous_track, icon: const Icon(SpotubeIcons.skipBack), style: buttonStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToPrevious, ), @@ -206,7 +205,7 @@ class PlayerControls extends HookConsumerWidget { tooltip: playing ? context.l10n.pause_playback : context.l10n.resume_playback, - icon: playlistNotifier.isFetching() + icon: isFetchingActiveTrack ? SizedBox( height: 20, width: 20, @@ -219,7 +218,7 @@ class PlayerControls extends HookConsumerWidget { playing ? SpotubeIcons.pause : SpotubeIcons.play, ), style: resumePauseStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : Actions.handler( context, @@ -230,9 +229,8 @@ class PlayerControls extends HookConsumerWidget { tooltip: context.l10n.next_track, icon: const Icon(SpotubeIcons.skipForward), style: buttonStyle, - onPressed: playlistNotifier.isFetching() - ? null - : audioPlayer.skipToNext, + onPressed: + isFetchingActiveTrack ? null : audioPlayer.skipToNext, ), StreamBuilder( stream: audioPlayer.loopModeStream, @@ -253,7 +251,7 @@ class PlayerControls extends HookConsumerWidget { loopMode == PlaylistMode.loop ? activeButtonStyle : buttonStyle, - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : () async { await audioPlayer.setLoopMode(loopMode); diff --git a/lib/modules/player/player_overlay.dart b/lib/modules/player/player_overlay.dart index c1b285ee..2322bcba 100644 --- a/lib/modules/player/player_overlay.dart +++ b/lib/modules/player/player_overlay.dart @@ -12,6 +12,7 @@ import 'package:spotube/collections/intents.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/modules/player/player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerOverlay extends HookConsumerWidget { @@ -24,7 +25,7 @@ class PlayerOverlay extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final playlist = ref.watch(audioPlayerProvider); final canShow = playlist.activeTrack != null; @@ -127,14 +128,14 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipBack, color: textColor, ), - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToPrevious, ), Consumer( builder: (context, ref, _) { return IconButton( - icon: playlistNotifier.isFetching() + icon: isFetchingActiveTrack ? const SizedBox( height: 20, width: 20, @@ -158,7 +159,7 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipForward, color: textColor, ), - onPressed: playlistNotifier.isFetching() + onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToNext, ), diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 8592f1e3..ddc77b15 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -16,6 +16,7 @@ import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -54,7 +55,7 @@ class SiblingTracksSheet extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final playlist = ref.watch(audioPlayerProvider); - final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final preferences = ref.watch(userPreferencesProvider); final isSearching = useState(false); @@ -130,7 +131,7 @@ class SiblingTracksSheet extends HookConsumerWidget { ]); final siblings = useMemoized( - () => playlistNotifier.isFetching() + () => isFetchingActiveTrack ? [ (activeTrack as SourcedTrack).sourceInfo, ...activeTrack.siblings, @@ -176,12 +177,12 @@ class SiblingTracksSheet extends HookConsumerWidget { Text(" • ${sourceInfo.artist}"), ], ), - enabled: !playlistNotifier.isFetching(), - selected: !playlistNotifier.isFetching() && + enabled: !isFetchingActiveTrack, + selected: !isFetchingActiveTrack && sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, selectedTileColor: theme.popupMenuTheme.color, onTap: () { - if (!playlistNotifier.isFetching() && + if (!isFetchingActiveTrack && sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { activeTrackNotifier.swapSibling(sourceInfo); Navigator.of(context).pop(); diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index c4164701..4e81a254 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -7,6 +7,7 @@ import 'package:spotube/components/playbutton_card.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/pages/playlist/playlist.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; @@ -24,6 +25,7 @@ class PlaylistCard extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlistQueue = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final historyNotifier = ref.read(playbackHistoryProvider.notifier); final playing = @@ -65,8 +67,7 @@ class PlaylistCard extends HookConsumerWidget { placeholder: ImagePlaceholder.collection, ), isPlaying: isPlaylistPlaying, - isLoading: (isPlaylistPlaying && playlistNotifier.isFetching()) || - updating.value, + isLoading: (isPlaylistPlaying && isFetchingActiveTrack) || updating.value, isOwner: playlist.owner?.id == me.asData?.value.id && me.asData?.value.id != null, onTap: () { diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 258e15d8..06081b19 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -299,11 +299,6 @@ class AudioPlayerNotifier extends Notifier { await audioPlayer.moveTrack(oldIndex, newIndex); } - bool isFetching() { - if (state.activeTrack == null) return false; - return ref.read(sourcedTrackProvider(state.activeTrack!)).isLoading; - } - Future stop() async { await audioPlayer.stop(); ref.read(discordProvider.notifier).clear(); diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart new file mode 100644 index 00000000..4069523b --- /dev/null +++ b/lib/provider/audio_player/querying_track_info.dart @@ -0,0 +1,12 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/server/sourced_track.dart'; + +final queryingTrackInfoProvider = Provider((ref) { + final activeTrack = + ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); + + if (activeTrack == null) return false; + + return ref.read(sourcedTrackProvider(activeTrack)).isLoading; +}); From a621a45f0bbbb4dfc4b342388996827fb64dfad8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 21:43:09 +0600 Subject: [PATCH 12/24] chore: fix alternative track sources not showing up --- lib/modules/player/player_controls.dart | 57 +++++++++++-------- lib/modules/player/sibling_tracks_sheet.dart | 4 +- .../audio_player/querying_track_info.dart | 12 +++- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index 25080b66..1b9d9f86 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -11,6 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -232,32 +233,38 @@ class PlayerControls extends HookConsumerWidget { onPressed: isFetchingActiveTrack ? null : audioPlayer.skipToNext, ), - StreamBuilder( - stream: audioPlayer.loopModeStream, - builder: (context, snapshot) { - final loopMode = snapshot.data ?? PlaylistMode.none; - return IconButton( - tooltip: loopMode == PlaylistMode.single - ? context.l10n.loop_track - : loopMode == PlaylistMode.loop - ? context.l10n.repeat_playlist - : null, - icon: Icon( - loopMode == PlaylistMode.single - ? SpotubeIcons.repeatOne - : SpotubeIcons.repeat, - ), - style: loopMode == PlaylistMode.single || - loopMode == PlaylistMode.loop - ? activeButtonStyle - : buttonStyle, - onPressed: isFetchingActiveTrack - ? null - : () async { - await audioPlayer.setLoopMode(loopMode); + Consumer(builder: (context, ref, _) { + final loopMode = ref + .watch(audioPlayerProvider.select((s) => s.loopMode)); + + return IconButton( + tooltip: loopMode == PlaylistMode.single + ? context.l10n.loop_track + : loopMode == PlaylistMode.loop + ? context.l10n.repeat_playlist + : null, + icon: Icon( + loopMode == PlaylistMode.single + ? SpotubeIcons.repeatOne + : SpotubeIcons.repeat, + ), + style: loopMode == PlaylistMode.single || + loopMode == PlaylistMode.loop + ? activeButtonStyle + : buttonStyle, + onPressed: isFetchingActiveTrack + ? null + : () async { + await audioPlayer.setLoopMode( + switch (loopMode) { + PlaylistMode.loop => PlaylistMode.single, + PlaylistMode.single => PlaylistMode.none, + PlaylistMode.none => PlaylistMode.loop, }, - ); - }), + ); + }, + ); + }), ], ), const SizedBox(height: 5) diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index ddc77b15..092d631f 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -131,13 +131,13 @@ class SiblingTracksSheet extends HookConsumerWidget { ]); final siblings = useMemoized( - () => isFetchingActiveTrack + () => !isFetchingActiveTrack ? [ (activeTrack as SourcedTrack).sourceInfo, ...activeTrack.siblings, ] : [], - [activeTrack], + [activeTrack, isFetchingActiveTrack], ); final borderRadius = floating diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart index 4069523b..f03efd9e 100644 --- a/lib/provider/audio_player/querying_track_info.dart +++ b/lib/provider/audio_player/querying_track_info.dart @@ -1,12 +1,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/server/sourced_track.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; final queryingTrackInfoProvider = Provider((ref) { + final media = audioPlayer.playlist.index == -1 + ? null + : audioPlayer.playlist.medias.elementAtOrNull(audioPlayer.playlist.index); + final audioPlayerActiveTrack = + media == null ? null : SpotubeMedia.fromMedia(media).track; + final activeTrack = - ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); + ref.watch(audioPlayerProvider.select((s) => s.activeTrack)) ?? + audioPlayerActiveTrack; if (activeTrack == null) return false; - return ref.read(sourcedTrackProvider(activeTrack)).isLoading; + return ref.watch(sourcedTrackProvider(activeTrack)).isLoading; }); From 1b420e661beae8af86b4f639ce6c3569ffbc5f48 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Jun 2024 22:26:44 +0600 Subject: [PATCH 13/24] chore: player skipping all tracks from cache --- lib/provider/audio_player/audio_player.dart | 10 +++++++--- lib/provider/audio_player/state.dart | 12 +++++++----- lib/provider/server/server.dart | 7 +++---- lib/services/audio_player/audio_player.dart | 16 +++++++++++++--- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 06081b19..e5db78c0 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -72,14 +72,18 @@ class AudioPlayerNotifier extends Notifier { ], ); }); - } else { + } else if (medias.isNotEmpty) { await audioPlayer.openPlaylist( medias - .map((media) => Media( + .map( + (media) => SpotubeMedia.fromMedia( + Media( media.uri, extras: media.extras, httpHeaders: media.httpHeaders, - )) + ), + ), + ) .toList(), initialIndex: playlist.index, ); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 685ce112..3572e289 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -33,11 +33,13 @@ class AudioPlayerState { shuffled: json['shuffled'], playlist: Playlist( json['playlist']['medias'] - .map((media) => Media( - media['uri'], - extras: media['extras'], - httpHeaders: media['httpHeaders'], - )) + .map( + (media) => SpotubeMedia.fromMedia(Media( + media['uri'], + extras: media['extras'], + httpHeaders: media['httpHeaders'], + )), + ) .toList(), index: json['playlist']['index'], ), diff --git a/lib/provider/server/server.dart b/lib/provider/server/server.dart index 5232bb17..131f1ea4 100644 --- a/lib/provider/server/server.dart +++ b/lib/provider/server/server.dart @@ -5,15 +5,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelf/shelf_io.dart'; import 'package:spotube/provider/server/pipeline.dart'; import 'package:spotube/provider/server/router.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; -int serverPort = 0; final serverProvider = FutureProvider( (ref) async { final pipeline = ref.watch(pipelineProvider); final router = ref.watch(serverRouterProvider); + final port = Random().nextInt(17500) + 5000; - final port = Random().nextInt(17000) + 1500; + SpotubeMedia.serverPort = port; final server = await serve( pipeline.addHandler(router.call), @@ -28,8 +29,6 @@ final serverProvider = FutureProvider( server.close(); }); - serverPort = port; - return (server: server, port: port); }, ); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 713d518b..0b62c068 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:media_kit/media_kit.dart' hide Track; -import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:spotify/spotify.dart' hide Playlist; @@ -20,9 +19,11 @@ part 'audio_player_impl.dart'; class SpotubeMedia extends mk.Media { final Track track; + static int serverPort = 0; + SpotubeMedia( this.track, { - Map? extras, + Map? extras, super.httpHeaders, }) : super( track is LocalTrack @@ -38,11 +39,20 @@ class SpotubeMedia extends mk.Media { }, ); + @override + String get uri => track is LocalTrack + ? (track as LocalTrack).path + : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; + factory SpotubeMedia.fromMedia(mk.Media media) { final track = media.uri.startsWith("http") ? Track.fromJson(media.extras?["track"]) : LocalTrack.fromJson(media.extras?["track"]); - return SpotubeMedia(track); + return SpotubeMedia( + track, + extras: media.extras, + httpHeaders: media.httpHeaders, + ); } } From 6c5cab9899080450bbd077c206bb8dd41c8e9b5d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 25 Jun 2024 20:36:23 +0600 Subject: [PATCH 14/24] chore: fix use SpotubeMedia to avoid duplicate sourceTrackProvider instances --- lib/provider/audio_player/audio_player.dart | 8 +++++--- .../audio_player/audio_player_streams.dart | 10 ++++++---- .../audio_player/querying_track_info.dart | 18 +++++++++++------- lib/provider/audio_player/state.dart | 5 +++++ lib/provider/server/routes/playback.dart | 3 ++- lib/provider/server/sourced_track.dart | 5 +++-- lib/services/audio_player/audio_player.dart | 15 +++++++++++++++ 7 files changed, 47 insertions(+), 17 deletions(-) diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index e5db78c0..9dfc2c0a 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -86,6 +86,7 @@ class AudioPlayerNotifier extends Notifier { ) .toList(), initialIndex: playlist.index, + autoPlay: false, ); } } @@ -270,17 +271,18 @@ class AudioPlayerNotifier extends Notifier { int initialIndex = 0, bool autoPlay = false, }) async { - tracks = _blacklist.filter(tracks).toList() as List; + final medias = + (_blacklist.filter(tracks).toList() as List).asMediaList(); // Giving the initial track a boost so MediaKit won't skip // because of timeout - final intendedActiveTrack = tracks.elementAt(initialIndex); + final intendedActiveTrack = medias.elementAt(initialIndex); if (intendedActiveTrack is! LocalTrack) { await ref.read(sourcedTrackProvider(intendedActiveTrack).future); } await audioPlayer.openPlaylist( - tracks.asMediaList(), + medias, initialIndex: initialIndex, autoPlay: autoPlay, ); diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index d5473dd5..42944075 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -128,15 +128,17 @@ class AudioPlayerStreamListeners { audioPlayerState.tracks.length - 1) { return; } - final nextTrack = audioPlayerState.tracks - .elementAt(audioPlayerState.playlist.index + 1); + final nextTrack = SpotubeMedia.fromMedia(audioPlayerState.playlist.medias + .elementAt(audioPlayerState.playlist.index + 1)); - if (lastTrack == nextTrack.id || nextTrack is LocalTrack) return; + if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { + return; + } try { await ref.read(sourcedTrackProvider(nextTrack).future); } finally { - lastTrack = nextTrack.id!; + lastTrack = nextTrack.track.id!; } }); } diff --git a/lib/provider/audio_player/querying_track_info.dart b/lib/provider/audio_player/querying_track_info.dart index f03efd9e..55590d48 100644 --- a/lib/provider/audio_player/querying_track_info.dart +++ b/lib/provider/audio_player/querying_track_info.dart @@ -4,17 +4,21 @@ import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; final queryingTrackInfoProvider = Provider((ref) { - final media = audioPlayer.playlist.index == -1 + final media = audioPlayer.playlist.index == -1 || + audioPlayer.playlist.medias.isEmpty ? null : audioPlayer.playlist.medias.elementAtOrNull(audioPlayer.playlist.index); final audioPlayerActiveTrack = - media == null ? null : SpotubeMedia.fromMedia(media).track; + media == null ? null : SpotubeMedia.fromMedia(media); - final activeTrack = - ref.watch(audioPlayerProvider.select((s) => s.activeTrack)) ?? - audioPlayerActiveTrack; + final activeMedia = ref.watch(audioPlayerProvider.select( + (s) => s.activeMedia == null + ? null + : SpotubeMedia.fromMedia(s.activeMedia!), + )) ?? + audioPlayerActiveTrack; - if (activeTrack == null) return false; + if (activeMedia == null) return false; - return ref.watch(sourcedTrackProvider(activeTrack)).isLoading; + return ref.watch(sourcedTrackProvider(activeMedia)).isLoading; }); diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 3572e289..387c2e30 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -88,6 +88,11 @@ class AudioPlayerState { return tracks.elementAtOrNull(playlist.index); } + Media? get activeMedia { + if (playlist.index == -1 || playlist.medias.isEmpty) return null; + return playlist.medias.elementAt(playlist.index); + } + bool containsTrack(Track track) { return tracks.any((t) => t.id == track.id); } diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index f29aecf4..aa380d01 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -7,6 +7,7 @@ import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; class ServerPlaybackRoutes { @@ -25,7 +26,7 @@ class ServerPlaybackRoutes { final activeSourcedTrack = ref.read(activeSourcedTrackProvider); final sourcedTrack = activeSourcedTrack?.id == track.id ? activeSourcedTrack - : await ref.read(sourcedTrackProvider(track).future); + : await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future); ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); diff --git a/lib/provider/server/sourced_track.dart b/lib/provider/server/sourced_track.dart index 37c889b0..53a04023 100644 --- a/lib/provider/server/sourced_track.dart +++ b/lib/provider/server/sourced_track.dart @@ -1,12 +1,13 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; final sourcedTrackProvider = - FutureProvider.family((ref, track) async { + FutureProvider.family((ref, media) async { + final track = media?.track; if (track == null || track is LocalTrack) { return null; } diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 0b62c068..bb1a6203 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -54,6 +54,21 @@ class SpotubeMedia extends mk.Media { httpHeaders: media.httpHeaders, ); } + + @override + operator ==(Object other) { + if (other is! SpotubeMedia) return false; + + final isLocal = track is LocalTrack && other.track is LocalTrack; + return isLocal + ? (other.track as LocalTrack).path == (track as LocalTrack).path + : other.track.id == track.id; + } + + @override + int get hashCode => track is LocalTrack + ? (track as LocalTrack).path.hashCode + : track.id.hashCode; } abstract class AudioPlayerInterface { From 44418868ad10e4f1043777973f70e1d2745c7fcc Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 25 Jun 2024 20:38:40 +0600 Subject: [PATCH 15/24] chore: fix volume not being set after launch --- lib/provider/volume_provider.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/provider/volume_provider.dart b/lib/provider/volume_provider.dart index ddd38fd9..64bcfe1a 100644 --- a/lib/provider/volume_provider.dart +++ b/lib/provider/volume_provider.dart @@ -9,6 +9,7 @@ class VolumeProvider extends Notifier { @override build() { + audioPlayer.setVolume(KVStoreService.volume); return KVStoreService.volume; } From 08ac29c97936e4015ef23ee4b7890ab0cfe31766 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 29 Jun 2024 17:05:06 +0600 Subject: [PATCH 16/24] refactor(stats): migrate stats to use drift db --- build.yaml | 7 + lib/collections/fake.dart | 34 ++ .../sections/body/track_view_body.dart | 2 +- .../sections/body/track_view_options.dart | 2 +- .../sections/header/header_actions.dart | 2 +- .../sections/header/header_buttons.dart | 2 +- lib/models/database/database.dart | 6 +- lib/models/database/database.g.dart | 441 ++++++++++++++++++ lib/models/database/tables/history.dart | 25 + lib/modules/album/album_card.dart | 2 +- lib/modules/home/sections/recent.dart | 35 +- lib/modules/playlist/playlist_card.dart | 2 +- lib/modules/stats/summary/summary.dart | 156 ++++--- lib/modules/stats/top/albums.dart | 30 +- lib/modules/stats/top/artists.dart | 8 +- lib/modules/stats/top/tracks.dart | 8 +- lib/pages/lyrics/synced_lyrics.dart | 6 +- lib/pages/stats/albums/albums.dart | 12 +- lib/pages/stats/artists/artists.dart | 8 +- lib/pages/stats/fees/fees.dart | 8 +- lib/pages/stats/minutes/minutes.dart | 8 +- lib/pages/stats/playlists/playlists.dart | 10 +- lib/pages/stats/streams/streams.dart | 8 +- .../audio_player/audio_player_streams.dart | 4 +- lib/provider/history/history.dart | 163 ++----- lib/provider/history/recent.dart | 89 ++-- lib/provider/history/summary.dart | 237 ++++++++-- lib/provider/history/top.dart | 273 +++++++---- lib/provider/server/routes/connect.dart | 4 +- 29 files changed, 1169 insertions(+), 423 deletions(-) create mode 100644 lib/models/database/tables/history.dart diff --git a/build.yaml b/build.yaml index d83d6a20..8dbfe45d 100644 --- a/build.yaml +++ b/build.yaml @@ -8,3 +8,10 @@ targets: options: any_map: true explicit_to_json: true + drift_dev: + options: + sql: + dialect: sqlite + options: + modules: + - json1 diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart index 7391d3a0..31f97e0c 100644 --- a/lib/collections/fake.dart +++ b/lib/collections/fake.dart @@ -1,6 +1,8 @@ import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/spotify/home_feed.dart'; import 'package:spotube/models/spotify_friends.dart'; +import 'package:spotube/provider/history/summary.dart'; abstract class FakeData { static final Image image = Image() @@ -222,4 +224,36 @@ abstract class FakeData { ) ], ); + + static const historySummary = PlaybackHistorySummary( + albums: 1, + artists: 1, + duration: Duration(seconds: 1), + playlists: 1, + tracks: 1, + fees: 1, + ); + + static final historyRecentlyPlayedPlaylist = HistoryTableData( + id: 0, + type: HistoryEntryType.track, + createdAt: DateTime.now(), + itemId: "1", + data: playlist.toJson(), + ); + + static final historyRecentlyPlayedAlbum = HistoryTableData( + id: 0, + type: HistoryEntryType.track, + createdAt: DateTime.now(), + itemId: "1", + data: album.toJson(), + ); + + static final historyRecentlyPlayedItems = List.generate( + 10, + (index) => index % 2 == 0 + ? historyRecentlyPlayedPlaylist + : historyRecentlyPlayedAlbum, + ); } diff --git a/lib/components/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart index a6089cc3..df841b8d 100644 --- a/lib/components/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/tracks_view/sections/body/track_view_body.dart @@ -29,7 +29,7 @@ class TrackViewBodySection extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final props = InheritedTrackView.of(context); final trackViewState = ref.watch(trackViewProvider(props.tracks)); diff --git a/lib/components/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart index 98ddca25..23198aec 100644 --- a/lib/components/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/tracks_view/sections/body/track_view_options.dart @@ -25,7 +25,7 @@ class TrackViewBodyOptions extends HookConsumerWidget { ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final audioSource = ref.watch(userPreferencesProvider.select((s) => s.audioSource)); diff --git a/lib/components/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart index 6769ed52..94f0baa2 100644 --- a/lib/components/tracks_view/sections/header/header_actions.dart +++ b/lib/components/tracks_view/sections/header/header_actions.dart @@ -22,7 +22,7 @@ class TrackViewHeaderActions extends HookConsumerWidget { final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final isActive = playlist.collections.contains(props.collectionId); diff --git a/lib/components/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart index aabca20f..e9fbf6bb 100644 --- a/lib/components/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/tracks_view/sections/header/header_buttons.dart @@ -30,7 +30,7 @@ class TrackViewHeaderButtons extends HookConsumerWidget { final props = InheritedTrackView.of(context); final playlist = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.watch(playbackHistoryProvider.notifier); + final historyNotifier = ref.watch(playbackHistoryActionsProvider); final isActive = playlist.collections.contains(props.collectionId); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 98dc22dc..9b47aaab 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -5,10 +5,10 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:encrypt/encrypt.dart'; -import 'package:media_kit/media_kit.dart'; +import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -27,6 +27,7 @@ part 'tables/scrobbler.dart'; part 'tables/skip_segment.dart'; part 'tables/source_match.dart'; part 'tables/audio_player_state.dart'; +part 'tables/history.dart'; part 'typeconverters/color.dart'; part 'typeconverters/locale.dart'; @@ -45,6 +46,7 @@ part 'typeconverters/map.dart'; AudioPlayerStateTable, PlaylistTable, PlaylistMediaTable, + HistoryTable, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 37cc930c..bb7d2fb6 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -3414,6 +3414,301 @@ class PlaylistMediaTableCompanion } } +class $HistoryTableTable extends HistoryTable + with TableInfo<$HistoryTableTable, HistoryTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $HistoryTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($HistoryTableTable.$convertertype); + static const VerificationMeta _itemIdMeta = const VerificationMeta('itemId'); + @override + late final GeneratedColumn itemId = GeneratedColumn( + 'item_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _dataMeta = const VerificationMeta('data'); + @override + late final GeneratedColumnWithTypeConverter, String> + data = GeneratedColumn('data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $HistoryTableTable.$converterdata); + @override + List get $columns => [id, createdAt, type, itemId, data]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'history_table'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + context.handle(_typeMeta, const VerificationResult.success()); + if (data.containsKey('item_id')) { + context.handle(_itemIdMeta, + itemId.isAcceptableOrUnknown(data['item_id']!, _itemIdMeta)); + } else if (isInserting) { + context.missing(_itemIdMeta); + } + context.handle(_dataMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + HistoryTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return HistoryTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + type: $HistoryTableTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + itemId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}item_id'])!, + data: $HistoryTableTable.$converterdata.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}data'])!), + ); + } + + @override + $HistoryTableTable createAlias(String alias) { + return $HistoryTableTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(HistoryEntryType.values); + static TypeConverter, String> $converterdata = + const MapTypeConverter(); +} + +class HistoryTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final HistoryEntryType type; + final String itemId; + final Map data; + const HistoryTableData( + {required this.id, + required this.createdAt, + required this.type, + required this.itemId, + required this.data}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + { + map['type'] = + Variable($HistoryTableTable.$convertertype.toSql(type)); + } + map['item_id'] = Variable(itemId); + { + map['data'] = + Variable($HistoryTableTable.$converterdata.toSql(data)); + } + return map; + } + + HistoryTableCompanion toCompanion(bool nullToAbsent) { + return HistoryTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + type: Value(type), + itemId: Value(itemId), + data: Value(data), + ); + } + + factory HistoryTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return HistoryTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + type: $HistoryTableTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + itemId: serializer.fromJson(json['itemId']), + data: serializer.fromJson>(json['data']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'type': serializer + .toJson($HistoryTableTable.$convertertype.toJson(type)), + 'itemId': serializer.toJson(itemId), + 'data': serializer.toJson>(data), + }; + } + + HistoryTableData copyWith( + {int? id, + DateTime? createdAt, + HistoryEntryType? type, + String? itemId, + Map? data}) => + HistoryTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + @override + String toString() { + return (StringBuffer('HistoryTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, type, itemId, data); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is HistoryTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.type == this.type && + other.itemId == this.itemId && + other.data == this.data); +} + +class HistoryTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value type; + final Value itemId; + final Value> data; + const HistoryTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.type = const Value.absent(), + this.itemId = const Value.absent(), + this.data = const Value.absent(), + }); + HistoryTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required HistoryEntryType type, + required String itemId, + required Map data, + }) : type = Value(type), + itemId = Value(itemId), + data = Value(data); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? type, + Expression? itemId, + Expression? data, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (type != null) 'type': type, + if (itemId != null) 'item_id': itemId, + if (data != null) 'data': data, + }); + } + + HistoryTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? type, + Value? itemId, + Value>? data}) { + return HistoryTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + itemId: itemId ?? this.itemId, + data: data ?? this.data, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (type.present) { + map['type'] = + Variable($HistoryTableTable.$convertertype.toSql(type.value)); + } + if (itemId.present) { + map['item_id'] = Variable(itemId.value); + } + if (data.present) { + map['data'] = + Variable($HistoryTableTable.$converterdata.toSql(data.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('HistoryTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('type: $type, ') + ..write('itemId: $itemId, ') + ..write('data: $data') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); _$AppDatabaseManager get managers => _$AppDatabaseManager(this); @@ -3432,6 +3727,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $PlaylistTableTable playlistTable = $PlaylistTableTable(this); late final $PlaylistMediaTableTable playlistMediaTable = $PlaylistMediaTableTable(this); + late final $HistoryTableTable historyTable = $HistoryTableTable(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', @@ -3450,6 +3746,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { audioPlayerStateTable, playlistTable, playlistMediaTable, + historyTable, uniqueBlacklist, uniqTrackMatch ]; @@ -5053,6 +5350,148 @@ class $$PlaylistMediaTableTableOrderingComposer } } +typedef $$HistoryTableTableInsertCompanionBuilder = HistoryTableCompanion + Function({ + Value id, + Value createdAt, + required HistoryEntryType type, + required String itemId, + required Map data, +}); +typedef $$HistoryTableTableUpdateCompanionBuilder = HistoryTableCompanion + Function({ + Value id, + Value createdAt, + Value type, + Value itemId, + Value> data, +}); + +class $$HistoryTableTableTableManager extends RootTableManager< + _$AppDatabase, + $HistoryTableTable, + HistoryTableData, + $$HistoryTableTableFilterComposer, + $$HistoryTableTableOrderingComposer, + $$HistoryTableTableProcessedTableManager, + $$HistoryTableTableInsertCompanionBuilder, + $$HistoryTableTableUpdateCompanionBuilder> { + $$HistoryTableTableTableManager(_$AppDatabase db, $HistoryTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$HistoryTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$HistoryTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$HistoryTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + Value type = const Value.absent(), + Value itemId = const Value.absent(), + Value> data = const Value.absent(), + }) => + HistoryTableCompanion( + id: id, + createdAt: createdAt, + type: type, + itemId: itemId, + data: data, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + required HistoryEntryType type, + required String itemId, + required Map data, + }) => + HistoryTableCompanion.insert( + id: id, + createdAt: createdAt, + type: type, + itemId: itemId, + data: data, + ), + )); +} + +class $$HistoryTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $HistoryTableTable, + HistoryTableData, + $$HistoryTableTableFilterComposer, + $$HistoryTableTableOrderingComposer, + $$HistoryTableTableProcessedTableManager, + $$HistoryTableTableInsertCompanionBuilder, + $$HistoryTableTableUpdateCompanionBuilder> { + $$HistoryTableTableProcessedTableManager(super.$state); +} + +class $$HistoryTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $HistoryTableTable> { + $$HistoryTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters + get type => $state.composableBuilder( + column: $state.table.type, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get itemId => $state.composableBuilder( + column: $state.table.itemId, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnWithTypeConverterFilters, Map, + String> + get data => $state.composableBuilder( + column: $state.table.data, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); +} + +class $$HistoryTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $HistoryTableTable> { + $$HistoryTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get type => $state.composableBuilder( + column: $state.table.type, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get itemId => $state.composableBuilder( + column: $state.table.itemId, + 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); @@ -5074,4 +5513,6 @@ class _$AppDatabaseManager { $$PlaylistTableTableTableManager(_db, _db.playlistTable); $$PlaylistMediaTableTableTableManager get playlistMediaTable => $$PlaylistMediaTableTableTableManager(_db, _db.playlistMediaTable); + $$HistoryTableTableTableManager get historyTable => + $$HistoryTableTableTableManager(_db, _db.historyTable); } diff --git a/lib/models/database/tables/history.dart b/lib/models/database/tables/history.dart new file mode 100644 index 00000000..23c16f17 --- /dev/null +++ b/lib/models/database/tables/history.dart @@ -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()(); + TextColumn get itemId => text()(); + TextColumn get data => + text().map(const MapTypeConverter())(); +} + +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; +} diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index de7aa5f8..dd914fad 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -35,7 +35,7 @@ class AlbumCard extends HookConsumerWidget { final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final playlistNotifier = ref.watch(audioPlayerProvider.notifier); - final historyNotifier = ref.read(playbackHistoryProvider.notifier); + final historyNotifier = ref.read(playbackHistoryActionsProvider); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); bool isPlaylistPlaying = useMemoized( diff --git a/lib/modules/home/sections/recent.dart b/lib/modules/home/sections/recent.dart index 5be2fcc2..b26c0e16 100644 --- a/lib/modules/home/sections/recent.dart +++ b/lib/modules/home/sections/recent.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/history/recent.dart'; -import 'package:spotube/provider/history/state.dart'; class HomeRecentlyPlayedSection extends HookConsumerWidget { const HomeRecentlyPlayedSection({super.key}); @@ -10,23 +12,28 @@ class HomeRecentlyPlayedSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final history = ref.watch(recentlyPlayedItems); + final historyData = + history.asData?.value ?? FakeData.historyRecentlyPlayedItems; - if (history.isEmpty) { + if (history.asData?.value.isEmpty == true) { return const SizedBox(); } - return HorizontalPlaybuttonCardView( - title: const Text('Recently Played'), - items: [ - for (final item in history) - if (item is PlaybackHistoryPlaylist) - item.playlist - else if (item is PlaybackHistoryAlbum) - item.album - ], - hasNextPage: false, - isLoadingNextPage: false, - onFetchMore: () {}, + return Skeletonizer( + enabled: history.isLoading, + child: HorizontalPlaybuttonCardView( + title: const Text('Recently Played'), + items: [ + for (final item in historyData) + if (item.playlist != null) + item.playlist + else if (item.album != null) + item.album + ], + hasNextPage: false, + isLoadingNextPage: false, + onFetchMore: () {}, + ), ); } } diff --git a/lib/modules/playlist/playlist_card.dart b/lib/modules/playlist/playlist_card.dart index 4e81a254..d6ea2a46 100644 --- a/lib/modules/playlist/playlist_card.dart +++ b/lib/modules/playlist/playlist_card.dart @@ -26,7 +26,7 @@ class PlaylistCard extends HookConsumerWidget { final playlistQueue = ref.watch(audioPlayerProvider); final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); - final historyNotifier = ref.read(playbackHistoryProvider.notifier); + final historyNotifier = ref.read(playbackHistoryActionsProvider); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; diff --git a/lib/modules/stats/summary/summary.dart b/lib/modules/stats/summary/summary.dart index 0b6c6040..ef8aa1b0 100644 --- a/lib/modules/stats/summary/summary.dart +++ b/lib/modules/stats/summary/summary.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/summary/summary_card.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -18,83 +20,87 @@ class StatsPageSummarySection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final summary = ref.watch(playbackHistorySummaryProvider); + final summaryData = summary.asData?.value ?? FakeData.historySummary; - return SliverPadding( - padding: const EdgeInsets.all(10), - sliver: SliverLayoutBuilder(builder: (context, constrains) { - return SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: constrains.isXs - ? 2 - : constrains.smAndDown - ? 3 - : constrains.mdAndDown - ? 4 - : constrains.lgAndDown - ? 5 - : 6, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: constrains.isXs ? 1.3 : 1.5, - ), - delegate: SliverChildListDelegate([ - SummaryCard( - title: summary.duration.inMinutes.toDouble(), - unit: "minutes", - description: 'Listened to music', - color: Colors.purple, - onTap: () { - ServiceUtils.pushNamed(context, StatsMinutesPage.name); - }, + return Skeletonizer.sliver( + enabled: summary.isLoading, + child: SliverPadding( + padding: const EdgeInsets.all(10), + sliver: SliverLayoutBuilder(builder: (context, constrains) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: constrains.isXs + ? 2 + : constrains.smAndDown + ? 3 + : constrains.mdAndDown + ? 4 + : constrains.lgAndDown + ? 5 + : 6, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: constrains.isXs ? 1.3 : 1.5, ), - SummaryCard( - title: summary.tracks.toDouble(), - unit: "songs", - description: 'Streamed overall', - color: Colors.lightBlue, - onTap: () { - ServiceUtils.pushNamed(context, StatsStreamsPage.name); - }, - ), - SummaryCard.unformatted( - title: usdFormatter.format(summary.fees.toDouble()), - unit: "", - description: 'Owed to artists\nthis month', - color: Colors.green, - onTap: () { - ServiceUtils.pushNamed(context, StatsStreamFeesPage.name); - }, - ), - SummaryCard( - title: summary.artists.toDouble(), - unit: "artist's", - description: 'Music reached you', - color: Colors.yellow, - onTap: () { - ServiceUtils.pushNamed(context, StatsArtistsPage.name); - }, - ), - SummaryCard( - title: summary.albums.toDouble(), - unit: "full albums", - description: 'Got your love', - color: Colors.pink, - onTap: () { - ServiceUtils.pushNamed(context, StatsAlbumsPage.name); - }, - ), - SummaryCard( - title: summary.playlists.toDouble(), - unit: "playlists", - description: 'Were on repeat', - color: Colors.teal, - onTap: () { - ServiceUtils.pushNamed(context, StatsPlaylistsPage.name); - }, - ), - ]), - ); - }), + delegate: SliverChildListDelegate([ + SummaryCard( + title: summaryData.duration.inMinutes.toDouble(), + unit: "minutes", + description: 'Listened to music', + color: Colors.purple, + onTap: () { + ServiceUtils.pushNamed(context, StatsMinutesPage.name); + }, + ), + SummaryCard( + title: summaryData.tracks.toDouble(), + unit: "songs", + description: 'Streamed overall', + color: Colors.lightBlue, + onTap: () { + ServiceUtils.pushNamed(context, StatsStreamsPage.name); + }, + ), + SummaryCard.unformatted( + title: usdFormatter.format(summaryData.fees.toDouble()), + unit: "", + description: 'Owed to artists\nthis month', + color: Colors.green, + onTap: () { + ServiceUtils.pushNamed(context, StatsStreamFeesPage.name); + }, + ), + SummaryCard( + title: summaryData.artists.toDouble(), + unit: "artist's", + description: 'Music reached you', + color: Colors.yellow, + onTap: () { + ServiceUtils.pushNamed(context, StatsArtistsPage.name); + }, + ), + SummaryCard( + title: summaryData.albums.toDouble(), + unit: "full albums", + description: 'Got your love', + color: Colors.pink, + onTap: () { + ServiceUtils.pushNamed(context, StatsAlbumsPage.name); + }, + ), + SummaryCard( + title: summaryData.playlists.toDouble(), + unit: "playlists", + description: 'Were on repeat', + color: Colors.teal, + onTap: () { + ServiceUtils.pushNamed(context, StatsPlaylistsPage.name); + }, + ), + ]), + ); + }), + ), ); } } diff --git a/lib/modules/stats/top/albums.dart b/lib/modules/stats/top/albums.dart index 808a58a4..bcaa75c5 100644 --- a/lib/modules/stats/top/albums.dart +++ b/lib/modules/stats/top/albums.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/top.dart'; @@ -11,19 +12,24 @@ class TopAlbums extends HookConsumerWidget { Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final albums = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.albums)); + .select((value) => value.whenData((s) => s.albums))); - return SliverList.builder( - itemCount: albums.length, - itemBuilder: (context, index) { - final album = albums[index]; - return StatsAlbumItem( - album: album.album, - info: Text( - "${compactNumberFormatter.format(album.count)} plays", - ), - ); - }, + final albumsData = albums.asData?.value ?? []; + + return Skeletonizer( + enabled: albums.isLoading, + child: SliverList.builder( + itemCount: albumsData.length, + itemBuilder: (context, index) { + final album = albumsData[index]; + return StatsAlbumItem( + album: album.album, + info: Text( + "${compactNumberFormatter.format(album.count)} plays", + ), + ); + }, + ), ); } } diff --git a/lib/modules/stats/top/artists.dart b/lib/modules/stats/top/artists.dart index 24e97601..094353f2 100644 --- a/lib/modules/stats/top/artists.dart +++ b/lib/modules/stats/top/artists.dart @@ -11,12 +11,14 @@ class TopArtists extends HookConsumerWidget { Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final artists = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.artists)); + .select((value) => value.whenData((s) => s.artists))); + + final artistsData = artists.asData?.value ?? []; return SliverList.builder( - itemCount: artists.length, + itemCount: artistsData.length, itemBuilder: (context, index) { - final artist = artists[index]; + final artist = artistsData[index]; return StatsArtistItem( artist: artist.artist, info: Text("${compactNumberFormatter.format(artist.count)} plays"), diff --git a/lib/modules/stats/top/tracks.dart b/lib/modules/stats/top/tracks.dart index ee37af3b..8bffa800 100644 --- a/lib/modules/stats/top/tracks.dart +++ b/lib/modules/stats/top/tracks.dart @@ -12,13 +12,15 @@ class TopTracks extends HookConsumerWidget { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final tracks = ref.watch( playbackHistoryTopProvider(historyDuration) - .select((value) => value.tracks), + .select((value) => value.whenData((s) => s.tracks)), ); + final tracksData = tracks.asData?.value ?? []; + return SliverList.builder( - itemCount: tracks.length, + itemCount: tracksData.length, itemBuilder: (context, index) { - final track = tracks[index]; + final track = tracksData[index]; return StatsTrackItem( track: track.track, info: Text( diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 3294bab5..21796725 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -139,14 +139,12 @@ class SyncedLyrics extends HookConsumerWidget { textAlign: TextAlign.center, child: InkWell( onTap: () async { - final duration = - await audioPlayer.duration ?? - Duration.zero; final time = Duration( seconds: lyricSlice.time.inSeconds - delay, ); - if (time > duration || time.isNegative) { + if (time > audioPlayer.duration || + time.isNegative) { return; } audioPlayer.seek(time); diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 868f068a..a13e500b 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -12,10 +12,10 @@ class StatsAlbumsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final albums = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.albums), - ); + final albums = ref.watch(playbackHistoryTopProvider(HistoryDuration.allTime) + .select((value) => value.whenData((s) => s.albums))); + + final albumsData = albums.asData?.value ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -24,9 +24,9 @@ class StatsAlbumsPage extends HookConsumerWidget { title: Text("Albums"), ), body: ListView.builder( - itemCount: albums.length, + itemCount: albumsData.length, itemBuilder: (context, index) { - final album = albums[index]; + final album = albumsData[index]; return StatsAlbumItem( album: album.album, info: Text("${compactNumberFormatter.format(album.count)} plays"), diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index b3f8c240..9ebdbe5d 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -14,9 +14,11 @@ class StatsArtistsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final artists = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.artists), + .select((s) => s.whenData((s) => s.artists)), ); + final artistsData = artists.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -24,9 +26,9 @@ class StatsArtistsPage extends HookConsumerWidget { title: Text("Artists"), ), body: ListView.builder( - itemCount: artists.length, + itemCount: artistsData.length, itemBuilder: (context, index) { - final artist = artists[index]; + final artist = artistsData[index]; return StatsArtistItem( artist: artist.artist, info: Text("${compactNumberFormatter.format(artist.count)} plays"), diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index ee141475..e881ec70 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -18,9 +18,11 @@ class StatsStreamFeesPage extends HookConsumerWidget { final artists = ref.watch( playbackHistoryTopProvider(HistoryDuration.days30) - .select((value) => value.artists), + .select((value) => value.whenData((s) => s.artists)), ); + final artistsData = artists.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -49,9 +51,9 @@ class StatsStreamFeesPage extends HookConsumerWidget { ), ), SliverList.builder( - itemCount: artists.length, + itemCount: artistsData.length, itemBuilder: (context, index) { - final artist = artists[index]; + final artist = artistsData[index]; return StatsArtistItem( artist: artist.artist, info: Text(usdFormatter.format(artist.count * 0.005)), diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index ea0a0c10..1d6a5844 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -16,9 +16,11 @@ class StatsMinutesPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final topTracks = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.tracks), + .select((s) => s.whenData((s) => s.tracks)), ); + final topTracksData = topTracks.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( title: Text("Minutes listened"), @@ -27,9 +29,9 @@ class StatsMinutesPage extends HookConsumerWidget { ), body: ListView.separated( separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracks.length, + itemCount: topTracksData.length, itemBuilder: (context, index) { - final (:track, :count) = topTracks[index]; + final (:track, :count) = topTracksData[index]; return StatsTrackItem( track: track, diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index d31f1dfa..94f8ce9d 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -14,9 +14,11 @@ class StatsPlaylistsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final playlists = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.playlists), + .select((s) => s.whenData((s) => s.playlists)), ); + final playlistsData = playlists.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -24,11 +26,11 @@ class StatsPlaylistsPage extends HookConsumerWidget { title: Text("Playlists"), ), body: ListView.builder( - itemCount: playlists.length, + itemCount: playlistsData.length, itemBuilder: (context, index) { - final playlist = playlists[index]; + final playlist = playlistsData[index]; return StatsPlaylistItem( - playlist: playlist.playlist.playlist, + playlist: playlist.playlist, info: Text("${compactNumberFormatter.format(playlist.count)} plays"), ); diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 3df34483..41f2d33a 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -16,9 +16,11 @@ class StatsStreamsPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final topTracks = ref.watch( playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.tracks), + .select((s) => s.whenData((s) => s.tracks)), ); + final topTracksData = topTracks.asData?.value ?? []; + return Scaffold( appBar: const PageWindowTitleBar( title: Text("Streamed songs"), @@ -27,9 +29,9 @@ class StatsStreamsPage extends HookConsumerWidget { ), body: ListView.separated( separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracks.length, + itemCount: topTracksData.length, itemBuilder: (context, index) { - final (:track, :count) = topTracks[index]; + final (:track, :count) = topTracksData[index]; return StatsTrackItem( track: track, diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 42944075..368fc6d9 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -45,8 +45,8 @@ class AudioPlayerStreamListeners { UserPreferences get preferences => ref.read(userPreferencesProvider); Discord get discord => ref.read(discordProvider); AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider); - PlaybackHistoryNotifier get history => - ref.read(playbackHistoryProvider.notifier); + PlaybackHistoryActions get history => + ref.read(playbackHistoryActionsProvider); Future updatePalette() async { final palette = ref.read(paletteProvider); diff --git a/lib/provider/history/history.dart b/lib/provider/history/history.dart index 4436626d..0c20a9e5 100644 --- a/lib/provider/history/history.dart +++ b/lib/provider/history/history.dart @@ -1,129 +1,68 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/history/state.dart'; -import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; -class PlaybackHistoryState { - final List items; - const PlaybackHistoryState({this.items = const []}); - - factory PlaybackHistoryState.fromJson(Map json) { - return PlaybackHistoryState( - items: json["items"] - ?.map( - (json) => PlaybackHistoryItem.fromJson(json), - ) - .toList() - .cast() ?? - [], - ); - } - - Map toJson() { - return { - "items": items.map((s) => s.toJson()).toList(), - }; - } - - PlaybackHistoryState copyWith({ - List? items, - }) { - return PlaybackHistoryState(items: items ?? this.items); - } -} - -class PlaybackHistoryNotifier - extends PersistedStateNotifier { +class PlaybackHistoryActions { final Ref ref; - PlaybackHistoryNotifier(this.ref) - : super(const PlaybackHistoryState(), "playback_history"); + AppDatabase get _db => ref.read(databaseProvider); - SpotifyApi get spotify => ref.read(spotifyProvider); + PlaybackHistoryActions(this.ref); - @override - FutureOr fromJson(Map json) => - PlaybackHistoryState.fromJson(json); - - @override - Map toJson() { - return state.toJson(); + Future _batchInsertHistoryEntries( + List entries) async { + await _db.batch((batch) { + batch.insertAll(_db.historyTable, entries); + }); } - void addPlaylists(List playlists) { - state = state.copyWith( - items: [ - ...state.items, - for (final playlist in playlists) - PlaybackHistoryItem.playlist( - date: DateTime.now(), playlist: playlist), - ], - ); + Future addPlaylists(List playlists) async { + await _batchInsertHistoryEntries([ + for (final playlist in playlists) + HistoryTableCompanion.insert( + type: HistoryEntryType.playlist, + itemId: playlist.id!, + data: playlist.toJson(), + ), + ]); } - void addAlbums(List albums) { - state = state.copyWith( - items: [ - ...state.items, - for (final album in albums) - PlaybackHistoryItem.album(date: DateTime.now(), album: album), - ], - ); + Future addAlbums(List albums) async { + await _batchInsertHistoryEntries([ + for (final albums in albums) + HistoryTableCompanion.insert( + type: HistoryEntryType.album, + itemId: albums.id!, + data: albums.toJson(), + ), + ]); } - void addTrack(Track track) async { - // For some reason Track's artists images are `null` - // so we need to fetch them from the API - final artists = - await spotify.artists.list(track.artists!.map((e) => e.id!).toList()); - - track.artists = artists.toList(); - - state = state.copyWith( - items: [ - ...state.items, - PlaybackHistoryItem.track(date: DateTime.now(), track: track), - ], - ); + Future addTracks(List tracks) async { + await _batchInsertHistoryEntries([ + for (final track in tracks) + HistoryTableCompanion.insert( + type: HistoryEntryType.track, + itemId: track.id!, + data: track.toJson(), + ), + ]); } - void clear() { - state = state.copyWith(items: []); + Future addTrack(Track track) async { + await _db.into(_db.historyTable).insert( + HistoryTableCompanion.insert( + type: HistoryEntryType.track, + itemId: track.id!, + data: track.toJson(), + ), + ); + } + + Future clear() async { + _db.delete(_db.historyTable).go(); } } -final playbackHistoryProvider = - StateNotifierProvider( - (ref) => PlaybackHistoryNotifier(ref), -); - -typedef PlaybackHistoryGrouped = ({ - List tracks, - List albums, - List playlists, -}); - -final playbackHistoryGroupedProvider = Provider((ref) { - final history = ref.watch(playbackHistoryProvider); - final tracks = history.items - .whereType() - .sorted((a, b) => b.date.compareTo(a.date)) - .toList(); - final albums = history.items - .whereType() - .sorted((a, b) => b.date.compareTo(a.date)) - .toList(); - final playlists = history.items - .whereType() - .sorted((a, b) => b.date.compareTo(a.date)) - .toList(); - - return ( - tracks: tracks, - albums: albums, - playlists: playlists, - ); -}); +final playbackHistoryActionsProvider = + Provider((ref) => PlaybackHistoryActions(ref)); diff --git a/lib/provider/history/recent.dart b/lib/provider/history/recent.dart index 9953858d..4e445500 100644 --- a/lib/provider/history/recent.dart +++ b/lib/provider/history/recent.dart @@ -1,40 +1,55 @@ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/history/state.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; -final recentlyPlayedItems = Provider((ref) { - return ref.watch( - playbackHistoryProvider.select( - (s) => s.items - .toSet() - // unique items - .whereIndexed( - (index, item) => - index == - s.items.lastIndexWhere( - (e) => switch ((e, item)) { - ( - PlaybackHistoryPlaylist(:final playlist), - PlaybackHistoryPlaylist(playlist: final playlist2) - ) => - playlist.id == playlist2.id, - ( - PlaybackHistoryAlbum(:final album), - PlaybackHistoryAlbum(album: final album2) - ) => - album.id == album2.id, - _ => false, - }, - ), - ) - .where( - (s) => s is PlaybackHistoryPlaylist || s is PlaybackHistoryAlbum, - ) - .take(10) - .sortedBy((s) => s.date) - .reversed - .toList(), - ), - ); -}); +class RecentlyPlayedItemNotifier extends AsyncNotifier> { + @override + build() async { + final database = ref.watch(databaseProvider); + + final uniqueItemIds = + await (database.selectOnly(database.historyTable, distinct: true) + ..addColumns([database.historyTable.itemId]) + ..where( + database.historyTable.type.isIn([ + HistoryEntryType.playlist.name, + HistoryEntryType.album.name, + ]), + ) + ..limit(10)) + .map((row) => row.read(database.historyTable.itemId)) + .get() + .then((value) => value.whereNotNull().toList()); + + final query = database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.isIn([ + HistoryEntryType.playlist.name, + HistoryEntryType.album.name, + ]) & + tbl.itemId.isIn(uniqueItemIds), + ) + ..orderBy([ + (tbl) => OrderingTerm( + expression: tbl.createdAt, + mode: OrderingMode.desc, + ), + ]); + + final subscription = query.watch().listen((event) { + state = AsyncData(event); + }); + + ref.onDispose(() => subscription.cancel()); + + return await query.get(); + } +} + +final recentlyPlayedItems = + AsyncNotifierProvider>( + () => RecentlyPlayedItemNotifier(), +); diff --git a/lib/provider/history/summary.dart b/lib/provider/history/summary.dart index 2aa86ac9..99df4c11 100644 --- a/lib/provider/history/summary.dart +++ b/lib/provider/history/summary.dart @@ -1,62 +1,197 @@ -import 'package:collection/collection.dart'; +import 'dart:async'; +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:drift/extensions/json1.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/history/history.dart'; -import 'package:spotube/provider/history/state.dart'; -import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; -final playbackHistorySummaryProvider = Provider((ref) { - final (:tracks, :albums, :playlists) = - ref.watch(playbackHistoryGroupedProvider); +class PlaybackHistorySummary { + final Duration duration; + final int tracks; + final int artists; + final double fees; + final int albums; + final int playlists; - final totalDurationListened = tracks.fold( - Duration.zero, - (previousValue, element) => previousValue + element.track.duration!, - ); + const PlaybackHistorySummary({ + required this.duration, + required this.tracks, + required this.artists, + required this.fees, + required this.albums, + required this.playlists, + }); - final totalTracksListened = tracks - .whereIndexed( - (i, track) => - i == tracks.lastIndexWhere((e) => e.track.id == track.track.id), - ) - .length; + PlaybackHistorySummary copyWith({ + Duration? duration, + int? tracks, + int? artists, + double? fees, + int? albums, + int? playlists, + }) { + return PlaybackHistorySummary( + duration: duration ?? this.duration, + tracks: tracks ?? this.tracks, + artists: artists ?? this.artists, + fees: fees ?? this.fees, + albums: albums ?? this.albums, + playlists: playlists ?? this.playlists, + ); + } +} - final artists = - tracks.map((e) => e.track.artists).expand((e) => e ?? []).toList(); +class PlaybackHistorySummaryNotifier + extends AsyncNotifier { + @override + build() async { + final database = ref.watch(databaseProvider); - final totalArtistsListened = artists - .whereIndexed( - (i, artist) => i == artists.lastIndexWhere((e) => e.id == artist.id), - ) - .length; + final uniqItemIdCountingCol = + database.historyTable.itemId.count(distinct: true); + final itemIdCountingCol = database.historyTable.itemId.count(); + final durationSumJsonColumn = + database.historyTable.data.jsonExtract(r"$.duration_ms").sum(); + final artistCountingCol = + database.historyTable.data.jsonExtract(r"$.artists"); - final totalAlbumsListened = albums - .whereIndexed( - (i, album) => - i == albums.lastIndexWhere((e) => e.album.id == album.album.id), - ) - .length; + final totalTracksListenedQuery = (database.selectOnly(database.historyTable) + ..addColumns([uniqItemIdCountingCol]) + ..where( + database.historyTable.type.equals(HistoryEntryType.track.name))) + .map((row) => row.read(uniqItemIdCountingCol)); - final totalPlaylistsListened = playlists - .whereIndexed( - (i, playlist) => - i == - playlists - .lastIndexWhere((e) => e.playlist.id == playlist.playlist.id), - ) - .length; + final totalDurationListenedQuery = (database + .selectOnly(database.historyTable) + ..addColumns([durationSumJsonColumn]) + ..where( + database.historyTable.type.equals(HistoryEntryType.track.name))) + .map( + (row) => Duration(milliseconds: row.read(durationSumJsonColumn) ?? 0), + ); - final tracksThisMonth = ref.watch( - playbackHistoryTopProvider(HistoryDuration.days30).select((s) => s.tracks), - ); + final totalArtistsListenedQuery = + (database.selectOnly(database.historyTable) + ..addColumns([artistCountingCol]) + ..where( + database.historyTable.type.equals(HistoryEntryType.track.name), + )) + .map( + (row) { + final data = jsonDecode(row.read(artistCountingCol)!) as List; + return data.map((e) => e['id'] as String).cast().toList(); + }, + ); - final streams = tracksThisMonth.fold(0, (acc, el) => acc + el.count); + final totalAlbumsListenedQuery = (database.selectOnly(database.historyTable) + ..addColumns([uniqItemIdCountingCol]) + ..where( + database.historyTable.type.equals(HistoryEntryType.album.name))) + .map((row) => row.read(uniqItemIdCountingCol)); - return ( - duration: totalDurationListened, - tracks: totalTracksListened, - artists: totalArtistsListened, - fees: streams * 0.005, // Spotify pays $0.003 to $0.005 - albums: totalAlbumsListened, - playlists: totalPlaylistsListened, - ); -}); + final totalPlaylistsListenedQuery = + (database.selectOnly(database.historyTable) + ..addColumns([uniqItemIdCountingCol]) + ..where( + database.historyTable.type + .equals(HistoryEntryType.playlist.name), + )) + .map((row) => row.read(uniqItemIdCountingCol)); + + final oldestDate = DateTime.now().copyWith(day: 1, hour: 0, minute: 0); + final newestDate = DateTime.now().copyWith(day: 30, hour: 23, minute: 59); + final totalTracksListenedThisMonthQuery = + (database.selectOnly(database.historyTable) + ..addColumns([itemIdCountingCol]) + ..where( + database.historyTable.type.equals( + HistoryEntryType.track.name, + ) & + database.historyTable.createdAt + .isBetweenValues(oldestDate, newestDate), + )) + .map((row) => row.read(itemIdCountingCol)); + + final subscriptions = [ + totalTracksListenedQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + tracks: event, + )); + }), + totalDurationListenedQuery.watchSingle().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + duration: event, + )); + }), + totalArtistsListenedQuery.watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + artists: event.expand((e) => e).toSet().length, + )); + }), + totalAlbumsListenedQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + albums: event, + )); + }), + totalPlaylistsListenedQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + playlists: event, + )); + }), + totalTracksListenedThisMonthQuery.watchSingle().listen((event) { + if (event == null || state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + fees: event * 0.005, + )); + }), + ]; + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + + return database.transaction(() async { + final totalTracksListened = + await totalTracksListenedQuery.getSingle() ?? 0; + + final totalDurationListened = + await totalDurationListenedQuery.getSingle(); + + final totalArtistsListened = await totalArtistsListenedQuery + .get() + .then((value) => value.expand((e) => e).toSet().length); + + final totalAlbumsListened = + await totalAlbumsListenedQuery.getSingle() ?? 0; + + final totalPlaylistsListened = + await totalPlaylistsListenedQuery.getSingle() ?? 0; + + final totalTracksListenedThisMonth = + await totalTracksListenedThisMonthQuery.getSingle() ?? 0; + + return PlaybackHistorySummary( + duration: totalDurationListened, + tracks: totalTracksListened, + artists: totalArtistsListened, + fees: totalTracksListenedThisMonth * 0.005, + albums: totalAlbumsListened, + playlists: totalPlaylistsListened, + ); + }); + } +} + +final playbackHistorySummaryProvider = AsyncNotifierProvider< + PlaybackHistorySummaryNotifier, PlaybackHistorySummary>( + () => PlaybackHistorySummaryNotifier(), +); diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index 7d4594f0..aa12c9b3 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -1,95 +1,212 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/history/history.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/history/state.dart'; final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); -final playbackHistoryTopProvider = - Provider.family((ref, HistoryDuration durationState) { - final grouped = ref.watch(playbackHistoryGroupedProvider); +typedef PlaybackHistoryTrack = ({int count, Track track}); +typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); +typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); +typedef PlaybackHistoryArtist = ({int count, Artist artist}); - final duration = switch (durationState) { - HistoryDuration.allTime => const Duration(days: 365 * 2003), - HistoryDuration.days7 => const Duration(days: 7), - HistoryDuration.days30 => const Duration(days: 30), - HistoryDuration.months6 => const Duration(days: 30 * 6), - HistoryDuration.year => const Duration(days: 365), - HistoryDuration.years2 => const Duration(days: 365 * 2), - }; - final tracks = grouped.tracks - .where( - (item) => item.date.isAfter( - DateTime.now().subtract(duration), - ), - ) - .toList(); - final albums = grouped.albums - .where( - (item) => item.date.isAfter( - DateTime.now().subtract(duration), - ), - ) - .toList(); +class PlaybackHistoryTopState { + final List tracks; + final List albums; + final List playlists; + final List artists; - final playlists = grouped.playlists - .where( - (item) => item.date.isAfter( - DateTime.now().subtract(duration), - ), - ) - .toList(); + const PlaybackHistoryTopState({ + required this.tracks, + required this.albums, + required this.playlists, + required this.artists, + }); - final tracksWithCount = groupBy( - tracks, - (track) => track.track.id!, - ) - .entries - .map((entry) { - return (count: entry.value.length, track: entry.value.first.track); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); + PlaybackHistoryTopState copyWith({ + List? tracks, + List? albums, + List? playlists, + List? artists, + }) { + return PlaybackHistoryTopState( + tracks: tracks ?? this.tracks, + albums: albums ?? this.albums, + playlists: playlists ?? this.playlists, + artists: artists ?? this.artists, + ); + } +} - final albumsWithTrackAlbums = [ - for (final historicAlbum in albums) historicAlbum.album, - for (final track in tracks) track.track.album! - ]; +class PlaybackHistoryTopNotifier + extends FamilyAsyncNotifier { + @override + build(arg) async { + final database = ref.watch(databaseProvider); - final albumsWithCount = groupBy(albumsWithTrackAlbums, (album) => album.id!) - .entries - .map((entry) { - return (count: entry.value.length, album: entry.value.first); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); + final duration = switch (arg) { + HistoryDuration.allTime => const Duration(days: 365 * 2003), + HistoryDuration.days7 => const Duration(days: 7), + HistoryDuration.days30 => const Duration(days: 30), + HistoryDuration.months6 => const Duration(days: 30 * 6), + HistoryDuration.year => const Duration(days: 365), + HistoryDuration.years2 => const Duration(days: 365 * 2), + }; - final artists = - tracks.map((track) => track.track.artists).expand((e) => e ?? []); + final tracksQuery = (database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.track) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(duration), + ), + )); - final artistsWithCount = groupBy(artists, (artist) => artist.id!) - .entries - .map((entry) { - return (count: entry.value.length, artist: entry.value.first); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); + final albumsQuery = database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.album) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(duration), + ), + ); - final playlistsWithCount = - groupBy(playlists, (playlist) => playlist.playlist.id!) - .entries - .map((entry) { - return (count: entry.value.length, playlist: entry.value.first); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); + final playlistsQuery = database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.playlist) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(duration), + ), + ); - return ( - tracks: tracksWithCount, - albums: albumsWithCount, - artists: artistsWithCount, - playlists: playlistsWithCount, - ); -}); + final subscriptions = [ + tracksQuery.watch().listen((event) { + if (state.asData == null) return; + final artists = event + .map((track) => track.track!.artists) + .expand((e) => e ?? []); + state = AsyncData(state.asData!.value.copyWith( + tracks: getTracksWithCount(event), + artists: getArtistsWithCount(artists), + )); + }), + albumsQuery.watch().listen((event) async { + if (state.asData == null) return; + final tracks = await tracksQuery.get(); + + final albumsWithTrackAlbums = [ + for (final historicAlbum in event) historicAlbum.album!, + for (final track in tracks) track.track!.album! + ]; + + state = AsyncData(state.asData!.value.copyWith( + albums: getAlbumsWithCount(albumsWithTrackAlbums), + )); + }), + playlistsQuery.watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + playlists: getPlaylistsWithCount(event), + )); + }), + ]; + + ref.onDispose(() { + for (final subscription in subscriptions) { + subscription.cancel(); + } + }); + + return database.transaction(() async { + final tracks = await tracksQuery.get(); + final albums = await albumsQuery.get(); + final playlists = await playlistsQuery.get(); + + final tracksWithCount = getTracksWithCount(tracks); + + final albumsWithTrackAlbums = [ + for (final historicAlbum in albums) historicAlbum.album!, + for (final track in tracks) track.track!.album! + ]; + + final albumsWithCount = getAlbumsWithCount(albumsWithTrackAlbums); + + final artists = tracks + .map((track) => track.track!.artists) + .expand((e) => e ?? []); + + final artistsWithCount = getArtistsWithCount(artists); + + final playlistsWithCount = getPlaylistsWithCount(playlists); + + return PlaybackHistoryTopState( + tracks: tracksWithCount, + albums: albumsWithCount, + artists: artistsWithCount, + playlists: playlistsWithCount, + ); + }); + } + + List getTracksWithCount(List tracks) { + return groupBy( + tracks, + (track) => track.track!.id!, + ) + .entries + .map((entry) { + return (count: entry.value.length, track: entry.value.first.track!); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } + + List getAlbumsWithCount( + List albumsWithTrackAlbums, + ) { + return groupBy(albumsWithTrackAlbums, (album) => album.id!) + .entries + .map((entry) { + return (count: entry.value.length, album: entry.value.first); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } + + List getArtistsWithCount(Iterable artists) { + return groupBy(artists, (artist) => artist.id!) + .entries + .map((entry) { + return (count: entry.value.length, artist: entry.value.first); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } + + List getPlaylistsWithCount( + List playlists, + ) { + return groupBy(playlists, (playlist) => playlist.playlist!.id!) + .entries + .map((entry) { + return ( + count: entry.value.length, + playlist: entry.value.first.playlist!, + ); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } +} + +final playbackHistoryTopProvider = AsyncNotifierProviderFamily< + PlaybackHistoryTopNotifier, + PlaybackHistoryTopState, + HistoryDuration>(PlaybackHistoryTopNotifier.new); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart index a2fa70b8..8e75a87e 100644 --- a/lib/provider/server/routes/connect.dart +++ b/lib/provider/server/routes/connect.dart @@ -40,8 +40,8 @@ class ServerConnectRoutes { AudioPlayerNotifier get audioPlayerNotifier => ref.read(audioPlayerProvider.notifier); - PlaybackHistoryNotifier get historyNotifier => - ref.read(playbackHistoryProvider.notifier); + PlaybackHistoryActions get historyNotifier => + ref.read(playbackHistoryActionsProvider); Stream get connectClientStream => _connectClientStreamController.stream; From 1cfd377c298e98f460a082e1196c441e71291079 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 11:01:40 +0600 Subject: [PATCH 17/24] 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(); - } -} From a3021e4c52b24c5a6870eeb89fa00e3f8bd51636 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 14:14:02 +0600 Subject: [PATCH 18/24] chore: removed unused files --- lib/collections/cache_keys.dart | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 lib/collections/cache_keys.dart diff --git a/lib/collections/cache_keys.dart b/lib/collections/cache_keys.dart deleted file mode 100644 index bca13322..00000000 --- a/lib/collections/cache_keys.dart +++ /dev/null @@ -1,21 +0,0 @@ -abstract class LocalStorageKeys { - static String saveTrackLyrics = 'save_track_lyrics'; - static String recommendationMarket = 'recommendation_market'; - static String ytSearchFormate = 'youtube_search_format'; - - static String clientId = 'clientId'; - static String clientSecret = 'clientSecret'; - static String accessToken = 'accessToken'; - static String refreshToken = 'refreshToken'; - static String expiration = "expiration"; - static String geniusAccessToken = "genius_access_token"; - - static String themeMode = "theme_mode"; - static String nextTrackHotKey = "next_track_hot_key"; - static String prevTrackHotKey = "prev_track_hot_key"; - static String playPauseHotKey = "play_pause_hot_key"; - - static String volume = "volume"; - - static String windowSizeInfo = "window_size_info"; -} From ffb3a3377fe84947213b11aed100d2dd049bd13f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 15:44:24 +0600 Subject: [PATCH 19/24] chore: add migration script to migrate hive to drift --- lib/main.dart | 16 +- lib/modules/stats/top/top.dart | 2 +- lib/pages/stats/albums/albums.dart | 2 +- lib/pages/stats/artists/artists.dart | 2 +- lib/pages/stats/fees/fees.dart | 2 +- lib/pages/stats/minutes/minutes.dart | 2 +- lib/pages/stats/playlists/playlists.dart | 2 +- lib/pages/stats/streams/streams.dart | 2 +- lib/provider/history/state.dart | 35 - lib/provider/history/state.freezed.dart | 644 -------- lib/provider/history/state.g.dart | 55 - lib/provider/history/top.dart | 10 +- lib/services/kv_store/encrypted_kv_store.dart | 2 + lib/services/kv_store/kv_store.dart | 5 + lib/utils/migrations/adapters.dart | 320 ++++ lib/utils/migrations/adapters.freezed.dart | 1380 +++++++++++++++++ lib/utils/migrations/adapters.g.dart | 600 +++++++ lib/utils/migrations/cache_box.dart | 100 ++ lib/utils/migrations/hive.dart | 316 ++++ 19 files changed, 2754 insertions(+), 743 deletions(-) delete mode 100644 lib/provider/history/state.dart delete mode 100644 lib/provider/history/state.freezed.dart delete mode 100644 lib/provider/history/state.g.dart create mode 100644 lib/utils/migrations/adapters.dart create mode 100644 lib/utils/migrations/adapters.freezed.dart create mode 100644 lib/utils/migrations/adapters.g.dart create mode 100644 lib/utils/migrations/cache_box.dart create mode 100644 lib/utils/migrations/hive.dart diff --git a/lib/main.dart b/lib/main.dart index 69c89062..cb553115 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,9 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart'; import 'package:spotube/hooks/configurators/use_deep_linking.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; +import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/audio_player/audio_player_streams.dart'; +import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/server/bonsoir.dart'; import 'package:spotube/provider/server/server.dart'; import 'package:spotube/provider/tray_manager/tray_manager.dart'; @@ -33,6 +35,7 @@ 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/migrations/hive.dart'; import 'package:spotube/utils/platform.dart'; import 'package:system_theme/system_theme.dart'; import 'package:path_provider/path_provider.dart'; @@ -84,12 +87,23 @@ Future main(List rawArgs) async { Hive.init(hiveCacheDir); + final database = AppDatabase(); + + await migrateFromHiveToDrift(database); + if (kIsDesktop) { await localNotifier.setup(appName: "Spotube"); await WindowManagerTools.initialize(); } - runApp(const ProviderScope(child: Spotube())); + runApp( + ProviderScope( + overrides: [ + databaseProvider.overrideWith((ref) => database), + ], + child: const Spotube(), + ), + ); }); } diff --git a/lib/modules/stats/top/top.dart b/lib/modules/stats/top/top.dart index 52529f3f..ea52c517 100644 --- a/lib/modules/stats/top/top.dart +++ b/lib/modules/stats/top/top.dart @@ -5,7 +5,7 @@ import 'package:spotube/components/themed_button_tab_bar.dart'; import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/tracks.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsPageTopSection extends HookConsumerWidget { diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index a13e500b..859eaf26 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsAlbumsPage extends HookConsumerWidget { diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index 9ebdbe5d..e6dadd95 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsArtistsPage extends HookConsumerWidget { diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index e881ec70..e1d701eb 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -4,7 +4,7 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsStreamFeesPage extends HookConsumerWidget { diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index 1d6a5844..587e9007 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsMinutesPage extends HookConsumerWidget { diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index 94f8ce9d..f5ee62d0 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsPlaylistsPage extends HookConsumerWidget { diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 41f2d33a..20e8ff96 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; -import 'package:spotube/provider/history/state.dart'; + import 'package:spotube/provider/history/top.dart'; class StatsStreamsPage extends HookConsumerWidget { diff --git a/lib/provider/history/state.dart b/lib/provider/history/state.dart deleted file mode 100644 index 67658502..00000000 --- a/lib/provider/history/state.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:spotify/spotify.dart'; - -part 'state.freezed.dart'; -part 'state.g.dart'; - -enum HistoryDuration { - allTime, - days7, - days30, - months6, - year, - years2, -} - -@freezed -class PlaybackHistoryItem with _$PlaybackHistoryItem { - factory PlaybackHistoryItem.playlist({ - required DateTime date, - required PlaylistSimple playlist, - }) = PlaybackHistoryPlaylist; - - factory PlaybackHistoryItem.album({ - required DateTime date, - required AlbumSimple album, - }) = PlaybackHistoryAlbum; - - factory PlaybackHistoryItem.track({ - required DateTime date, - required Track track, - }) = PlaybackHistoryTrack; - - factory PlaybackHistoryItem.fromJson(Map json) => - _$PlaybackHistoryItemFromJson(json); -} diff --git a/lib/provider/history/state.freezed.dart b/lib/provider/history/state.freezed.dart deleted file mode 100644 index e2ee9421..00000000 --- a/lib/provider/history/state.freezed.dart +++ /dev/null @@ -1,644 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -PlaybackHistoryItem _$PlaybackHistoryItemFromJson(Map json) { - switch (json['runtimeType']) { - case 'playlist': - return PlaybackHistoryPlaylist.fromJson(json); - case 'album': - return PlaybackHistoryAlbum.fromJson(json); - case 'track': - return PlaybackHistoryTrack.fromJson(json); - - default: - throw CheckedFromJsonException(json, 'runtimeType', 'PlaybackHistoryItem', - 'Invalid union type "${json['runtimeType']}"!'); - } -} - -/// @nodoc -mixin _$PlaybackHistoryItem { - DateTime get date => throw _privateConstructorUsedError; - @optionalTypeArgs - TResult when({ - required TResult Function(DateTime date, PlaylistSimple playlist) playlist, - required TResult Function(DateTime date, AlbumSimple album) album, - required TResult Function(DateTime date, Track track) track, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult? Function(DateTime date, AlbumSimple album)? album, - TResult? Function(DateTime date, Track track)? track, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult Function(DateTime date, AlbumSimple album)? album, - TResult Function(DateTime date, Track track)? track, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map({ - required TResult Function(PlaybackHistoryPlaylist value) playlist, - required TResult Function(PlaybackHistoryAlbum value) album, - required TResult Function(PlaybackHistoryTrack value) track, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(PlaybackHistoryPlaylist value)? playlist, - TResult? Function(PlaybackHistoryAlbum value)? album, - TResult? Function(PlaybackHistoryTrack value)? track, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap({ - TResult Function(PlaybackHistoryPlaylist value)? playlist, - TResult Function(PlaybackHistoryAlbum value)? album, - TResult Function(PlaybackHistoryTrack value)? track, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $PlaybackHistoryItemCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $PlaybackHistoryItemCopyWith<$Res> { - factory $PlaybackHistoryItemCopyWith( - PlaybackHistoryItem value, $Res Function(PlaybackHistoryItem) then) = - _$PlaybackHistoryItemCopyWithImpl<$Res, PlaybackHistoryItem>; - @useResult - $Res call({DateTime date}); -} - -/// @nodoc -class _$PlaybackHistoryItemCopyWithImpl<$Res, $Val extends PlaybackHistoryItem> - implements $PlaybackHistoryItemCopyWith<$Res> { - _$PlaybackHistoryItemCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? date = null, - }) { - return _then(_value.copyWith( - date: null == date - ? _value.date - : date // ignore: cast_nullable_to_non_nullable - as DateTime, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$PlaybackHistoryPlaylistImplCopyWith<$Res> - implements $PlaybackHistoryItemCopyWith<$Res> { - factory _$$PlaybackHistoryPlaylistImplCopyWith( - _$PlaybackHistoryPlaylistImpl value, - $Res Function(_$PlaybackHistoryPlaylistImpl) then) = - __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({DateTime date, PlaylistSimple playlist}); -} - -/// @nodoc -class __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res> - extends _$PlaybackHistoryItemCopyWithImpl<$Res, - _$PlaybackHistoryPlaylistImpl> - implements _$$PlaybackHistoryPlaylistImplCopyWith<$Res> { - __$$PlaybackHistoryPlaylistImplCopyWithImpl( - _$PlaybackHistoryPlaylistImpl _value, - $Res Function(_$PlaybackHistoryPlaylistImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? date = null, - Object? playlist = null, - }) { - return _then(_$PlaybackHistoryPlaylistImpl( - date: null == date - ? _value.date - : date // ignore: cast_nullable_to_non_nullable - as DateTime, - playlist: null == playlist - ? _value.playlist - : playlist // ignore: cast_nullable_to_non_nullable - as PlaylistSimple, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist { - _$PlaybackHistoryPlaylistImpl( - {required this.date, required this.playlist, final String? $type}) - : $type = $type ?? 'playlist'; - - factory _$PlaybackHistoryPlaylistImpl.fromJson(Map json) => - _$$PlaybackHistoryPlaylistImplFromJson(json); - - @override - final DateTime date; - @override - final PlaylistSimple playlist; - - @JsonKey(name: 'runtimeType') - final String $type; - - @override - String toString() { - return 'PlaybackHistoryItem.playlist(date: $date, playlist: $playlist)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$PlaybackHistoryPlaylistImpl && - (identical(other.date, date) || other.date == date) && - (identical(other.playlist, playlist) || - other.playlist == playlist)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, date, playlist); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> - get copyWith => __$$PlaybackHistoryPlaylistImplCopyWithImpl< - _$PlaybackHistoryPlaylistImpl>(this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function(DateTime date, PlaylistSimple playlist) playlist, - required TResult Function(DateTime date, AlbumSimple album) album, - required TResult Function(DateTime date, Track track) track, - }) { - return playlist(date, this.playlist); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult? Function(DateTime date, AlbumSimple album)? album, - TResult? Function(DateTime date, Track track)? track, - }) { - return playlist?.call(date, this.playlist); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult Function(DateTime date, AlbumSimple album)? album, - TResult Function(DateTime date, Track track)? track, - required TResult orElse(), - }) { - if (playlist != null) { - return playlist(date, this.playlist); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(PlaybackHistoryPlaylist value) playlist, - required TResult Function(PlaybackHistoryAlbum value) album, - required TResult Function(PlaybackHistoryTrack value) track, - }) { - return playlist(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(PlaybackHistoryPlaylist value)? playlist, - TResult? Function(PlaybackHistoryAlbum value)? album, - TResult? Function(PlaybackHistoryTrack value)? track, - }) { - return playlist?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(PlaybackHistoryPlaylist value)? playlist, - TResult Function(PlaybackHistoryAlbum value)? album, - TResult Function(PlaybackHistoryTrack value)? track, - required TResult orElse(), - }) { - if (playlist != null) { - return playlist(this); - } - return orElse(); - } - - @override - Map toJson() { - return _$$PlaybackHistoryPlaylistImplToJson( - this, - ); - } -} - -abstract class PlaybackHistoryPlaylist implements PlaybackHistoryItem { - factory PlaybackHistoryPlaylist( - {required final DateTime date, - required final PlaylistSimple playlist}) = _$PlaybackHistoryPlaylistImpl; - - factory PlaybackHistoryPlaylist.fromJson(Map json) = - _$PlaybackHistoryPlaylistImpl.fromJson; - - @override - DateTime get date; - PlaylistSimple get playlist; - @override - @JsonKey(ignore: true) - _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> - get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class _$$PlaybackHistoryAlbumImplCopyWith<$Res> - implements $PlaybackHistoryItemCopyWith<$Res> { - factory _$$PlaybackHistoryAlbumImplCopyWith(_$PlaybackHistoryAlbumImpl value, - $Res Function(_$PlaybackHistoryAlbumImpl) then) = - __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({DateTime date, AlbumSimple album}); -} - -/// @nodoc -class __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res> - extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryAlbumImpl> - implements _$$PlaybackHistoryAlbumImplCopyWith<$Res> { - __$$PlaybackHistoryAlbumImplCopyWithImpl(_$PlaybackHistoryAlbumImpl _value, - $Res Function(_$PlaybackHistoryAlbumImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? date = null, - Object? album = null, - }) { - return _then(_$PlaybackHistoryAlbumImpl( - date: null == date - ? _value.date - : date // ignore: cast_nullable_to_non_nullable - as DateTime, - album: null == album - ? _value.album - : album // ignore: cast_nullable_to_non_nullable - as AlbumSimple, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum { - _$PlaybackHistoryAlbumImpl( - {required this.date, required this.album, final String? $type}) - : $type = $type ?? 'album'; - - factory _$PlaybackHistoryAlbumImpl.fromJson(Map json) => - _$$PlaybackHistoryAlbumImplFromJson(json); - - @override - final DateTime date; - @override - final AlbumSimple album; - - @JsonKey(name: 'runtimeType') - final String $type; - - @override - String toString() { - return 'PlaybackHistoryItem.album(date: $date, album: $album)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$PlaybackHistoryAlbumImpl && - (identical(other.date, date) || other.date == date) && - (identical(other.album, album) || other.album == album)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, date, album); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> - get copyWith => - __$$PlaybackHistoryAlbumImplCopyWithImpl<_$PlaybackHistoryAlbumImpl>( - this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function(DateTime date, PlaylistSimple playlist) playlist, - required TResult Function(DateTime date, AlbumSimple album) album, - required TResult Function(DateTime date, Track track) track, - }) { - return album(date, this.album); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult? Function(DateTime date, AlbumSimple album)? album, - TResult? Function(DateTime date, Track track)? track, - }) { - return album?.call(date, this.album); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult Function(DateTime date, AlbumSimple album)? album, - TResult Function(DateTime date, Track track)? track, - required TResult orElse(), - }) { - if (album != null) { - return album(date, this.album); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(PlaybackHistoryPlaylist value) playlist, - required TResult Function(PlaybackHistoryAlbum value) album, - required TResult Function(PlaybackHistoryTrack value) track, - }) { - return album(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(PlaybackHistoryPlaylist value)? playlist, - TResult? Function(PlaybackHistoryAlbum value)? album, - TResult? Function(PlaybackHistoryTrack value)? track, - }) { - return album?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(PlaybackHistoryPlaylist value)? playlist, - TResult Function(PlaybackHistoryAlbum value)? album, - TResult Function(PlaybackHistoryTrack value)? track, - required TResult orElse(), - }) { - if (album != null) { - return album(this); - } - return orElse(); - } - - @override - Map toJson() { - return _$$PlaybackHistoryAlbumImplToJson( - this, - ); - } -} - -abstract class PlaybackHistoryAlbum implements PlaybackHistoryItem { - factory PlaybackHistoryAlbum( - {required final DateTime date, - required final AlbumSimple album}) = _$PlaybackHistoryAlbumImpl; - - factory PlaybackHistoryAlbum.fromJson(Map json) = - _$PlaybackHistoryAlbumImpl.fromJson; - - @override - DateTime get date; - AlbumSimple get album; - @override - @JsonKey(ignore: true) - _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> - get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class _$$PlaybackHistoryTrackImplCopyWith<$Res> - implements $PlaybackHistoryItemCopyWith<$Res> { - factory _$$PlaybackHistoryTrackImplCopyWith(_$PlaybackHistoryTrackImpl value, - $Res Function(_$PlaybackHistoryTrackImpl) then) = - __$$PlaybackHistoryTrackImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({DateTime date, Track track}); -} - -/// @nodoc -class __$$PlaybackHistoryTrackImplCopyWithImpl<$Res> - extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryTrackImpl> - implements _$$PlaybackHistoryTrackImplCopyWith<$Res> { - __$$PlaybackHistoryTrackImplCopyWithImpl(_$PlaybackHistoryTrackImpl _value, - $Res Function(_$PlaybackHistoryTrackImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? date = null, - Object? track = null, - }) { - return _then(_$PlaybackHistoryTrackImpl( - date: null == date - ? _value.date - : date // ignore: cast_nullable_to_non_nullable - as DateTime, - track: null == track - ? _value.track - : track // ignore: cast_nullable_to_non_nullable - as Track, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack { - _$PlaybackHistoryTrackImpl( - {required this.date, required this.track, final String? $type}) - : $type = $type ?? 'track'; - - factory _$PlaybackHistoryTrackImpl.fromJson(Map json) => - _$$PlaybackHistoryTrackImplFromJson(json); - - @override - final DateTime date; - @override - final Track track; - - @JsonKey(name: 'runtimeType') - final String $type; - - @override - String toString() { - return 'PlaybackHistoryItem.track(date: $date, track: $track)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$PlaybackHistoryTrackImpl && - (identical(other.date, date) || other.date == date) && - (identical(other.track, track) || other.track == track)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, date, track); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> - get copyWith => - __$$PlaybackHistoryTrackImplCopyWithImpl<_$PlaybackHistoryTrackImpl>( - this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function(DateTime date, PlaylistSimple playlist) playlist, - required TResult Function(DateTime date, AlbumSimple album) album, - required TResult Function(DateTime date, Track track) track, - }) { - return track(date, this.track); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult? Function(DateTime date, AlbumSimple album)? album, - TResult? Function(DateTime date, Track track)? track, - }) { - return track?.call(date, this.track); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(DateTime date, PlaylistSimple playlist)? playlist, - TResult Function(DateTime date, AlbumSimple album)? album, - TResult Function(DateTime date, Track track)? track, - required TResult orElse(), - }) { - if (track != null) { - return track(date, this.track); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(PlaybackHistoryPlaylist value) playlist, - required TResult Function(PlaybackHistoryAlbum value) album, - required TResult Function(PlaybackHistoryTrack value) track, - }) { - return track(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(PlaybackHistoryPlaylist value)? playlist, - TResult? Function(PlaybackHistoryAlbum value)? album, - TResult? Function(PlaybackHistoryTrack value)? track, - }) { - return track?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(PlaybackHistoryPlaylist value)? playlist, - TResult Function(PlaybackHistoryAlbum value)? album, - TResult Function(PlaybackHistoryTrack value)? track, - required TResult orElse(), - }) { - if (track != null) { - return track(this); - } - return orElse(); - } - - @override - Map toJson() { - return _$$PlaybackHistoryTrackImplToJson( - this, - ); - } -} - -abstract class PlaybackHistoryTrack implements PlaybackHistoryItem { - factory PlaybackHistoryTrack( - {required final DateTime date, - required final Track track}) = _$PlaybackHistoryTrackImpl; - - factory PlaybackHistoryTrack.fromJson(Map json) = - _$PlaybackHistoryTrackImpl.fromJson; - - @override - DateTime get date; - Track get track; - @override - @JsonKey(ignore: true) - _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> - get copyWith => throw _privateConstructorUsedError; -} diff --git a/lib/provider/history/state.g.dart b/lib/provider/history/state.g.dart deleted file mode 100644 index dfd01c2c..00000000 --- a/lib/provider/history/state.g.dart +++ /dev/null @@ -1,55 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'state.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$PlaybackHistoryPlaylistImpl _$$PlaybackHistoryPlaylistImplFromJson( - Map json) => - _$PlaybackHistoryPlaylistImpl( - date: DateTime.parse(json['date'] as String), - playlist: PlaylistSimple.fromJson( - Map.from(json['playlist'] as Map)), - $type: json['runtimeType'] as String?, - ); - -Map _$$PlaybackHistoryPlaylistImplToJson( - _$PlaybackHistoryPlaylistImpl instance) => - { - 'date': instance.date.toIso8601String(), - 'playlist': instance.playlist.toJson(), - 'runtimeType': instance.$type, - }; - -_$PlaybackHistoryAlbumImpl _$$PlaybackHistoryAlbumImplFromJson(Map json) => - _$PlaybackHistoryAlbumImpl( - date: DateTime.parse(json['date'] as String), - album: - AlbumSimple.fromJson(Map.from(json['album'] as Map)), - $type: json['runtimeType'] as String?, - ); - -Map _$$PlaybackHistoryAlbumImplToJson( - _$PlaybackHistoryAlbumImpl instance) => - { - 'date': instance.date.toIso8601String(), - 'album': instance.album.toJson(), - 'runtimeType': instance.$type, - }; - -_$PlaybackHistoryTrackImpl _$$PlaybackHistoryTrackImplFromJson(Map json) => - _$PlaybackHistoryTrackImpl( - date: DateTime.parse(json['date'] as String), - track: Track.fromJson(Map.from(json['track'] as Map)), - $type: json['runtimeType'] as String?, - ); - -Map _$$PlaybackHistoryTrackImplToJson( - _$PlaybackHistoryTrackImpl instance) => - { - 'date': instance.date.toIso8601String(), - 'track': instance.track.toJson(), - 'runtimeType': instance.$type, - }; diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index aa12c9b3..965fb3ad 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -6,7 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/database/database.dart'; -import 'package:spotube/provider/history/state.dart'; + +enum HistoryDuration { + allTime, + days7, + days30, + months6, + year, + years2, +} final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); diff --git a/lib/services/kv_store/encrypted_kv_store.dart b/lib/services/kv_store/encrypted_kv_store.dart index ab4a750e..4eca0007 100644 --- a/lib/services/kv_store/encrypted_kv_store.dart +++ b/lib/services/kv_store/encrypted_kv_store.dart @@ -10,6 +10,8 @@ abstract class EncryptedKvStoreService { ), ); + static FlutterSecureStorage get storage => _storage; + static String? _encryptionKeySync; static Future initialize() async { diff --git a/lib/services/kv_store/kv_store.dart b/lib/services/kv_store/kv_store.dart index 2707ea4d..efe83abf 100644 --- a/lib/services/kv_store/kv_store.dart +++ b/lib/services/kv_store/kv_store.dart @@ -82,4 +82,9 @@ abstract class KVStoreService { static double get volume => sharedPreferences.getDouble('volume') ?? 1.0; static Future setVolume(double value) async => await sharedPreferences.setDouble('volume', value); + + static bool get hasMigratedToDrift => + sharedPreferences.getBool('hasMigratedToDrift') ?? false; + static Future setHasMigratedToDrift(bool value) async => + await sharedPreferences.setBool('hasMigratedToDrift', value); } diff --git a/lib/utils/migrations/adapters.dart b/lib/utils/migrations/adapters.dart new file mode 100644 index 00000000..f7f6350b --- /dev/null +++ b/lib/utils/migrations/adapters.dart @@ -0,0 +1,320 @@ +import 'package:hive/hive.dart'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; + +part 'adapters.g.dart'; +part 'adapters.freezed.dart'; + +@HiveType(typeId: 2) +class SkipSegment { + @HiveField(0) + final int start; + @HiveField(1) + final int end; + SkipSegment(this.start, this.end); + + static String version = 'v1'; + static final boxName = "oss.krtirtho.spotube.skip_segments.$version"; + static LazyBox get box => Hive.lazyBox(boxName); + + SkipSegment.fromJson(Map json) + : start = json['start'], + end = json['end']; + + Map toJson() => { + 'start': start, + 'end': end, + }; +} + +@JsonEnum() +@HiveType(typeId: 5) +enum SourceType { + @HiveField(0) + youtube._("YouTube"), + + @HiveField(1) + youtubeMusic._("YouTube Music"), + + @HiveField(2) + jiosaavn._("JioSaavn"); + + final String label; + + const SourceType._(this.label); +} + +@JsonSerializable() +@HiveType(typeId: 6) +class SourceMatch { + @HiveField(0) + String id; + + @HiveField(1) + String sourceId; + + @HiveField(2) + SourceType sourceType; + + @HiveField(3) + DateTime createdAt; + + SourceMatch({ + required this.id, + required this.sourceId, + required this.sourceType, + required this.createdAt, + }); + + factory SourceMatch.fromJson(Map json) => + _$SourceMatchFromJson(json); + + Map toJson() => _$SourceMatchToJson(this); + + static String version = 'v1'; + static final boxName = "oss.krtirtho.spotube.source_matches.$version"; + + static LazyBox get box => Hive.lazyBox(boxName); +} + +@JsonSerializable() +class AuthenticationCredentials { + String cookie; + String accessToken; + DateTime expiration; + + AuthenticationCredentials({ + required this.cookie, + required this.accessToken, + required this.expiration, + }); + + factory AuthenticationCredentials.fromJson(Map json) { + return AuthenticationCredentials( + cookie: json['cookie'] as String, + accessToken: json['accessToken'] as String, + expiration: DateTime.parse(json['expiration'] as String), + ); + } + + Map toJson() { + return { + 'cookie': cookie, + 'accessToken': accessToken, + 'expiration': expiration.toIso8601String(), + }; + } +} + +@JsonEnum() +enum LayoutMode { + compact, + extended, + adaptive, +} + +@JsonEnum() +enum CloseBehavior { + minimizeToTray, + close, +} + +@JsonEnum() +enum AudioSource { + youtube, + piped, + jiosaavn; + + String get label => name[0].toUpperCase() + name.substring(1); +} + +@JsonEnum() +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); +} + +@JsonEnum() +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); + } +} + +@freezed +class UserPreferences with _$UserPreferences { + const factory UserPreferences({ + @Default(SourceQualities.high) SourceQualities audioQuality, + @Default(true) bool albumColorSync, + @Default(false) bool amoledDarkTheme, + @Default(true) bool checkUpdate, + @Default(false) bool normalizeAudio, + @Default(false) bool showSystemTrayIcon, + @Default(false) bool skipNonMusic, + @Default(false) bool systemTitleBar, + @Default(CloseBehavior.close) CloseBehavior closeBehavior, + @Default(SpotubeColor(0xFF2196F3, name: "Blue")) + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue, + ) + SpotubeColor accentColorScheme, + @Default(LayoutMode.adaptive) LayoutMode layoutMode, + @Default(Locale("system", "system")) + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue, + ) + Locale locale, + @Default(Market.US) Market recommendationMarket, + @Default(SearchMode.youtube) SearchMode searchMode, + @Default("") String downloadLocation, + @Default([]) List localLibraryLocation, + @Default("https://pipedapi.kavin.rocks") String pipedInstance, + @Default(ThemeMode.system) ThemeMode themeMode, + @Default(AudioSource.youtube) AudioSource audioSource, + @Default(SourceCodecs.weba) SourceCodecs streamMusicCodec, + @Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec, + @Default(true) bool discordPresence, + @Default(true) bool endlessPlayback, + @Default(false) bool enableConnect, + }) = _UserPreferences; + factory UserPreferences.fromJson(Map json) => + _$UserPreferencesFromJson(json); + + factory UserPreferences.withDefaults() => UserPreferences.fromJson({}); + + static SpotubeColor _accentColorSchemeFromJson(Map json) { + return SpotubeColor.fromString(json["color"]); + } + + static Map? _accentColorSchemeReadValue( + Map json, String key) { + if (json[key] is String) { + return {"color": json[key]}; + } + + return json[key] as Map?; + } + + static Map _accentColorSchemeToJson(SpotubeColor color) { + return {"color": color.toString()}; + } + + static Locale _localeFromJson(Map json) { + return Locale(json["languageCode"], json["countryCode"]); + } + + static Map _localeToJson(Locale locale) { + return { + "languageCode": locale.languageCode, + "countryCode": locale.countryCode, + }; + } + + static Map? _localeReadValue( + Map json, String key) { + if (json[key] is String) { + final map = jsonDecode(json[key]); + return { + "languageCode": map["lc"], + "countryCode": map["cc"], + }; + } + + return json[key] as Map?; + } +} + +enum BlacklistedType { + artist, + track; + + static BlacklistedType fromName(String name) => + BlacklistedType.values.firstWhere((e) => e.name == name); +} + +class BlacklistedElement { + final String id; + final String name; + final BlacklistedType type; + + BlacklistedElement.fromJson(Map json) + : id = json['id'], + name = json['name'], + type = BlacklistedType.fromName(json['type']); + + Map toJson() => {'id': id, 'type': type.name, 'name': name}; +} + +@freezed +class PlaybackHistoryItem with _$PlaybackHistoryItem { + factory PlaybackHistoryItem.playlist({ + required DateTime date, + required PlaylistSimple playlist, + }) = PlaybackHistoryPlaylist; + + factory PlaybackHistoryItem.album({ + required DateTime date, + required AlbumSimple album, + }) = PlaybackHistoryAlbum; + + factory PlaybackHistoryItem.track({ + required DateTime date, + required Track track, + }) = PlaybackHistoryTrack; + + factory PlaybackHistoryItem.fromJson(Map json) => + _$PlaybackHistoryItemFromJson(json); +} + +class PlaybackHistoryState { + final List items; + const PlaybackHistoryState({this.items = const []}); + + factory PlaybackHistoryState.fromJson(Map json) { + return PlaybackHistoryState( + items: json["items"] + ?.map( + (json) => PlaybackHistoryItem.fromJson(json), + ) + .toList() + .cast() ?? + [], + ); + } +} + +class ScrobblerState { + final String username; + final String passwordHash; + + ScrobblerState({ + required this.username, + required this.passwordHash, + }); + + factory ScrobblerState.fromJson(Map json) { + return ScrobblerState( + username: json["username"], + passwordHash: json["passwordHash"], + ); + } +} diff --git a/lib/utils/migrations/adapters.freezed.dart b/lib/utils/migrations/adapters.freezed.dart new file mode 100644 index 00000000..339ec0e5 --- /dev/null +++ b/lib/utils/migrations/adapters.freezed.dart @@ -0,0 +1,1380 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'adapters.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +UserPreferences _$UserPreferencesFromJson(Map json) { + return _UserPreferences.fromJson(json); +} + +/// @nodoc +mixin _$UserPreferences { + SourceQualities get audioQuality => throw _privateConstructorUsedError; + bool get albumColorSync => throw _privateConstructorUsedError; + bool get amoledDarkTheme => throw _privateConstructorUsedError; + bool get checkUpdate => throw _privateConstructorUsedError; + bool get normalizeAudio => throw _privateConstructorUsedError; + bool get showSystemTrayIcon => throw _privateConstructorUsedError; + bool get skipNonMusic => throw _privateConstructorUsedError; + bool get systemTitleBar => throw _privateConstructorUsedError; + CloseBehavior get closeBehavior => throw _privateConstructorUsedError; + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor get accentColorScheme => throw _privateConstructorUsedError; + LayoutMode get layoutMode => throw _privateConstructorUsedError; + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale get locale => throw _privateConstructorUsedError; + Market get recommendationMarket => throw _privateConstructorUsedError; + SearchMode get searchMode => throw _privateConstructorUsedError; + String get downloadLocation => throw _privateConstructorUsedError; + List get localLibraryLocation => throw _privateConstructorUsedError; + String get pipedInstance => throw _privateConstructorUsedError; + ThemeMode get themeMode => throw _privateConstructorUsedError; + AudioSource get audioSource => throw _privateConstructorUsedError; + SourceCodecs get streamMusicCodec => throw _privateConstructorUsedError; + SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError; + bool get discordPresence => throw _privateConstructorUsedError; + bool get endlessPlayback => throw _privateConstructorUsedError; + bool get enableConnect => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $UserPreferencesCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserPreferencesCopyWith<$Res> { + factory $UserPreferencesCopyWith( + UserPreferences value, $Res Function(UserPreferences) then) = + _$UserPreferencesCopyWithImpl<$Res, UserPreferences>; + @useResult + $Res call( + {SourceQualities audioQuality, + bool albumColorSync, + bool amoledDarkTheme, + bool checkUpdate, + bool normalizeAudio, + bool showSystemTrayIcon, + bool skipNonMusic, + bool systemTitleBar, + CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor accentColorScheme, + LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale locale, + Market recommendationMarket, + SearchMode searchMode, + String downloadLocation, + List localLibraryLocation, + String pipedInstance, + ThemeMode themeMode, + AudioSource audioSource, + SourceCodecs streamMusicCodec, + SourceCodecs downloadMusicCodec, + bool discordPresence, + bool endlessPlayback, + bool enableConnect}); +} + +/// @nodoc +class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> + implements $UserPreferencesCopyWith<$Res> { + _$UserPreferencesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? audioQuality = null, + Object? albumColorSync = null, + Object? amoledDarkTheme = null, + Object? checkUpdate = null, + Object? normalizeAudio = null, + Object? showSystemTrayIcon = null, + Object? skipNonMusic = null, + Object? systemTitleBar = null, + Object? closeBehavior = null, + Object? accentColorScheme = null, + Object? layoutMode = null, + Object? locale = null, + Object? recommendationMarket = null, + Object? searchMode = null, + Object? downloadLocation = null, + Object? localLibraryLocation = null, + Object? pipedInstance = null, + Object? themeMode = null, + Object? audioSource = null, + Object? streamMusicCodec = null, + Object? downloadMusicCodec = null, + Object? discordPresence = null, + Object? endlessPlayback = null, + Object? enableConnect = null, + }) { + return _then(_value.copyWith( + audioQuality: null == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as SourceQualities, + albumColorSync: null == albumColorSync + ? _value.albumColorSync + : albumColorSync // ignore: cast_nullable_to_non_nullable + as bool, + amoledDarkTheme: null == amoledDarkTheme + ? _value.amoledDarkTheme + : amoledDarkTheme // ignore: cast_nullable_to_non_nullable + as bool, + checkUpdate: null == checkUpdate + ? _value.checkUpdate + : checkUpdate // ignore: cast_nullable_to_non_nullable + as bool, + normalizeAudio: null == normalizeAudio + ? _value.normalizeAudio + : normalizeAudio // ignore: cast_nullable_to_non_nullable + as bool, + showSystemTrayIcon: null == showSystemTrayIcon + ? _value.showSystemTrayIcon + : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable + as bool, + skipNonMusic: null == skipNonMusic + ? _value.skipNonMusic + : skipNonMusic // ignore: cast_nullable_to_non_nullable + as bool, + systemTitleBar: null == systemTitleBar + ? _value.systemTitleBar + : systemTitleBar // ignore: cast_nullable_to_non_nullable + as bool, + closeBehavior: null == closeBehavior + ? _value.closeBehavior + : closeBehavior // ignore: cast_nullable_to_non_nullable + as CloseBehavior, + accentColorScheme: null == accentColorScheme + ? _value.accentColorScheme + : accentColorScheme // ignore: cast_nullable_to_non_nullable + as SpotubeColor, + layoutMode: null == layoutMode + ? _value.layoutMode + : layoutMode // ignore: cast_nullable_to_non_nullable + as LayoutMode, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + recommendationMarket: null == recommendationMarket + ? _value.recommendationMarket + : recommendationMarket // ignore: cast_nullable_to_non_nullable + as Market, + searchMode: null == searchMode + ? _value.searchMode + : searchMode // ignore: cast_nullable_to_non_nullable + as SearchMode, + downloadLocation: null == downloadLocation + ? _value.downloadLocation + : downloadLocation // ignore: cast_nullable_to_non_nullable + as String, + localLibraryLocation: null == localLibraryLocation + ? _value.localLibraryLocation + : localLibraryLocation // ignore: cast_nullable_to_non_nullable + as List, + pipedInstance: null == pipedInstance + ? _value.pipedInstance + : pipedInstance // ignore: cast_nullable_to_non_nullable + as String, + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + audioSource: null == audioSource + ? _value.audioSource + : audioSource // ignore: cast_nullable_to_non_nullable + as AudioSource, + streamMusicCodec: null == streamMusicCodec + ? _value.streamMusicCodec + : streamMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + downloadMusicCodec: null == downloadMusicCodec + ? _value.downloadMusicCodec + : downloadMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + discordPresence: null == discordPresence + ? _value.discordPresence + : discordPresence // ignore: cast_nullable_to_non_nullable + as bool, + endlessPlayback: null == endlessPlayback + ? _value.endlessPlayback + : endlessPlayback // ignore: cast_nullable_to_non_nullable + as bool, + enableConnect: null == enableConnect + ? _value.enableConnect + : enableConnect // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserPreferencesImplCopyWith<$Res> + implements $UserPreferencesCopyWith<$Res> { + factory _$$UserPreferencesImplCopyWith(_$UserPreferencesImpl value, + $Res Function(_$UserPreferencesImpl) then) = + __$$UserPreferencesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SourceQualities audioQuality, + bool albumColorSync, + bool amoledDarkTheme, + bool checkUpdate, + bool normalizeAudio, + bool showSystemTrayIcon, + bool skipNonMusic, + bool systemTitleBar, + CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor accentColorScheme, + LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale locale, + Market recommendationMarket, + SearchMode searchMode, + String downloadLocation, + List localLibraryLocation, + String pipedInstance, + ThemeMode themeMode, + AudioSource audioSource, + SourceCodecs streamMusicCodec, + SourceCodecs downloadMusicCodec, + bool discordPresence, + bool endlessPlayback, + bool enableConnect}); +} + +/// @nodoc +class __$$UserPreferencesImplCopyWithImpl<$Res> + extends _$UserPreferencesCopyWithImpl<$Res, _$UserPreferencesImpl> + implements _$$UserPreferencesImplCopyWith<$Res> { + __$$UserPreferencesImplCopyWithImpl( + _$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? audioQuality = null, + Object? albumColorSync = null, + Object? amoledDarkTheme = null, + Object? checkUpdate = null, + Object? normalizeAudio = null, + Object? showSystemTrayIcon = null, + Object? skipNonMusic = null, + Object? systemTitleBar = null, + Object? closeBehavior = null, + Object? accentColorScheme = null, + Object? layoutMode = null, + Object? locale = null, + Object? recommendationMarket = null, + Object? searchMode = null, + Object? downloadLocation = null, + Object? localLibraryLocation = null, + Object? pipedInstance = null, + Object? themeMode = null, + Object? audioSource = null, + Object? streamMusicCodec = null, + Object? downloadMusicCodec = null, + Object? discordPresence = null, + Object? endlessPlayback = null, + Object? enableConnect = null, + }) { + return _then(_$UserPreferencesImpl( + audioQuality: null == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as SourceQualities, + albumColorSync: null == albumColorSync + ? _value.albumColorSync + : albumColorSync // ignore: cast_nullable_to_non_nullable + as bool, + amoledDarkTheme: null == amoledDarkTheme + ? _value.amoledDarkTheme + : amoledDarkTheme // ignore: cast_nullable_to_non_nullable + as bool, + checkUpdate: null == checkUpdate + ? _value.checkUpdate + : checkUpdate // ignore: cast_nullable_to_non_nullable + as bool, + normalizeAudio: null == normalizeAudio + ? _value.normalizeAudio + : normalizeAudio // ignore: cast_nullable_to_non_nullable + as bool, + showSystemTrayIcon: null == showSystemTrayIcon + ? _value.showSystemTrayIcon + : showSystemTrayIcon // ignore: cast_nullable_to_non_nullable + as bool, + skipNonMusic: null == skipNonMusic + ? _value.skipNonMusic + : skipNonMusic // ignore: cast_nullable_to_non_nullable + as bool, + systemTitleBar: null == systemTitleBar + ? _value.systemTitleBar + : systemTitleBar // ignore: cast_nullable_to_non_nullable + as bool, + closeBehavior: null == closeBehavior + ? _value.closeBehavior + : closeBehavior // ignore: cast_nullable_to_non_nullable + as CloseBehavior, + accentColorScheme: null == accentColorScheme + ? _value.accentColorScheme + : accentColorScheme // ignore: cast_nullable_to_non_nullable + as SpotubeColor, + layoutMode: null == layoutMode + ? _value.layoutMode + : layoutMode // ignore: cast_nullable_to_non_nullable + as LayoutMode, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + recommendationMarket: null == recommendationMarket + ? _value.recommendationMarket + : recommendationMarket // ignore: cast_nullable_to_non_nullable + as Market, + searchMode: null == searchMode + ? _value.searchMode + : searchMode // ignore: cast_nullable_to_non_nullable + as SearchMode, + downloadLocation: null == downloadLocation + ? _value.downloadLocation + : downloadLocation // ignore: cast_nullable_to_non_nullable + as String, + localLibraryLocation: null == localLibraryLocation + ? _value._localLibraryLocation + : localLibraryLocation // ignore: cast_nullable_to_non_nullable + as List, + pipedInstance: null == pipedInstance + ? _value.pipedInstance + : pipedInstance // ignore: cast_nullable_to_non_nullable + as String, + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + audioSource: null == audioSource + ? _value.audioSource + : audioSource // ignore: cast_nullable_to_non_nullable + as AudioSource, + streamMusicCodec: null == streamMusicCodec + ? _value.streamMusicCodec + : streamMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + downloadMusicCodec: null == downloadMusicCodec + ? _value.downloadMusicCodec + : downloadMusicCodec // ignore: cast_nullable_to_non_nullable + as SourceCodecs, + discordPresence: null == discordPresence + ? _value.discordPresence + : discordPresence // ignore: cast_nullable_to_non_nullable + as bool, + endlessPlayback: null == endlessPlayback + ? _value.endlessPlayback + : endlessPlayback // ignore: cast_nullable_to_non_nullable + as bool, + enableConnect: null == enableConnect + ? _value.enableConnect + : enableConnect // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserPreferencesImpl implements _UserPreferences { + const _$UserPreferencesImpl( + {this.audioQuality = SourceQualities.high, + this.albumColorSync = true, + this.amoledDarkTheme = false, + this.checkUpdate = true, + this.normalizeAudio = false, + this.showSystemTrayIcon = false, + this.skipNonMusic = false, + this.systemTitleBar = false, + this.closeBehavior = CloseBehavior.close, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + this.accentColorScheme = const SpotubeColor(0xFF2196F3, name: "Blue"), + this.layoutMode = LayoutMode.adaptive, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + this.locale = const Locale("system", "system"), + this.recommendationMarket = Market.US, + this.searchMode = SearchMode.youtube, + this.downloadLocation = "", + final List localLibraryLocation = const [], + this.pipedInstance = "https://pipedapi.kavin.rocks", + this.themeMode = ThemeMode.system, + this.audioSource = AudioSource.youtube, + this.streamMusicCodec = SourceCodecs.weba, + this.downloadMusicCodec = SourceCodecs.m4a, + this.discordPresence = true, + this.endlessPlayback = true, + this.enableConnect = false}) + : _localLibraryLocation = localLibraryLocation; + + factory _$UserPreferencesImpl.fromJson(Map json) => + _$$UserPreferencesImplFromJson(json); + + @override + @JsonKey() + final SourceQualities audioQuality; + @override + @JsonKey() + final bool albumColorSync; + @override + @JsonKey() + final bool amoledDarkTheme; + @override + @JsonKey() + final bool checkUpdate; + @override + @JsonKey() + final bool normalizeAudio; + @override + @JsonKey() + final bool showSystemTrayIcon; + @override + @JsonKey() + final bool skipNonMusic; + @override + @JsonKey() + final bool systemTitleBar; + @override + @JsonKey() + final CloseBehavior closeBehavior; + @override + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + final SpotubeColor accentColorScheme; + @override + @JsonKey() + final LayoutMode layoutMode; + @override + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + final Locale locale; + @override + @JsonKey() + final Market recommendationMarket; + @override + @JsonKey() + final SearchMode searchMode; + @override + @JsonKey() + final String downloadLocation; + final List _localLibraryLocation; + @override + @JsonKey() + List get localLibraryLocation { + if (_localLibraryLocation is EqualUnmodifiableListView) + return _localLibraryLocation; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_localLibraryLocation); + } + + @override + @JsonKey() + final String pipedInstance; + @override + @JsonKey() + final ThemeMode themeMode; + @override + @JsonKey() + final AudioSource audioSource; + @override + @JsonKey() + final SourceCodecs streamMusicCodec; + @override + @JsonKey() + final SourceCodecs downloadMusicCodec; + @override + @JsonKey() + final bool discordPresence; + @override + @JsonKey() + final bool endlessPlayback; + @override + @JsonKey() + final bool enableConnect; + + @override + String toString() { + return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, localLibraryLocation: $localLibraryLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserPreferencesImpl && + (identical(other.audioQuality, audioQuality) || + other.audioQuality == audioQuality) && + (identical(other.albumColorSync, albumColorSync) || + other.albumColorSync == albumColorSync) && + (identical(other.amoledDarkTheme, amoledDarkTheme) || + other.amoledDarkTheme == amoledDarkTheme) && + (identical(other.checkUpdate, checkUpdate) || + other.checkUpdate == checkUpdate) && + (identical(other.normalizeAudio, normalizeAudio) || + other.normalizeAudio == normalizeAudio) && + (identical(other.showSystemTrayIcon, showSystemTrayIcon) || + other.showSystemTrayIcon == showSystemTrayIcon) && + (identical(other.skipNonMusic, skipNonMusic) || + other.skipNonMusic == skipNonMusic) && + (identical(other.systemTitleBar, systemTitleBar) || + other.systemTitleBar == systemTitleBar) && + (identical(other.closeBehavior, closeBehavior) || + other.closeBehavior == closeBehavior) && + (identical(other.accentColorScheme, accentColorScheme) || + other.accentColorScheme == accentColorScheme) && + (identical(other.layoutMode, layoutMode) || + other.layoutMode == layoutMode) && + (identical(other.locale, locale) || other.locale == locale) && + (identical(other.recommendationMarket, recommendationMarket) || + other.recommendationMarket == recommendationMarket) && + (identical(other.searchMode, searchMode) || + other.searchMode == searchMode) && + (identical(other.downloadLocation, downloadLocation) || + other.downloadLocation == downloadLocation) && + const DeepCollectionEquality() + .equals(other._localLibraryLocation, _localLibraryLocation) && + (identical(other.pipedInstance, pipedInstance) || + other.pipedInstance == pipedInstance) && + (identical(other.themeMode, themeMode) || + other.themeMode == themeMode) && + (identical(other.audioSource, audioSource) || + other.audioSource == audioSource) && + (identical(other.streamMusicCodec, streamMusicCodec) || + other.streamMusicCodec == streamMusicCodec) && + (identical(other.downloadMusicCodec, downloadMusicCodec) || + other.downloadMusicCodec == downloadMusicCodec) && + (identical(other.discordPresence, discordPresence) || + other.discordPresence == discordPresence) && + (identical(other.endlessPlayback, endlessPlayback) || + other.endlessPlayback == endlessPlayback) && + (identical(other.enableConnect, enableConnect) || + other.enableConnect == enableConnect)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hashAll([ + runtimeType, + audioQuality, + albumColorSync, + amoledDarkTheme, + checkUpdate, + normalizeAudio, + showSystemTrayIcon, + skipNonMusic, + systemTitleBar, + closeBehavior, + accentColorScheme, + layoutMode, + locale, + recommendationMarket, + searchMode, + downloadLocation, + const DeepCollectionEquality().hash(_localLibraryLocation), + pipedInstance, + themeMode, + audioSource, + streamMusicCodec, + downloadMusicCodec, + discordPresence, + endlessPlayback, + enableConnect + ]); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => + __$$UserPreferencesImplCopyWithImpl<_$UserPreferencesImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$UserPreferencesImplToJson( + this, + ); + } +} + +abstract class _UserPreferences implements UserPreferences { + const factory _UserPreferences( + {final SourceQualities audioQuality, + final bool albumColorSync, + final bool amoledDarkTheme, + final bool checkUpdate, + final bool normalizeAudio, + final bool showSystemTrayIcon, + final bool skipNonMusic, + final bool systemTitleBar, + final CloseBehavior closeBehavior, + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + final SpotubeColor accentColorScheme, + final LayoutMode layoutMode, + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + final Locale locale, + final Market recommendationMarket, + final SearchMode searchMode, + final String downloadLocation, + final List localLibraryLocation, + final String pipedInstance, + final ThemeMode themeMode, + final AudioSource audioSource, + final SourceCodecs streamMusicCodec, + final SourceCodecs downloadMusicCodec, + final bool discordPresence, + final bool endlessPlayback, + final bool enableConnect}) = _$UserPreferencesImpl; + + factory _UserPreferences.fromJson(Map json) = + _$UserPreferencesImpl.fromJson; + + @override + SourceQualities get audioQuality; + @override + bool get albumColorSync; + @override + bool get amoledDarkTheme; + @override + bool get checkUpdate; + @override + bool get normalizeAudio; + @override + bool get showSystemTrayIcon; + @override + bool get skipNonMusic; + @override + bool get systemTitleBar; + @override + CloseBehavior get closeBehavior; + @override + @JsonKey( + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue) + SpotubeColor get accentColorScheme; + @override + LayoutMode get layoutMode; + @override + @JsonKey( + fromJson: UserPreferences._localeFromJson, + toJson: UserPreferences._localeToJson, + readValue: UserPreferences._localeReadValue) + Locale get locale; + @override + Market get recommendationMarket; + @override + SearchMode get searchMode; + @override + String get downloadLocation; + @override + List get localLibraryLocation; + @override + String get pipedInstance; + @override + ThemeMode get themeMode; + @override + AudioSource get audioSource; + @override + SourceCodecs get streamMusicCodec; + @override + SourceCodecs get downloadMusicCodec; + @override + bool get discordPresence; + @override + bool get endlessPlayback; + @override + bool get enableConnect; + @override + @JsonKey(ignore: true) + _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => + throw _privateConstructorUsedError; +} + +PlaybackHistoryItem _$PlaybackHistoryItemFromJson(Map json) { + switch (json['runtimeType']) { + case 'playlist': + return PlaybackHistoryPlaylist.fromJson(json); + case 'album': + return PlaybackHistoryAlbum.fromJson(json); + case 'track': + return PlaybackHistoryTrack.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'PlaybackHistoryItem', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$PlaybackHistoryItem { + DateTime get date => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PlaybackHistoryItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PlaybackHistoryItemCopyWith<$Res> { + factory $PlaybackHistoryItemCopyWith( + PlaybackHistoryItem value, $Res Function(PlaybackHistoryItem) then) = + _$PlaybackHistoryItemCopyWithImpl<$Res, PlaybackHistoryItem>; + @useResult + $Res call({DateTime date}); +} + +/// @nodoc +class _$PlaybackHistoryItemCopyWithImpl<$Res, $Val extends PlaybackHistoryItem> + implements $PlaybackHistoryItemCopyWith<$Res> { + _$PlaybackHistoryItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + }) { + return _then(_value.copyWith( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PlaybackHistoryPlaylistImplCopyWith<$Res> + implements $PlaybackHistoryItemCopyWith<$Res> { + factory _$$PlaybackHistoryPlaylistImplCopyWith( + _$PlaybackHistoryPlaylistImpl value, + $Res Function(_$PlaybackHistoryPlaylistImpl) then) = + __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime date, PlaylistSimple playlist}); +} + +/// @nodoc +class __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res> + extends _$PlaybackHistoryItemCopyWithImpl<$Res, + _$PlaybackHistoryPlaylistImpl> + implements _$$PlaybackHistoryPlaylistImplCopyWith<$Res> { + __$$PlaybackHistoryPlaylistImplCopyWithImpl( + _$PlaybackHistoryPlaylistImpl _value, + $Res Function(_$PlaybackHistoryPlaylistImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? playlist = null, + }) { + return _then(_$PlaybackHistoryPlaylistImpl( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + playlist: null == playlist + ? _value.playlist + : playlist // ignore: cast_nullable_to_non_nullable + as PlaylistSimple, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist { + _$PlaybackHistoryPlaylistImpl( + {required this.date, required this.playlist, final String? $type}) + : $type = $type ?? 'playlist'; + + factory _$PlaybackHistoryPlaylistImpl.fromJson(Map json) => + _$$PlaybackHistoryPlaylistImplFromJson(json); + + @override + final DateTime date; + @override + final PlaylistSimple playlist; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PlaybackHistoryItem.playlist(date: $date, playlist: $playlist)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlaybackHistoryPlaylistImpl && + (identical(other.date, date) || other.date == date) && + (identical(other.playlist, playlist) || + other.playlist == playlist)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, date, playlist); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> + get copyWith => __$$PlaybackHistoryPlaylistImplCopyWithImpl< + _$PlaybackHistoryPlaylistImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) { + return playlist(date, this.playlist); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) { + return playlist?.call(date, this.playlist); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) { + if (playlist != null) { + return playlist(date, this.playlist); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) { + return playlist(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) { + return playlist?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) { + if (playlist != null) { + return playlist(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$PlaybackHistoryPlaylistImplToJson( + this, + ); + } +} + +abstract class PlaybackHistoryPlaylist implements PlaybackHistoryItem { + factory PlaybackHistoryPlaylist( + {required final DateTime date, + required final PlaylistSimple playlist}) = _$PlaybackHistoryPlaylistImpl; + + factory PlaybackHistoryPlaylist.fromJson(Map json) = + _$PlaybackHistoryPlaylistImpl.fromJson; + + @override + DateTime get date; + PlaylistSimple get playlist; + @override + @JsonKey(ignore: true) + _$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$PlaybackHistoryAlbumImplCopyWith<$Res> + implements $PlaybackHistoryItemCopyWith<$Res> { + factory _$$PlaybackHistoryAlbumImplCopyWith(_$PlaybackHistoryAlbumImpl value, + $Res Function(_$PlaybackHistoryAlbumImpl) then) = + __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime date, AlbumSimple album}); +} + +/// @nodoc +class __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res> + extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryAlbumImpl> + implements _$$PlaybackHistoryAlbumImplCopyWith<$Res> { + __$$PlaybackHistoryAlbumImplCopyWithImpl(_$PlaybackHistoryAlbumImpl _value, + $Res Function(_$PlaybackHistoryAlbumImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? album = null, + }) { + return _then(_$PlaybackHistoryAlbumImpl( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + album: null == album + ? _value.album + : album // ignore: cast_nullable_to_non_nullable + as AlbumSimple, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum { + _$PlaybackHistoryAlbumImpl( + {required this.date, required this.album, final String? $type}) + : $type = $type ?? 'album'; + + factory _$PlaybackHistoryAlbumImpl.fromJson(Map json) => + _$$PlaybackHistoryAlbumImplFromJson(json); + + @override + final DateTime date; + @override + final AlbumSimple album; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PlaybackHistoryItem.album(date: $date, album: $album)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlaybackHistoryAlbumImpl && + (identical(other.date, date) || other.date == date) && + (identical(other.album, album) || other.album == album)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, date, album); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> + get copyWith => + __$$PlaybackHistoryAlbumImplCopyWithImpl<_$PlaybackHistoryAlbumImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) { + return album(date, this.album); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) { + return album?.call(date, this.album); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) { + if (album != null) { + return album(date, this.album); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) { + return album(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) { + return album?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) { + if (album != null) { + return album(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$PlaybackHistoryAlbumImplToJson( + this, + ); + } +} + +abstract class PlaybackHistoryAlbum implements PlaybackHistoryItem { + factory PlaybackHistoryAlbum( + {required final DateTime date, + required final AlbumSimple album}) = _$PlaybackHistoryAlbumImpl; + + factory PlaybackHistoryAlbum.fromJson(Map json) = + _$PlaybackHistoryAlbumImpl.fromJson; + + @override + DateTime get date; + AlbumSimple get album; + @override + @JsonKey(ignore: true) + _$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$PlaybackHistoryTrackImplCopyWith<$Res> + implements $PlaybackHistoryItemCopyWith<$Res> { + factory _$$PlaybackHistoryTrackImplCopyWith(_$PlaybackHistoryTrackImpl value, + $Res Function(_$PlaybackHistoryTrackImpl) then) = + __$$PlaybackHistoryTrackImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime date, Track track}); +} + +/// @nodoc +class __$$PlaybackHistoryTrackImplCopyWithImpl<$Res> + extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryTrackImpl> + implements _$$PlaybackHistoryTrackImplCopyWith<$Res> { + __$$PlaybackHistoryTrackImplCopyWithImpl(_$PlaybackHistoryTrackImpl _value, + $Res Function(_$PlaybackHistoryTrackImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? date = null, + Object? track = null, + }) { + return _then(_$PlaybackHistoryTrackImpl( + date: null == date + ? _value.date + : date // ignore: cast_nullable_to_non_nullable + as DateTime, + track: null == track + ? _value.track + : track // ignore: cast_nullable_to_non_nullable + as Track, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack { + _$PlaybackHistoryTrackImpl( + {required this.date, required this.track, final String? $type}) + : $type = $type ?? 'track'; + + factory _$PlaybackHistoryTrackImpl.fromJson(Map json) => + _$$PlaybackHistoryTrackImplFromJson(json); + + @override + final DateTime date; + @override + final Track track; + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + String toString() { + return 'PlaybackHistoryItem.track(date: $date, track: $track)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PlaybackHistoryTrackImpl && + (identical(other.date, date) || other.date == date) && + (identical(other.track, track) || other.track == track)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, date, track); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> + get copyWith => + __$$PlaybackHistoryTrackImplCopyWithImpl<_$PlaybackHistoryTrackImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime date, PlaylistSimple playlist) playlist, + required TResult Function(DateTime date, AlbumSimple album) album, + required TResult Function(DateTime date, Track track) track, + }) { + return track(date, this.track); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult? Function(DateTime date, AlbumSimple album)? album, + TResult? Function(DateTime date, Track track)? track, + }) { + return track?.call(date, this.track); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime date, PlaylistSimple playlist)? playlist, + TResult Function(DateTime date, AlbumSimple album)? album, + TResult Function(DateTime date, Track track)? track, + required TResult orElse(), + }) { + if (track != null) { + return track(date, this.track); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(PlaybackHistoryPlaylist value) playlist, + required TResult Function(PlaybackHistoryAlbum value) album, + required TResult Function(PlaybackHistoryTrack value) track, + }) { + return track(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(PlaybackHistoryPlaylist value)? playlist, + TResult? Function(PlaybackHistoryAlbum value)? album, + TResult? Function(PlaybackHistoryTrack value)? track, + }) { + return track?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(PlaybackHistoryPlaylist value)? playlist, + TResult Function(PlaybackHistoryAlbum value)? album, + TResult Function(PlaybackHistoryTrack value)? track, + required TResult orElse(), + }) { + if (track != null) { + return track(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$PlaybackHistoryTrackImplToJson( + this, + ); + } +} + +abstract class PlaybackHistoryTrack implements PlaybackHistoryItem { + factory PlaybackHistoryTrack( + {required final DateTime date, + required final Track track}) = _$PlaybackHistoryTrackImpl; + + factory PlaybackHistoryTrack.fromJson(Map json) = + _$PlaybackHistoryTrackImpl.fromJson; + + @override + DateTime get date; + Track get track; + @override + @JsonKey(ignore: true) + _$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/utils/migrations/adapters.g.dart b/lib/utils/migrations/adapters.g.dart new file mode 100644 index 00000000..ca95a840 --- /dev/null +++ b/lib/utils/migrations/adapters.g.dart @@ -0,0 +1,600 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'adapters.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SkipSegmentAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + SkipSegment read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SkipSegment( + fields[0] as int, + fields[1] as int, + ); + } + + @override + void write(BinaryWriter writer, SkipSegment obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.start) + ..writeByte(1) + ..write(obj.end); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SkipSegmentAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SourceMatchAdapter extends TypeAdapter { + @override + final int typeId = 6; + + @override + SourceMatch read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SourceMatch( + id: fields[0] as String, + sourceId: fields[1] as String, + sourceType: fields[2] as SourceType, + createdAt: fields[3] as DateTime, + ); + } + + @override + void write(BinaryWriter writer, SourceMatch obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.sourceId) + ..writeByte(2) + ..write(obj.sourceType) + ..writeByte(3) + ..write(obj.createdAt); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SourceMatchAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SourceTypeAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + SourceType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return SourceType.youtube; + case 1: + return SourceType.youtubeMusic; + case 2: + return SourceType.jiosaavn; + default: + return SourceType.youtube; + } + } + + @override + void write(BinaryWriter writer, SourceType obj) { + switch (obj) { + case SourceType.youtube: + writer.writeByte(0); + break; + case SourceType.youtubeMusic: + writer.writeByte(1); + break; + case SourceType.jiosaavn: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SourceTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch( + id: json['id'] as String, + sourceId: json['sourceId'] as String, + sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']), + createdAt: DateTime.parse(json['createdAt'] as String), + ); + +Map _$SourceMatchToJson(SourceMatch instance) => + { + 'id': instance.id, + 'sourceId': instance.sourceId, + 'sourceType': _$SourceTypeEnumMap[instance.sourceType]!, + 'createdAt': instance.createdAt.toIso8601String(), + }; + +const _$SourceTypeEnumMap = { + SourceType.youtube: 'youtube', + SourceType.youtubeMusic: 'youtubeMusic', + SourceType.jiosaavn: 'jiosaavn', +}; + +AuthenticationCredentials _$AuthenticationCredentialsFromJson(Map json) => + AuthenticationCredentials( + cookie: json['cookie'] as String, + accessToken: json['accessToken'] as String, + expiration: DateTime.parse(json['expiration'] as String), + ); + +Map _$AuthenticationCredentialsToJson( + AuthenticationCredentials instance) => + { + 'cookie': instance.cookie, + 'accessToken': instance.accessToken, + 'expiration': instance.expiration.toIso8601String(), + }; + +_$UserPreferencesImpl _$$UserPreferencesImplFromJson(Map json) => + _$UserPreferencesImpl( + audioQuality: + $enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ?? + SourceQualities.high, + albumColorSync: json['albumColorSync'] as bool? ?? true, + amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, + checkUpdate: json['checkUpdate'] as bool? ?? true, + normalizeAudio: json['normalizeAudio'] as bool? ?? false, + showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? false, + skipNonMusic: json['skipNonMusic'] as bool? ?? false, + systemTitleBar: json['systemTitleBar'] as bool? ?? false, + closeBehavior: + $enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ?? + CloseBehavior.close, + accentColorScheme: UserPreferences._accentColorSchemeReadValue( + json, 'accentColorScheme') == + null + ? const SpotubeColor(0xFF2196F3, name: "Blue") + : UserPreferences._accentColorSchemeFromJson( + UserPreferences._accentColorSchemeReadValue( + json, 'accentColorScheme') as Map), + layoutMode: + $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode']) ?? + LayoutMode.adaptive, + locale: UserPreferences._localeReadValue(json, 'locale') == null + ? const Locale("system", "system") + : UserPreferences._localeFromJson( + UserPreferences._localeReadValue(json, 'locale') + as Map), + recommendationMarket: + $enumDecodeNullable(_$MarketEnumMap, json['recommendationMarket']) ?? + Market.US, + searchMode: + $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ?? + SearchMode.youtube, + downloadLocation: json['downloadLocation'] as String? ?? "", + localLibraryLocation: (json['localLibraryLocation'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + pipedInstance: + json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks", + themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? + ThemeMode.system, + audioSource: + $enumDecodeNullable(_$AudioSourceEnumMap, json['audioSource']) ?? + AudioSource.youtube, + streamMusicCodec: $enumDecodeNullable( + _$SourceCodecsEnumMap, json['streamMusicCodec']) ?? + SourceCodecs.weba, + downloadMusicCodec: $enumDecodeNullable( + _$SourceCodecsEnumMap, json['downloadMusicCodec']) ?? + SourceCodecs.m4a, + discordPresence: json['discordPresence'] as bool? ?? true, + endlessPlayback: json['endlessPlayback'] as bool? ?? true, + enableConnect: json['enableConnect'] as bool? ?? false, + ); + +Map _$$UserPreferencesImplToJson( + _$UserPreferencesImpl instance) => + { + 'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!, + 'albumColorSync': instance.albumColorSync, + 'amoledDarkTheme': instance.amoledDarkTheme, + 'checkUpdate': instance.checkUpdate, + 'normalizeAudio': instance.normalizeAudio, + 'showSystemTrayIcon': instance.showSystemTrayIcon, + 'skipNonMusic': instance.skipNonMusic, + 'systemTitleBar': instance.systemTitleBar, + 'closeBehavior': _$CloseBehaviorEnumMap[instance.closeBehavior]!, + 'accentColorScheme': + UserPreferences._accentColorSchemeToJson(instance.accentColorScheme), + 'layoutMode': _$LayoutModeEnumMap[instance.layoutMode]!, + 'locale': UserPreferences._localeToJson(instance.locale), + 'recommendationMarket': _$MarketEnumMap[instance.recommendationMarket]!, + 'searchMode': _$SearchModeEnumMap[instance.searchMode]!, + 'downloadLocation': instance.downloadLocation, + 'localLibraryLocation': instance.localLibraryLocation, + 'pipedInstance': instance.pipedInstance, + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'audioSource': _$AudioSourceEnumMap[instance.audioSource]!, + 'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!, + 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, + 'discordPresence': instance.discordPresence, + 'endlessPlayback': instance.endlessPlayback, + 'enableConnect': instance.enableConnect, + }; + +const _$SourceQualitiesEnumMap = { + SourceQualities.high: 'high', + SourceQualities.medium: 'medium', + SourceQualities.low: 'low', +}; + +const _$CloseBehaviorEnumMap = { + CloseBehavior.minimizeToTray: 'minimizeToTray', + CloseBehavior.close: 'close', +}; + +const _$LayoutModeEnumMap = { + LayoutMode.compact: 'compact', + LayoutMode.extended: 'extended', + LayoutMode.adaptive: 'adaptive', +}; + +const _$MarketEnumMap = { + Market.AD: 'AD', + Market.AE: 'AE', + Market.AF: 'AF', + Market.AG: 'AG', + Market.AI: 'AI', + Market.AL: 'AL', + Market.AM: 'AM', + Market.AO: 'AO', + Market.AQ: 'AQ', + Market.AR: 'AR', + Market.AS: 'AS', + Market.AT: 'AT', + Market.AU: 'AU', + Market.AW: 'AW', + Market.AX: 'AX', + Market.AZ: 'AZ', + Market.BA: 'BA', + Market.BB: 'BB', + Market.BD: 'BD', + Market.BE: 'BE', + Market.BF: 'BF', + Market.BG: 'BG', + Market.BH: 'BH', + Market.BI: 'BI', + Market.BJ: 'BJ', + Market.BL: 'BL', + Market.BM: 'BM', + Market.BN: 'BN', + Market.BO: 'BO', + Market.BQ: 'BQ', + Market.BR: 'BR', + Market.BS: 'BS', + Market.BT: 'BT', + Market.BV: 'BV', + Market.BW: 'BW', + Market.BY: 'BY', + Market.BZ: 'BZ', + Market.CA: 'CA', + Market.CC: 'CC', + Market.CD: 'CD', + Market.CF: 'CF', + Market.CG: 'CG', + Market.CH: 'CH', + Market.CI: 'CI', + Market.CK: 'CK', + Market.CL: 'CL', + Market.CM: 'CM', + Market.CN: 'CN', + Market.CO: 'CO', + Market.CR: 'CR', + Market.CU: 'CU', + Market.CV: 'CV', + Market.CW: 'CW', + Market.CX: 'CX', + Market.CY: 'CY', + Market.CZ: 'CZ', + Market.DE: 'DE', + Market.DJ: 'DJ', + Market.DK: 'DK', + Market.DM: 'DM', + Market.DO: 'DO', + Market.DZ: 'DZ', + Market.EC: 'EC', + Market.EE: 'EE', + Market.EG: 'EG', + Market.EH: 'EH', + Market.ER: 'ER', + Market.ES: 'ES', + Market.ET: 'ET', + Market.FI: 'FI', + Market.FJ: 'FJ', + Market.FK: 'FK', + Market.FM: 'FM', + Market.FO: 'FO', + Market.FR: 'FR', + Market.GA: 'GA', + Market.GB: 'GB', + Market.GD: 'GD', + Market.GE: 'GE', + Market.GF: 'GF', + Market.GG: 'GG', + Market.GH: 'GH', + Market.GI: 'GI', + Market.GL: 'GL', + Market.GM: 'GM', + Market.GN: 'GN', + Market.GP: 'GP', + Market.GQ: 'GQ', + Market.GR: 'GR', + Market.GS: 'GS', + Market.GT: 'GT', + Market.GU: 'GU', + Market.GW: 'GW', + Market.GY: 'GY', + Market.HK: 'HK', + Market.HM: 'HM', + Market.HN: 'HN', + Market.HR: 'HR', + Market.HT: 'HT', + Market.HU: 'HU', + Market.ID: 'ID', + Market.IE: 'IE', + Market.IL: 'IL', + Market.IM: 'IM', + Market.IN: 'IN', + Market.IO: 'IO', + Market.IQ: 'IQ', + Market.IR: 'IR', + Market.IS: 'IS', + Market.IT: 'IT', + Market.JE: 'JE', + Market.JM: 'JM', + Market.JO: 'JO', + Market.JP: 'JP', + Market.KE: 'KE', + Market.KG: 'KG', + Market.KH: 'KH', + Market.KI: 'KI', + Market.KM: 'KM', + Market.KN: 'KN', + Market.KP: 'KP', + Market.KR: 'KR', + Market.KW: 'KW', + Market.KY: 'KY', + Market.KZ: 'KZ', + Market.LA: 'LA', + Market.LB: 'LB', + Market.LC: 'LC', + Market.LI: 'LI', + Market.LK: 'LK', + Market.LR: 'LR', + Market.LS: 'LS', + Market.LT: 'LT', + Market.LU: 'LU', + Market.LV: 'LV', + Market.LY: 'LY', + Market.MA: 'MA', + Market.MC: 'MC', + Market.MD: 'MD', + Market.ME: 'ME', + Market.MF: 'MF', + Market.MG: 'MG', + Market.MH: 'MH', + Market.MK: 'MK', + Market.ML: 'ML', + Market.MM: 'MM', + Market.MN: 'MN', + Market.MO: 'MO', + Market.MP: 'MP', + Market.MQ: 'MQ', + Market.MR: 'MR', + Market.MS: 'MS', + Market.MT: 'MT', + Market.MU: 'MU', + Market.MV: 'MV', + Market.MW: 'MW', + Market.MX: 'MX', + Market.MY: 'MY', + Market.MZ: 'MZ', + Market.NA: 'NA', + Market.NC: 'NC', + Market.NE: 'NE', + Market.NF: 'NF', + Market.NG: 'NG', + Market.NI: 'NI', + Market.NL: 'NL', + Market.NO: 'NO', + Market.NP: 'NP', + Market.NR: 'NR', + Market.NU: 'NU', + Market.NZ: 'NZ', + Market.OM: 'OM', + Market.PA: 'PA', + Market.PE: 'PE', + Market.PF: 'PF', + Market.PG: 'PG', + Market.PH: 'PH', + Market.PK: 'PK', + Market.PL: 'PL', + Market.PM: 'PM', + Market.PN: 'PN', + Market.PR: 'PR', + Market.PS: 'PS', + Market.PT: 'PT', + Market.PW: 'PW', + Market.PY: 'PY', + Market.QA: 'QA', + Market.RE: 'RE', + Market.RO: 'RO', + Market.RS: 'RS', + Market.RU: 'RU', + Market.RW: 'RW', + Market.SA: 'SA', + Market.SB: 'SB', + Market.SC: 'SC', + Market.SD: 'SD', + Market.SE: 'SE', + Market.SG: 'SG', + Market.SH: 'SH', + Market.SI: 'SI', + Market.SJ: 'SJ', + Market.SK: 'SK', + Market.SL: 'SL', + Market.SM: 'SM', + Market.SN: 'SN', + Market.SO: 'SO', + Market.SR: 'SR', + Market.SS: 'SS', + Market.ST: 'ST', + Market.SV: 'SV', + Market.SX: 'SX', + Market.SY: 'SY', + Market.SZ: 'SZ', + Market.TC: 'TC', + Market.TD: 'TD', + Market.TF: 'TF', + Market.TG: 'TG', + Market.TH: 'TH', + Market.TJ: 'TJ', + Market.TK: 'TK', + Market.TL: 'TL', + Market.TM: 'TM', + Market.TN: 'TN', + Market.TO: 'TO', + Market.TR: 'TR', + Market.TT: 'TT', + Market.TV: 'TV', + Market.TW: 'TW', + Market.TZ: 'TZ', + Market.UA: 'UA', + Market.UG: 'UG', + Market.UM: 'UM', + Market.US: 'US', + Market.UY: 'UY', + Market.UZ: 'UZ', + Market.VA: 'VA', + Market.VC: 'VC', + Market.VE: 'VE', + Market.VG: 'VG', + Market.VI: 'VI', + Market.VN: 'VN', + Market.VU: 'VU', + Market.WF: 'WF', + Market.WS: 'WS', + Market.XK: 'XK', + Market.YE: 'YE', + Market.YT: 'YT', + Market.ZA: 'ZA', + Market.ZM: 'ZM', + Market.ZW: 'ZW', +}; + +const _$SearchModeEnumMap = { + SearchMode.youtube: 'youtube', + SearchMode.youtubeMusic: 'youtubeMusic', +}; + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + +const _$AudioSourceEnumMap = { + AudioSource.youtube: 'youtube', + AudioSource.piped: 'piped', + AudioSource.jiosaavn: 'jiosaavn', +}; + +const _$SourceCodecsEnumMap = { + SourceCodecs.m4a: 'm4a', + SourceCodecs.weba: 'weba', +}; + +_$PlaybackHistoryPlaylistImpl _$$PlaybackHistoryPlaylistImplFromJson( + Map json) => + _$PlaybackHistoryPlaylistImpl( + date: DateTime.parse(json['date'] as String), + playlist: PlaylistSimple.fromJson( + Map.from(json['playlist'] as Map)), + $type: json['runtimeType'] as String?, + ); + +Map _$$PlaybackHistoryPlaylistImplToJson( + _$PlaybackHistoryPlaylistImpl instance) => + { + 'date': instance.date.toIso8601String(), + 'playlist': instance.playlist.toJson(), + 'runtimeType': instance.$type, + }; + +_$PlaybackHistoryAlbumImpl _$$PlaybackHistoryAlbumImplFromJson(Map json) => + _$PlaybackHistoryAlbumImpl( + date: DateTime.parse(json['date'] as String), + album: + AlbumSimple.fromJson(Map.from(json['album'] as Map)), + $type: json['runtimeType'] as String?, + ); + +Map _$$PlaybackHistoryAlbumImplToJson( + _$PlaybackHistoryAlbumImpl instance) => + { + 'date': instance.date.toIso8601String(), + 'album': instance.album.toJson(), + 'runtimeType': instance.$type, + }; + +_$PlaybackHistoryTrackImpl _$$PlaybackHistoryTrackImplFromJson(Map json) => + _$PlaybackHistoryTrackImpl( + date: DateTime.parse(json['date'] as String), + track: Track.fromJson(Map.from(json['track'] as Map)), + $type: json['runtimeType'] as String?, + ); + +Map _$$PlaybackHistoryTrackImplToJson( + _$PlaybackHistoryTrackImpl instance) => + { + 'date': instance.date.toIso8601String(), + 'track': instance.track.toJson(), + 'runtimeType': instance.$type, + }; diff --git a/lib/utils/migrations/cache_box.dart b/lib/utils/migrations/cache_box.dart new file mode 100644 index 00000000..dfe1947b --- /dev/null +++ b/lib/utils/migrations/cache_box.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:hive/hive.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/provider/spotify/utils/json_cast.dart'; +import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:spotube/utils/primitive_utils.dart'; + +const kKeyBoxName = "spotube_box_name"; +const kNoEncryptionWarningShownKey = "showedNoEncryptionWarning"; +const kIsUsingEncryption = "isUsingEncryption"; +String getBoxKey(String boxName) => "spotube_box_$boxName"; + +class PersistenceCacheBox { + static late LazyBox _box; + static late LazyBox _encryptedBox; + + final String cacheKey; + final bool encrypted; + + final T Function(Map) fromJson; + + PersistenceCacheBox( + this.cacheKey, { + required this.fromJson, + this.encrypted = false, + }); + + 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 EncryptedKvStoreService.storage.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 EncryptedKvStoreService.storage.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 getData() async { + final json = await box.get(cacheKey); + + if (json != null || + (json is Map && json.entries.isNotEmpty) || + (json is List && json.isNotEmpty)) { + return fromJson(castNestedJson(json)); + } + + return null; + } +} diff --git a/lib/utils/migrations/hive.dart b/lib/utils/migrations/hive.dart new file mode 100644 index 00000000..e43df1d8 --- /dev/null +++ b/lib/utils/migrations/hive.dart @@ -0,0 +1,316 @@ +import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotube/models/database/database.dart' + hide + SourceType, + AudioSource, + CloseBehavior, + MusicCodec, + LayoutMode, + SearchMode, + BlacklistedType; +import 'package:spotube/models/database/database.dart' as db; +import 'package:spotube/services/kv_store/kv_store.dart'; +import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/utils/migrations/adapters.dart'; +import 'package:spotube/utils/migrations/cache_box.dart'; + +late AppDatabase _database; + +Future getHiveCacheDir() async => + kIsWeb ? null : (await getApplicationSupportDirectory()).path; + +Future migrateAuthenticationInfo() async { + AppLogger.log.i("🔵 Migrating authentication info.."); + + final box = PersistenceCacheBox( + "authentication", + encrypted: true, + fromJson: (json) => AuthenticationCredentials.fromJson(json), + ); + + final credentials = await box.getData(); + + if (credentials == null) return; + + await _database.into(_database.authenticationTable).insertOnConflictUpdate( + AuthenticationTableCompanion.insert( + accessToken: DecryptedText(credentials.accessToken), + cookie: DecryptedText(credentials.cookie), + expiration: credentials.expiration, + id: const Value(0), + ), + ); + + AppLogger.log.i("✅ Migrated authentication info"); +} + +Future migratePreferences() async { + AppLogger.log.i("🔵 Migrating preferences.."); + final box = PersistenceCacheBox( + "preferences", + fromJson: (json) => UserPreferences.fromJson(json), + ); + + final preferences = await box.getData(); + + if (preferences == null) return; + + await _database.into(_database.preferencesTable).insertOnConflictUpdate( + PreferencesTableCompanion.insert( + id: const Value(0), + accentColorScheme: Value(preferences.accentColorScheme), + albumColorSync: Value(preferences.albumColorSync), + amoledDarkTheme: Value(preferences.amoledDarkTheme), + audioQuality: Value(preferences.audioQuality), + audioSource: Value( + switch (preferences.audioSource) { + AudioSource.youtube => db.AudioSource.youtube, + AudioSource.piped => db.AudioSource.piped, + AudioSource.jiosaavn => db.AudioSource.jiosaavn, + }, + ), + checkUpdate: Value(preferences.checkUpdate), + closeBehavior: Value( + switch (preferences.closeBehavior) { + CloseBehavior.minimizeToTray => db.CloseBehavior.minimizeToTray, + CloseBehavior.close => db.CloseBehavior.close, + }, + ), + discordPresence: Value(preferences.discordPresence), + downloadLocation: Value(preferences.downloadLocation), + downloadMusicCodec: Value(preferences.downloadMusicCodec), + enableConnect: Value(preferences.enableConnect), + endlessPlayback: Value(preferences.endlessPlayback), + layoutMode: Value( + switch (preferences.layoutMode) { + LayoutMode.adaptive => db.LayoutMode.adaptive, + LayoutMode.compact => db.LayoutMode.compact, + LayoutMode.extended => db.LayoutMode.extended, + }, + ), + localLibraryLocation: Value(preferences.localLibraryLocation), + locale: Value(preferences.locale), + market: Value(preferences.recommendationMarket), + normalizeAudio: Value(preferences.normalizeAudio), + pipedInstance: Value(preferences.pipedInstance), + searchMode: Value( + switch (preferences.searchMode) { + SearchMode.youtube => db.SearchMode.youtube, + SearchMode.youtubeMusic => db.SearchMode.youtubeMusic, + }, + ), + showSystemTrayIcon: Value(preferences.showSystemTrayIcon), + skipNonMusic: Value(preferences.skipNonMusic), + streamMusicCodec: Value(preferences.streamMusicCodec), + systemTitleBar: Value(preferences.systemTitleBar), + themeMode: Value(preferences.themeMode), + ), + ); + + AppLogger.log.i("✅ Migrated preferences"); +} + +Future migrateSkipSegment() async { + AppLogger.log.i("🔵 Migrating skip segments.."); + Hive.registerAdapter(SkipSegmentAdapter()); + + final box = await Hive.openLazyBox( + SkipSegment.boxName, + path: await getHiveCacheDir(), + ); + + final skipSegments = await Future.wait( + box.keys.map( + (key) async => ( + id: key as String, + data: await box.get(key), + ), + ), + ); + + await _database.batch((batch) { + batch.insertAll( + _database.skipSegmentTable, + skipSegments + .where((element) => element.data != null) + .expand((element) => (element.data as List).map( + (segment) => SkipSegmentTableCompanion.insert( + trackId: element.id, + start: segment["start"], + end: segment["end"], + ), + )) + .toList(), + ); + }); + + AppLogger.log.i("✅ Migrated skip segments"); +} + +Future migrateSourceMatches() async { + AppLogger.log.i("🔵 Migrating source matches.."); + + Hive.registerAdapter(SourceMatchAdapter()); + Hive.registerAdapter(SourceTypeAdapter()); + + final box = await Hive.openBox( + SourceMatch.boxName, + path: await getHiveCacheDir(), + ); + + final sourceMatches = + box.keys.map((key) => (data: box.get(key), trackId: key)); + + await _database.batch((batch) { + batch.insertAll( + _database.sourceMatchTable, + sourceMatches + .where((element) => element.data != null) + .map( + (sourceMatch) => SourceMatchTableCompanion.insert( + sourceId: sourceMatch.data!.sourceId, + trackId: sourceMatch.trackId, + sourceType: Value( + switch (sourceMatch.data!.sourceType) { + SourceType.jiosaavn => db.SourceType.jiosaavn, + SourceType.youtube => db.SourceType.youtube, + SourceType.youtubeMusic => db.SourceType.youtubeMusic, + }, + ), + ), + ) + .toList(), + ); + }); + + AppLogger.log.i("✅ Migrated source matches"); +} + +Future migrateBlacklist() async { + AppLogger.log.i("🔵 Migrating blacklist.."); + + final box = PersistenceCacheBox>( + "blacklist", + fromJson: (json) => (json["blacklist"] as List) + .map((e) => BlacklistedElement.fromJson(e)) + .toSet(), + ); + + final data = await box.getData(); + + if (data == null) return; + + await _database.batch((batch) { + batch.insertAll( + _database.blacklistTable, + data.map( + (element) => BlacklistTableCompanion.insert( + name: element.name, + elementId: element.id, + elementType: switch (element.type) { + BlacklistedType.artist => db.BlacklistedType.artist, + BlacklistedType.track => db.BlacklistedType.track, + }, + ), + ), + ); + }); + + AppLogger.log.i("✅ Migrated blacklist"); +} + +Future migrateLastFmCredentials() async { + AppLogger.log.i("🔵 Migrating Last.fm credentials.."); + + final box = PersistenceCacheBox( + "scrobbler", + fromJson: (json) => ScrobblerState.fromJson(json), + encrypted: true, + ); + + final data = await box.getData(); + + if (data == null) return; + + await _database.into(_database.scrobblerTable).insertOnConflictUpdate( + ScrobblerTableCompanion.insert( + id: const Value(0), + passwordHash: DecryptedText(data.passwordHash), + username: data.username, + ), + ); + + AppLogger.log.i("✅ Migrated Last.fm credentials"); +} + +Future migratePlaybackHistory() async { + AppLogger.log.i("🔵 Migrating playback history.."); + + final box = PersistenceCacheBox( + "playback_history", + fromJson: (json) => PlaybackHistoryState.fromJson(json), + ); + + final data = await box.getData(); + + if (data == null) return; + + await _database.batch((batch) { + batch.insertAll( + _database.historyTable, + data.items.map( + (item) => switch (item) { + PlaybackHistoryAlbum() => HistoryTableCompanion.insert( + createdAt: Value(item.date), + itemId: item.album.id!, + data: item.album.toJson(), + type: db.HistoryEntryType.album, + ), + PlaybackHistoryPlaylist() => HistoryTableCompanion.insert( + createdAt: Value(item.date), + itemId: item.playlist.id!, + data: item.playlist.toJson(), + type: db.HistoryEntryType.playlist, + ), + PlaybackHistoryTrack() => HistoryTableCompanion.insert( + createdAt: Value(item.date), + itemId: item.track.id!, + data: item.track.toJson(), + type: db.HistoryEntryType.track, + ), + _ => throw Exception("Unknown history item type"), + }, + ), + ); + }); + + AppLogger.log.i("✅ Migrated playback history"); +} + +Future migrateFromHiveToDrift(AppDatabase database) async { + if (KVStoreService.hasMigratedToDrift) return; + + await PersistenceCacheBox.initializeBoxes( + path: await getHiveCacheDir(), + ); + + _database = database; + + await migrateAuthenticationInfo(); + await migratePreferences(); + + await migrateSkipSegment(); + await migrateSourceMatches(); + + await migrateBlacklist(); + await migratePlaybackHistory(); + + await migrateLastFmCredentials(); + + await KVStoreService.setHasMigratedToDrift(true); + + AppLogger.log.i("🚀 Migrated all data to Drift"); +} From 261e1b6685f207df1808af30044a63dbafbefb4c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 18:00:50 +0600 Subject: [PATCH 20/24] chore: fix queue collections not being loaded --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- lib/provider/audio_player/audio_player.dart | 6 +++ lib/provider/history/recent.dart | 45 ++++++++++++--------- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 64ee89d2..fed66850 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -53,7 +53,7 @@ body: description: Where did you install Spotube from? multiple: true options: - - "Website (spotube.netlify.app) or (spotube.krtirtho.dev)" + - "Website (spotube.krtirtho.dev)" - "GitHub Releases (Binary)" - "GitHub Actions (Nightly Binary)" - "Play Store (Android)" @@ -77,4 +77,4 @@ body: description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions! options: - label: I'm ready to work on this issue! - required: false \ No newline at end of file + required: false diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 9dfc2c0a..da22b2ce 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -89,6 +89,12 @@ class AudioPlayerNotifier extends Notifier { autoPlay: false, ); } + + if (playerState.collections.isNotEmpty) { + state = state.copyWith( + collections: playerState.collections, + ); + } } Future _updatePlayerState( diff --git a/lib/provider/history/recent.dart b/lib/provider/history/recent.dart index 4e445500..8894b713 100644 --- a/lib/provider/history/recent.dart +++ b/lib/provider/history/recent.dart @@ -9,28 +9,31 @@ class RecentlyPlayedItemNotifier extends AsyncNotifier> { build() async { final database = ref.watch(databaseProvider); - final uniqueItemIds = - await (database.selectOnly(database.historyTable, distinct: true) - ..addColumns([database.historyTable.itemId]) - ..where( - database.historyTable.type.isIn([ - HistoryEntryType.playlist.name, - HistoryEntryType.album.name, - ]), - ) - ..limit(10)) - .map((row) => row.read(database.historyTable.itemId)) - .get() - .then((value) => value.whereNotNull().toList()); + final uniqueItemIds = await (database.selectOnly(database.historyTable, + distinct: true) + ..addColumns([database.historyTable.itemId, database.historyTable.id]) + ..where( + database.historyTable.type.isIn([ + HistoryEntryType.playlist.name, + HistoryEntryType.album.name, + ]), + ) + ..limit(10) + ..orderBy([ + OrderingTerm( + expression: database.historyTable.createdAt, + mode: OrderingMode.desc, + ), + ])) + .map( + (row) => row.read(database.historyTable.id), + ) + .get() + .then((value) => value.whereNotNull().toList()); final query = database.select(database.historyTable) ..where( - (tbl) => - tbl.type.isIn([ - HistoryEntryType.playlist.name, - HistoryEntryType.album.name, - ]) & - tbl.itemId.isIn(uniqueItemIds), + (tbl) => tbl.id.isIn(uniqueItemIds), ) ..orderBy([ (tbl) => OrderingTerm( @@ -45,7 +48,9 @@ class RecentlyPlayedItemNotifier extends AsyncNotifier> { ref.onDispose(() => subscription.cancel()); - return await query.get(); + final items = await query.get(); + + return items; } } From 4c5564fd2f6bb7e6f42896a01e95fe197c362b50 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 18:31:57 +0600 Subject: [PATCH 21/24] chore: use enum properties for history duration in top stats --- lib/provider/history/top.dart | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index 965fb3ad..82681267 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -8,20 +8,24 @@ import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/database/database.dart'; enum HistoryDuration { - allTime, - days7, - days30, - months6, - year, - years2, + allTime(Duration(days: 365 * 2003)), + days7(Duration(days: 7)), + days30(Duration(days: 30)), + months6(Duration(days: 30 * 6)), + year(Duration(days: 365)), + years2(Duration(days: 365 * 2)); + + final Duration duration; + + const HistoryDuration(this.duration); } final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); -typedef PlaybackHistoryTrack = ({int count, Track track}); typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); +typedef PlaybackHistoryTrack = ({int count, Track track}); typedef PlaybackHistoryArtist = ({int count, Artist artist}); class PlaybackHistoryTopState { @@ -58,14 +62,7 @@ class PlaybackHistoryTopNotifier build(arg) async { final database = ref.watch(databaseProvider); - final duration = switch (arg) { - HistoryDuration.allTime => const Duration(days: 365 * 2003), - HistoryDuration.days7 => const Duration(days: 7), - HistoryDuration.days30 => const Duration(days: 30), - HistoryDuration.months6 => const Duration(days: 30 * 6), - HistoryDuration.year => const Duration(days: 365), - HistoryDuration.years2 => const Duration(days: 365 * 2), - }; + final duration = arg.duration; final tracksQuery = (database.select(database.historyTable) ..where( From 3bdc46da4d05e675be11a41064029eb3c98baa5d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 30 Jun 2024 21:08:29 +0600 Subject: [PATCH 22/24] feat(stats): add lazy loading support --- lib/models/database/database.dart | 3 +- lib/modules/stats/top/albums.dart | 22 ++- lib/modules/stats/top/artists.dart | 42 +++-- lib/modules/stats/top/tracks.dart | 44 +++-- lib/pages/stats/albums/albums.dart | 41 +++-- lib/pages/stats/artists/artists.dart | 43 +++-- lib/pages/stats/fees/fees.dart | 42 +++-- lib/pages/stats/minutes/minutes.dart | 45 +++-- lib/pages/stats/playlists/playlists.dart | 44 +++-- lib/pages/stats/streams/streams.dart | 45 +++-- lib/provider/history/top.dart | 200 ----------------------- lib/provider/history/top/albums.dart | 135 +++++++++++++++ lib/provider/history/top/playlists.dart | 104 ++++++++++++ lib/provider/history/top/tracks.dart | 119 ++++++++++++++ 14 files changed, 610 insertions(+), 319 deletions(-) create mode 100644 lib/provider/history/top/albums.dart create mode 100644 lib/provider/history/top/playlists.dart create mode 100644 lib/provider/history/top/tracks.dart diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 609d6771..1c233f84 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; +import 'package:drift/extensions/json1.dart'; import 'package:encrypt/encrypt.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; @@ -13,7 +14,7 @@ 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'; -import 'package:flutter/material.dart' hide Table, Key; +import 'package:flutter/material.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; import 'package:sqlite3/sqlite3.dart'; diff --git a/lib/modules/stats/top/albums.dart b/lib/modules/stats/top/albums.dart index bcaa75c5..4329b871 100644 --- a/lib/modules/stats/top/albums.dart +++ b/lib/modules/stats/top/albums.dart @@ -4,6 +4,9 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/albums.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class TopAlbums extends HookConsumerWidget { const TopAlbums({super.key}); @@ -11,14 +14,21 @@ class TopAlbums extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); - final albums = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.whenData((s) => s.albums))); + final topAlbums = ref.watch(historyTopAlbumsProvider(historyDuration)); + final topAlbumsNotifier = + ref.watch(historyTopAlbumsProvider(historyDuration).notifier); - final albumsData = albums.asData?.value ?? []; + final albumsData = topAlbums.asData?.value.items ?? []; - return Skeletonizer( - enabled: albums.isLoading, - child: SliverList.builder( + return Skeletonizer.sliver( + enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topAlbumsNotifier.fetchMore(); + }, + hasError: topAlbums.hasError, + isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + hasReachedMax: topAlbums.asData?.value.hasMore ?? true, itemCount: albumsData.length, itemBuilder: (context, index) { final album = albumsData[index]; diff --git a/lib/modules/stats/top/artists.dart b/lib/modules/stats/top/artists.dart index 094353f2..d5eb2d0e 100644 --- a/lib/modules/stats/top/artists.dart +++ b/lib/modules/stats/top/artists.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class TopArtists extends HookConsumerWidget { const TopArtists({super.key}); @@ -10,20 +15,33 @@ class TopArtists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); - final artists = ref.watch(playbackHistoryTopProvider(historyDuration) - .select((value) => value.whenData((s) => s.artists))); + final topTracks = ref.watch( + historyTopTracksProvider(historyDuration), + ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(historyDuration).notifier); - final artistsData = artists.asData?.value ?? []; + final artistsData = useMemoized( + () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); - return SliverList.builder( - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text("${compactNumberFormatter.format(artist.count)} plays"), - ); - }, + return Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text("${compactNumberFormatter.format(artist.count)} plays"), + ); + }, + ), ); } } diff --git a/lib/modules/stats/top/tracks.dart b/lib/modules/stats/top/tracks.dart index 8bffa800..be457b2e 100644 --- a/lib/modules/stats/top/tracks.dart +++ b/lib/modules/stats/top/tracks.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class TopTracks extends HookConsumerWidget { const TopTracks({super.key}); @@ -10,24 +14,34 @@ class TopTracks extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final historyDuration = ref.watch(playbackHistoryTopDurationProvider); - final tracks = ref.watch( - playbackHistoryTopProvider(historyDuration) - .select((value) => value.whenData((s) => s.tracks)), + final topTracks = ref.watch( + historyTopTracksProvider(historyDuration), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(historyDuration).notifier); - final tracksData = tracks.asData?.value ?? []; + final tracksData = topTracks.asData?.value.items ?? []; - return SliverList.builder( - itemCount: tracksData.length, - itemBuilder: (context, index) { - final track = tracksData[index]; - return StatsTrackItem( - track: track.track, - info: Text( - "${compactNumberFormatter.format(track.count)} plays", - ), - ); - }, + return Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + "${compactNumberFormatter.format(track.count)} plays", + ), + ); + }, + ), ); } } diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index 859eaf26..db0eedf6 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/albums.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsAlbumsPage extends HookConsumerWidget { static const name = "stats_albums"; @@ -12,10 +16,12 @@ class StatsAlbumsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final albums = ref.watch(playbackHistoryTopProvider(HistoryDuration.allTime) - .select((value) => value.whenData((s) => s.albums))); + final topAlbums = + ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime)); + final topAlbumsNotifier = + ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime).notifier); - final albumsData = albums.asData?.value ?? []; + final albumsData = topAlbums.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -23,15 +29,26 @@ class StatsAlbumsPage extends HookConsumerWidget { centerTitle: false, title: Text("Albums"), ), - body: ListView.builder( - itemCount: albumsData.length, - itemBuilder: (context, index) { - final album = albumsData[index]; - return StatsAlbumItem( - album: album.album, - info: Text("${compactNumberFormatter.format(album.count)} plays"), - ); - }, + body: Skeletonizer( + enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topAlbumsNotifier.fetchMore(); + }, + hasError: topAlbums.hasError, + isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, + hasReachedMax: topAlbums.asData?.value.hasMore ?? true, + itemCount: albumsData.length, + itemBuilder: (context, index) { + final album = albumsData[index]; + return StatsAlbumItem( + album: album.album, + info: Text( + "${compactNumberFormatter.format(album.count)} plays", + ), + ); + }, + ), ), ); } diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index e6dadd95..80ff5f23 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsArtistsPage extends HookConsumerWidget { static const name = "stats_artists"; @@ -12,12 +17,14 @@ class StatsArtistsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final artists = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.artists)), + final topTracks = ref.watch( + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final artistsData = artists.asData?.value ?? []; + final artistsData = useMemoized( + () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); return Scaffold( appBar: const PageWindowTitleBar( @@ -25,15 +32,25 @@ class StatsArtistsPage extends HookConsumerWidget { centerTitle: false, title: Text("Artists"), ), - body: ListView.builder( - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text("${compactNumberFormatter.format(artist.count)} plays"), - ); - }, + body: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: + Text("${compactNumberFormatter.format(artist.count)} plays"), + ); + }, + ), ), ); } diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index e1d701eb..0e25c00b 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -1,11 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsStreamFeesPage extends HookConsumerWidget { static const name = "stats_stream_fees"; @@ -16,12 +21,14 @@ class StatsStreamFeesPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final ThemeData(:textTheme, :hintColor) = Theme.of(context); - final artists = ref.watch( - playbackHistoryTopProvider(HistoryDuration.days30) - .select((value) => value.whenData((s) => s.artists)), + final topTracks = ref.watch( + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final artistsData = artists.asData?.value ?? []; + final artistsData = useMemoized( + () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); return Scaffold( appBar: const PageWindowTitleBar( @@ -50,15 +57,24 @@ class StatsStreamFeesPage extends HookConsumerWidget { ), ), ), - SliverList.builder( - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text(usdFormatter.format(artist.count * 0.005)), - ); - }, + Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text(usdFormatter.format(artist.count * 0.005)), + ); + }, + ), ), ], ), diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index 587e9007..ea3048ef 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsMinutesPage extends HookConsumerWidget { static const name = "stats_minutes"; @@ -15,11 +19,12 @@ class StatsMinutesPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final topTracks = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.tracks)), + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final topTracksData = topTracks.asData?.value ?? []; + final tracksData = topTracks.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -27,19 +32,27 @@ class StatsMinutesPage extends HookConsumerWidget { centerTitle: false, automaticallyImplyLeading: true, ), - body: ListView.separated( - separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracksData.length, - itemBuilder: (context, index) { - final (:track, :count) = topTracksData[index]; - - return StatsTrackItem( - track: track, - info: Text( - "${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins", - ), - ); - }, + body: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + separatorBuilder: (context, index) => const Gap(8), + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + "${compactNumberFormatter.format(track.count)} plays", + ), + ); + }, + ), ), ); } diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index f5ee62d0..a6db3e1c 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/playlist_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/playlists.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsPlaylistsPage extends HookConsumerWidget { static const name = "stats_playlists"; @@ -12,12 +16,13 @@ class StatsPlaylistsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlists = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.playlists)), - ); + final topPlaylists = + ref.watch(historyTopPlaylistsProvider(HistoryDuration.allTime)); - final playlistsData = playlists.asData?.value ?? []; + final topPlaylistsNotifier = ref + .watch(historyTopPlaylistsProvider(HistoryDuration.allTime).notifier); + + final playlistsData = topPlaylists.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -25,16 +30,25 @@ class StatsPlaylistsPage extends HookConsumerWidget { centerTitle: false, title: Text("Playlists"), ), - body: ListView.builder( - itemCount: playlistsData.length, - itemBuilder: (context, index) { - final playlist = playlistsData[index]; - return StatsPlaylistItem( - playlist: playlist.playlist, - info: - Text("${compactNumberFormatter.format(playlist.count)} plays"), - ); - }, + body: Skeletonizer( + enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, + child: InfiniteList( + onFetchData: () async { + await topPlaylistsNotifier.fetchMore(); + }, + hasError: topPlaylists.hasError, + isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, + hasReachedMax: topPlaylists.asData?.value.hasMore ?? true, + itemCount: playlistsData.length, + itemBuilder: (context, index) { + final playlist = playlistsData[index]; + return StatsPlaylistItem( + playlist: playlist.playlist, + info: Text( + "${compactNumberFormatter.format(playlist.count)} plays"), + ); + }, + ), ), ); } diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 20e8ff96..dd5856d0 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/formatters.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/history/top/tracks.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class StatsStreamsPage extends HookConsumerWidget { static const name = "stats_streams"; @@ -15,11 +19,12 @@ class StatsStreamsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final topTracks = ref.watch( - playbackHistoryTopProvider(HistoryDuration.allTime) - .select((s) => s.whenData((s) => s.tracks)), + historyTopTracksProvider(HistoryDuration.allTime), ); + final topTracksNotifier = + ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); - final topTracksData = topTracks.asData?.value ?? []; + final tracksData = topTracks.asData?.value.items ?? []; return Scaffold( appBar: const PageWindowTitleBar( @@ -27,19 +32,27 @@ class StatsStreamsPage extends HookConsumerWidget { centerTitle: false, automaticallyImplyLeading: true, ), - body: ListView.separated( - separatorBuilder: (context, index) => const Gap(8), - itemCount: topTracksData.length, - itemBuilder: (context, index) { - final (:track, :count) = topTracksData[index]; - - return StatsTrackItem( - track: track, - info: Text( - "${compactNumberFormatter.format(count)} streams", - ), - ); - }, + body: Skeletonizer( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: InfiniteList( + separatorBuilder: (context, index) => const Gap(8), + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: tracksData.length, + itemBuilder: (context, index) { + final track = tracksData[index]; + return StatsTrackItem( + track: track.track, + info: Text( + "${compactNumberFormatter.format(track.count * track.track.duration!.inMinutes)} mins", + ), + ); + }, + ), ), ); } diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index 82681267..b52e65e2 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -1,11 +1,4 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/models/database/database.dart'; -import 'package:spotube/provider/database/database.dart'; enum HistoryDuration { allTime(Duration(days: 365 * 2003)), @@ -22,196 +15,3 @@ enum HistoryDuration { final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); - -typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); -typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); -typedef PlaybackHistoryTrack = ({int count, Track track}); -typedef PlaybackHistoryArtist = ({int count, Artist artist}); - -class PlaybackHistoryTopState { - final List tracks; - final List albums; - final List playlists; - final List artists; - - const PlaybackHistoryTopState({ - required this.tracks, - required this.albums, - required this.playlists, - required this.artists, - }); - - PlaybackHistoryTopState copyWith({ - List? tracks, - List? albums, - List? playlists, - List? artists, - }) { - return PlaybackHistoryTopState( - tracks: tracks ?? this.tracks, - albums: albums ?? this.albums, - playlists: playlists ?? this.playlists, - artists: artists ?? this.artists, - ); - } -} - -class PlaybackHistoryTopNotifier - extends FamilyAsyncNotifier { - @override - build(arg) async { - final database = ref.watch(databaseProvider); - - final duration = arg.duration; - - final tracksQuery = (database.select(database.historyTable) - ..where( - (tbl) => - tbl.type.equalsValue(HistoryEntryType.track) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(duration), - ), - )); - - final albumsQuery = database.select(database.historyTable) - ..where( - (tbl) => - tbl.type.equalsValue(HistoryEntryType.album) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(duration), - ), - ); - - final playlistsQuery = database.select(database.historyTable) - ..where( - (tbl) => - tbl.type.equalsValue(HistoryEntryType.playlist) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(duration), - ), - ); - - final subscriptions = [ - tracksQuery.watch().listen((event) { - if (state.asData == null) return; - final artists = event - .map((track) => track.track!.artists) - .expand((e) => e ?? []); - state = AsyncData(state.asData!.value.copyWith( - tracks: getTracksWithCount(event), - artists: getArtistsWithCount(artists), - )); - }), - albumsQuery.watch().listen((event) async { - if (state.asData == null) return; - final tracks = await tracksQuery.get(); - - final albumsWithTrackAlbums = [ - for (final historicAlbum in event) historicAlbum.album!, - for (final track in tracks) track.track!.album! - ]; - - state = AsyncData(state.asData!.value.copyWith( - albums: getAlbumsWithCount(albumsWithTrackAlbums), - )); - }), - playlistsQuery.watch().listen((event) { - if (state.asData == null) return; - state = AsyncData(state.asData!.value.copyWith( - playlists: getPlaylistsWithCount(event), - )); - }), - ]; - - ref.onDispose(() { - for (final subscription in subscriptions) { - subscription.cancel(); - } - }); - - return database.transaction(() async { - final tracks = await tracksQuery.get(); - final albums = await albumsQuery.get(); - final playlists = await playlistsQuery.get(); - - final tracksWithCount = getTracksWithCount(tracks); - - final albumsWithTrackAlbums = [ - for (final historicAlbum in albums) historicAlbum.album!, - for (final track in tracks) track.track!.album! - ]; - - final albumsWithCount = getAlbumsWithCount(albumsWithTrackAlbums); - - final artists = tracks - .map((track) => track.track!.artists) - .expand((e) => e ?? []); - - final artistsWithCount = getArtistsWithCount(artists); - - final playlistsWithCount = getPlaylistsWithCount(playlists); - - return PlaybackHistoryTopState( - tracks: tracksWithCount, - albums: albumsWithCount, - artists: artistsWithCount, - playlists: playlistsWithCount, - ); - }); - } - - List getTracksWithCount(List tracks) { - return groupBy( - tracks, - (track) => track.track!.id!, - ) - .entries - .map((entry) { - return (count: entry.value.length, track: entry.value.first.track!); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); - } - - List getAlbumsWithCount( - List albumsWithTrackAlbums, - ) { - return groupBy(albumsWithTrackAlbums, (album) => album.id!) - .entries - .map((entry) { - return (count: entry.value.length, album: entry.value.first); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); - } - - List getArtistsWithCount(Iterable artists) { - return groupBy(artists, (artist) => artist.id!) - .entries - .map((entry) { - return (count: entry.value.length, artist: entry.value.first); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); - } - - List getPlaylistsWithCount( - List playlists, - ) { - return groupBy(playlists, (playlist) => playlist.playlist!.id!) - .entries - .map((entry) { - return ( - count: entry.value.length, - playlist: entry.value.first.playlist!, - ); - }) - .sorted((a, b) => b.count.compareTo(a.count)) - .toList(); - } -} - -final playbackHistoryTopProvider = AsyncNotifierProviderFamily< - PlaybackHistoryTopNotifier, - PlaybackHistoryTopState, - HistoryDuration>(PlaybackHistoryTopNotifier.new); diff --git a/lib/provider/history/top/albums.dart b/lib/provider/history/top/albums.dart new file mode 100644 index 00000000..84518418 --- /dev/null +++ b/lib/provider/history/top/albums.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; + +typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); + +class HistoryTopAlbumsState extends PaginatedState { + HistoryTopAlbumsState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + HistoryTopAlbumsState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }) { + return HistoryTopAlbumsState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< + PlaybackHistoryAlbum, HistoryTopAlbumsState, HistoryDuration> { + HistoryTopAlbumsNotifier() : super(); + + Selectable createAlbumsQuery({int? limit, int? offset}) { + final database = ref.read(databaseProvider); + + final duration = switch (arg) { + HistoryDuration.allTime => '0', + HistoryDuration.days7 => "strftime('%s', 'now', 'weekday 0', '-7 days')", + HistoryDuration.days30 => "strftime('%s', 'now', 'start of month')", + HistoryDuration.months6 => + "strftime('%s', 'start of month', '-5 months')", + HistoryDuration.year => "strftime('%s', 'start of year')", + HistoryDuration.years2 => "strftime('%s', 'start of year', '-1 year')", + }; + + return database.customSelect( + """ + SELECT + history_table.created_at, + """ + r""" + json_extract(history_table.data, '$.album') as data, + json_extract(history_table.data, '$.album.id') as item_id, + json_extract(history_table.data, '$.album.type') as type + """ + """ + FROM history_table + WHERE type = 'track' AND + created_at >= $duration + UNION ALL + SELECT + history_table.created_at, + history_table.data, + history_table.item_id, + history_table.type + FROM history_table + WHERE type = 'album' AND + created_at >= $duration + ORDER BY created_at desc + ${limit != null && offset != null ? 'LIMIT $limit OFFSET $offset' : ''} + """, + readsFrom: {database.historyTable}, + ).map((row) { + final data = row.read('data'); + final album = AlbumSimple.fromJson(jsonDecode(data)); + return album; + }); + } + + @override + fetch(arg, offset, limit) async { + final albumsQuery = createAlbumsQuery(limit: limit, offset: offset); + + return getAlbumsWithCount(await albumsQuery.get()); + } + + @override + build(arg) async { + final albums = await fetch(arg, 0, 20); + + final subscription = createAlbumsQuery().watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + items: getAlbumsWithCount(event), + hasMore: false, + )); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + + return HistoryTopAlbumsState( + items: albums, + offset: albums.length, + limit: 20, + hasMore: true, + ); + } + + List getAlbumsWithCount( + List albumsWithTrackAlbums, + ) { + return groupBy(albumsWithTrackAlbums, (album) => album.id!) + .entries + .map((entry) { + return (count: entry.value.length, album: entry.value.first); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } +} + +final historyTopAlbumsProvider = AsyncNotifierProviderFamily< + HistoryTopAlbumsNotifier, HistoryTopAlbumsState, HistoryDuration>( + () => HistoryTopAlbumsNotifier(), +); diff --git a/lib/provider/history/top/playlists.dart b/lib/provider/history/top/playlists.dart new file mode 100644 index 00000000..04071f7a --- /dev/null +++ b/lib/provider/history/top/playlists.dart @@ -0,0 +1,104 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; + +typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); + +class HistoryTopPlaylistsState extends PaginatedState { + HistoryTopPlaylistsState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + HistoryTopPlaylistsState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }) { + return HistoryTopPlaylistsState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< + PlaybackHistoryPlaylist, HistoryTopPlaylistsState, HistoryDuration> { + HistoryTopPlaylistsNotifier() : super(); + + SimpleSelectStatement<$HistoryTableTable, HistoryTableData> + createPlaylistsQuery() { + final database = ref.read(databaseProvider); + + return database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.playlist) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(arg.duration), + ), + ); + } + + @override + fetch(arg, offset, limit) async { + final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset); + + return getPlaylistsWithCount(await playlistsQuery.get()); + } + + @override + build(arg) async { + final playlists = await fetch(arg, 0, 20); + + final subscription = createPlaylistsQuery().watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + items: getPlaylistsWithCount(event), + hasMore: false, + )); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + + return HistoryTopPlaylistsState( + items: playlists, + offset: playlists.length, + limit: 20, + hasMore: true, + ); + } + + List getPlaylistsWithCount( + List playlists, + ) { + return groupBy(playlists, (playlist) => playlist.playlist!.id!) + .entries + .map((entry) { + return ( + count: entry.value.length, + playlist: entry.value.first.playlist!, + ); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } +} + +final historyTopPlaylistsProvider = AsyncNotifierProviderFamily< + HistoryTopPlaylistsNotifier, HistoryTopPlaylistsState, HistoryDuration>( + () => HistoryTopPlaylistsNotifier(), +); diff --git a/lib/provider/history/top/tracks.dart b/lib/provider/history/top/tracks.dart new file mode 100644 index 00000000..6c4e44b7 --- /dev/null +++ b/lib/provider/history/top/tracks.dart @@ -0,0 +1,119 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/database/database.dart'; +import 'package:spotube/provider/history/top.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; + +typedef PlaybackHistoryTrack = ({int count, Track track}); +typedef PlaybackHistoryArtist = ({int count, Artist artist}); + +class HistoryTopTracksState extends PaginatedState { + HistoryTopTracksState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + List get artists { + return getArtistsWithCount( + items.expand((e) => e.track.artists ?? []), + ); + } + + List getArtistsWithCount(Iterable artists) { + return groupBy(artists, (artist) => artist.id!) + .entries + .map((entry) { + return (count: entry.value.length, artist: entry.value.first); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } + + @override + HistoryTopTracksState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }) { + return HistoryTopTracksState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< + PlaybackHistoryTrack, HistoryTopTracksState, HistoryDuration> { + HistoryTopTracksNotifier() : super(); + + SimpleSelectStatement<$HistoryTableTable, HistoryTableData> + createTracksQuery() { + final database = ref.read(databaseProvider); + + return database.select(database.historyTable) + ..where( + (tbl) => + tbl.type.equalsValue(HistoryEntryType.track) & + tbl.createdAt.isBiggerOrEqualValue( + DateTime.now().subtract(arg.duration), + ), + ); + } + + @override + fetch(arg, offset, limit) async { + final tracksQuery = createTracksQuery()..limit(limit, offset: offset); + + return getTracksWithCount(await tracksQuery.get()); + } + + @override + build(arg) async { + final tracks = await fetch(arg, 0, 20); + + final subscription = createTracksQuery().watch().listen((event) { + if (state.asData == null) return; + state = AsyncData(state.asData!.value.copyWith( + items: getTracksWithCount(event), + hasMore: false, + )); + }); + + ref.onDispose(() { + subscription.cancel(); + }); + + return HistoryTopTracksState( + items: tracks, + offset: tracks.length, + limit: 20, + hasMore: true, + ); + } + + List getTracksWithCount(List tracks) { + return groupBy( + tracks, + (track) => track.track!.id!, + ) + .entries + .map((entry) { + return (count: entry.value.length, track: entry.value.first.track!); + }) + .sorted((a, b) => b.count.compareTo(a.count)) + .toList(); + } +} + +final historyTopTracksProvider = AsyncNotifierProviderFamily< + HistoryTopTracksNotifier, HistoryTopTracksState, HistoryDuration>( + () => HistoryTopTracksNotifier(), +); From 7927a3e404cce1b828d4bc079911f85884d44a34 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 1 Jul 2024 13:25:28 +0600 Subject: [PATCH 23/24] chore: fix top album and track invalid time frame operations --- lib/models/database/database.dart | 1 - lib/modules/stats/common/album_item.dart | 2 +- lib/pages/stats/fees/fees.dart | 96 +++++++++++++++++++----- lib/provider/history/top/albums.dart | 9 ++- lib/provider/history/top/tracks.dart | 23 +++++- 5 files changed, 103 insertions(+), 28 deletions(-) diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 1c233f84..74f588ab 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:drift/drift.dart'; -import 'package:drift/extensions/json1.dart'; import 'package:encrypt/encrypt.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:path/path.dart'; diff --git a/lib/modules/stats/common/album_item.dart b/lib/modules/stats/common/album_item.dart index 0424ca70..58604c45 100644 --- a/lib/modules/stats/common/album_item.dart +++ b/lib/modules/stats/common/album_item.dart @@ -33,7 +33,7 @@ class StatsAlbumItem extends StatelessWidget { Text("${album.albumType?.formatted} • "), Flexible( child: ArtistLink( - artists: album.artists!, + artists: album.artists ?? [], mainAxisAlignment: WrapAlignment.start, ), ), diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index 0e25c00b..33d223ae 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -20,16 +20,25 @@ class StatsStreamFeesPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final ThemeData(:textTheme, :hintColor) = Theme.of(context); + final duration = useState(HistoryDuration.days30); final topTracks = ref.watch( - historyTopTracksProvider(HistoryDuration.allTime), + historyTopTracksProvider(duration.value), ); final topTracksNotifier = - ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier); + ref.watch(historyTopTracksProvider(duration.value).notifier); final artistsData = useMemoized( () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); + final total = useMemoized( + () => artistsData.fold( + 0, + (previousValue, element) => previousValue + element.count * 0.005, + ), + [artistsData], + ); + return Scaffold( appBar: const PageWindowTitleBar( automaticallyImplyLeading: true, @@ -57,23 +66,72 @@ class StatsStreamFeesPage extends HookConsumerWidget { ), ), ), - Skeletonizer.sliver( - enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, - child: SliverInfiniteList( - onFetchData: () async { - await topTracksNotifier.fetchMore(); - }, - hasError: topTracks.hasError, - isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, - hasReachedMax: topTracks.asData?.value.hasMore ?? true, - itemCount: artistsData.length, - itemBuilder: (context, index) { - final artist = artistsData[index]; - return StatsArtistItem( - artist: artist.artist, - info: Text(usdFormatter.format(artist.count * 0.005)), - ); - }, + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total ${usdFormatter.format(total)}", + style: textTheme.titleLarge, + ), + DropdownButton( + value: duration.value, + onChanged: (value) { + if (value == null) return; + duration.value = value; + }, + items: const [ + DropdownMenuItem( + value: HistoryDuration.days7, + child: Text("This week"), + ), + DropdownMenuItem( + value: HistoryDuration.days30, + child: Text("This month"), + ), + DropdownMenuItem( + value: HistoryDuration.months6, + child: Text("Last 6 months"), + ), + DropdownMenuItem( + value: HistoryDuration.year, + child: Text("This year"), + ), + DropdownMenuItem( + value: HistoryDuration.years2, + child: Text("Last 2 years"), + ), + DropdownMenuItem( + value: HistoryDuration.allTime, + child: Text("All time"), + ), + ], + ), + ], + ), + ), + ), + SliverSafeArea( + sliver: Skeletonizer.sliver( + enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, + child: SliverInfiniteList( + onFetchData: () async { + await topTracksNotifier.fetchMore(); + }, + hasError: topTracks.hasError, + isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, + hasReachedMax: topTracks.asData?.value.hasMore ?? true, + itemCount: artistsData.length, + itemBuilder: (context, index) { + final artist = artistsData[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text(usdFormatter.format(artist.count * 0.005)), + ); + }, + ), ), ), ], diff --git a/lib/provider/history/top/albums.dart b/lib/provider/history/top/albums.dart index 84518418..7448a849 100644 --- a/lib/provider/history/top/albums.dart +++ b/lib/provider/history/top/albums.dart @@ -46,9 +46,10 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< HistoryDuration.days7 => "strftime('%s', 'now', 'weekday 0', '-7 days')", HistoryDuration.days30 => "strftime('%s', 'now', 'start of month')", HistoryDuration.months6 => - "strftime('%s', 'start of month', '-5 months')", - HistoryDuration.year => "strftime('%s', 'start of year')", - HistoryDuration.years2 => "strftime('%s', 'start of year', '-1 year')", + "strftime('%s', date('now', '-5 months', 'start of month'))", + HistoryDuration.year => "strftime('%s', date('now', 'start of year'))", + HistoryDuration.years2 => + "strftime('%s', date('now', '-1 years', 'start of year'))", }; return database.customSelect( @@ -59,7 +60,7 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< r""" json_extract(history_table.data, '$.album') as data, json_extract(history_table.data, '$.album.id') as item_id, - json_extract(history_table.data, '$.album.type') as type + 'album' as type """ """ FROM history_table diff --git a/lib/provider/history/top/tracks.dart b/lib/provider/history/top/tracks.dart index 6c4e44b7..56795cc6 100644 --- a/lib/provider/history/top/tracks.dart +++ b/lib/provider/history/top/tracks.dart @@ -62,9 +62,26 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< ..where( (tbl) => tbl.type.equalsValue(HistoryEntryType.track) & - tbl.createdAt.isBiggerOrEqualValue( - DateTime.now().subtract(arg.duration), - ), + tbl.createdAt.isBiggerOrEqualValue(switch (arg) { + HistoryDuration.allTime => DateTime(1970), + // from start of the week + HistoryDuration.days7 => DateTime.now() + .subtract(Duration(days: DateTime.now().weekday - 1)), + // from start of the month + HistoryDuration.days30 => + DateTime.now().subtract(Duration(days: DateTime.now().day - 1)), + // from start of the 6th month + HistoryDuration.months6 => DateTime.now() + .subtract(Duration(days: DateTime.now().day - 1)) + .subtract(const Duration(days: 30 * 6)), + // from start of the year + HistoryDuration.year => DateTime.now() + .subtract(Duration(days: DateTime.now().day - 1)) + .subtract(const Duration(days: 30 * 12)), + HistoryDuration.years2 => DateTime.now() + .subtract(Duration(days: DateTime.now().day - 1)) + .subtract(const Duration(days: 30 * 12 * 2)), + }), ); } From cb6b6f142e44771109b708dd7fb7d30f89bbb38e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 1 Jul 2024 19:21:12 +0600 Subject: [PATCH 24/24] chore: playback not working in windows due to using loop back ipv4 address --- lib/provider/server/routes/playback.dart | 1 + lib/services/audio_player/audio_player.dart | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index aa380d01..30322a6f 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -23,6 +23,7 @@ class ServerPlaybackRoutes { try { final track = playlist.tracks.firstWhere((element) => element.id == trackId); + final activeSourcedTrack = ref.read(activeSourcedTrackProvider); final sourcedTrack = activeSourcedTrack?.id == track.id ? activeSourcedTrack diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index bb1a6203..7915dc3b 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -12,6 +12,7 @@ import 'package:media_kit/media_kit.dart' as mk; import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/utils/platform.dart'; part 'audio_players_streams_mixin.dart'; part 'audio_player_impl.dart'; @@ -28,7 +29,7 @@ class SpotubeMedia extends mk.Media { }) : super( track is LocalTrack ? track.path - : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", + : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", extras: { ...?extras, "track": switch (track) { @@ -42,7 +43,7 @@ class SpotubeMedia extends mk.Media { @override String get uri => track is LocalTrack ? (track as LocalTrack).path - : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; + : "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}"; factory SpotubeMedia.fromMedia(mk.Media media) { final track = media.uri.startsWith("http")