From 0c271a999c9703da690ce36f27d433a947b7d31e Mon Sep 17 00:00:00 2001 From: Feichtmeier Date: Fri, 15 Nov 2024 16:48:35 +0100 Subject: [PATCH] fix: move last fm to its own service, re-organize main by adding register.dart --- lib/app/app_model.dart | 22 +-- lib/constants.dart | 2 + lib/expose/expose_service.dart | 78 +++------ lib/expose/lastfm_service.dart | 96 ++++++++++ lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 1 + lib/main.dart | 252 ++------------------------- lib/register.dart | 214 +++++++++++++++++++++++ lib/settings/view/settings_page.dart | 79 ++++++--- needs_translation.json | 18 +- 10 files changed, 427 insertions(+), 337 deletions(-) create mode 100644 lib/expose/lastfm_service.dart create mode 100644 lib/register.dart diff --git a/lib/app/app_model.dart b/lib/app/app_model.dart index 6d020e92a..9f8856b1b 100644 --- a/lib/app/app_model.dart +++ b/lib/app/app_model.dart @@ -26,20 +26,20 @@ class AppModel extends SafeChangeNotifier { Stream get errorStream => _exposeService.discordErrorStream; Stream get isDiscordConnectedStream => _exposeService.isDiscordConnectedStream; - Stream get isLastFmAuthenticatedStream => - _exposeService.isLastFmAuthenticatedStream; - //TODO: Do something(ex:change the text of the save button) once authenticated - Future connectToDiscord() async => _exposeService.connectToDiscord(); Future disconnectFromDiscord() async => _exposeService.disconnectFromDiscord(); - Future setLastFmAuth() async { - final sessionData = await _exposeService.setLastFmAuth(); - if (sessionData != null) { - _settingsService.setLastFmSessionKey(sessionData['sessionKey']!); - _settingsService.setLastFmUsername(sessionData['username']!); - } - } + + ValueNotifier get isLastFmAuthorized => + _exposeService.isLastFmAuthorized; + Future authorizeLastFm({ + required String apiKey, + required String apiSecret, + }) async => + _exposeService.authorizeLastFm( + apiKey: apiKey, + apiSecret: apiSecret, + ); final GitHub _gitHub; final SettingsService _settingsService; diff --git a/lib/constants.dart b/lib/constants.dart index 73a03d919..a023b4a57 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -5,6 +5,8 @@ const kAppName = 'musicpod'; const kAppId = 'org.feichtmeier.Musicpod'; +const kMusicPodFallBackColor = Color(0xFFed3c63); + const kDiscordApplicationId = '1235321910221602837'; const kLinuxDBusName = 'org.mpris.MediaPlayer2.musicpod'; diff --git a/lib/expose/expose_service.dart b/lib/expose/expose_service.dart index 5b2ecebe5..8680d0ea3 100644 --- a/lib/expose/expose_service.dart +++ b/lib/expose/expose_service.dart @@ -1,24 +1,24 @@ import 'dart:async'; +import 'package:flutter/widgets.dart'; import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; -import 'package:lastfm/lastfm.dart'; -import 'package:url_launcher/url_launcher.dart'; + +import 'lastfm_service.dart'; class ExposeService { ExposeService({ required FlutterDiscordRPC? discordRPC, - required LastFMAuthorized? lastFm, + required LastfmService lastFmService, }) : _discordRPC = discordRPC, - _lastFm = lastFm; + _lastFmService = lastFmService; final FlutterDiscordRPC? _discordRPC; - final LastFMAuthorized? _lastFm; + final LastfmService _lastFmService; + final _errorController = StreamController.broadcast(); - final _lastFmAuthController = StreamController.broadcast(); Stream get discordErrorStream => _errorController.stream; Stream get isDiscordConnectedStream => _discordRPC?.isConnectedStream ?? Stream.value(false); - Stream get isLastFmAuthenticatedStream => _lastFmAuthController.stream; Future exposeTitleOnline({ required String title, @@ -32,43 +32,11 @@ class ExposeService { additionalInfo: additionalInfo, imageUrl: imageUrl, ); - if (_lastFm != null) { - await _exposeTitleToLastfm( - title: title, - artist: artist, - ); - } - } - Future?> setLastFmAuth() async { - final lastfmua = _lastFm as LastFMUnauthorized; - launchUrl( - Uri.parse(await lastfmua.authorizeDesktop()), + await _lastFmService.exposeTitleToLastfm( + title: title, + artist: artist, ); - - const maxWaitDuration = Duration(minutes: 2); // Customize as needed - final startTime = DateTime.now(); - await Future.delayed(const Duration(seconds: 10)); - while (DateTime.now().difference(startTime) < maxWaitDuration) { - try { - final lastfm = await lastfmua.finishAuthorizeDesktop(); - final sessionKey = lastfm.sessionKey; - final username = lastfm.username; - _updateLastFmAuthStatus(true); - return { - 'sessionKey': sessionKey, - 'username': username, - }; - } catch (e) { - await Future.delayed(const Duration(seconds: 10)); - } - } - _updateLastFmAuthStatus(false); - return null; - } - - void _updateLastFmAuthStatus(bool status) { - _lastFmAuthController.add(status); } Future _exposeTitleToDiscord({ @@ -99,21 +67,6 @@ class ExposeService { } } - Future _exposeTitleToLastfm({ - required String title, - required String artist, - }) async { - try { - await _lastFm?.scrobble( - track: title, - artist: artist, - startTime: DateTime.now(), - ); - } on Exception catch (e) { - _errorController.add(e.toString()); - } - } - Future connect() async { await connectToDiscord(); } @@ -126,6 +79,16 @@ class ExposeService { } } + ValueNotifier get isLastFmAuthorized => _lastFmService.isAuthorized; + Future authorizeLastFm({ + required String apiKey, + required String apiSecret, + }) => + _lastFmService.authorize( + apiKey: apiKey, + apiSecret: apiSecret, + ); + Future disconnectFromDiscord() async { try { await _discordRPC?.disconnect(); @@ -137,6 +100,5 @@ class ExposeService { Future dispose() async { await disconnectFromDiscord(); await _errorController.close(); - await _lastFmAuthController.close(); } } diff --git a/lib/expose/lastfm_service.dart b/lib/expose/lastfm_service.dart new file mode 100644 index 000000000..5f1063268 --- /dev/null +++ b/lib/expose/lastfm_service.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:lastfm/lastfm.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../common/logging.dart'; +import '../settings/settings_service.dart'; + +class LastfmService { + LastfmService({required SettingsService settingsService}) + : _settingsService = settingsService; + + final SettingsService _settingsService; + + LastFM? _lastFm; + final isAuthorized = ValueNotifier(false); + void _setLastFm(LastFM lastFm) { + _lastFm = lastFm; + isAuthorized.value = lastFm is LastFMAuthorized; + } + + Future exposeTitleToLastfm({ + required String title, + required String artist, + }) async { + if (isAuthorized.value && _settingsService.enableLastFmScrobbling) { + try { + await (_lastFm as LastFMAuthorized).scrobble( + track: title, + artist: artist, + startTime: DateTime.now(), + ); + } on Exception catch (e) { + printMessageInDebugMode(e); + } + } + } + + void init({LastFMAuthorized? lastFmAuthorized}) { + if (lastFmAuthorized != null) { + _setLastFm(lastFmAuthorized); + _settingsService.setLastFmSessionKey(lastFmAuthorized.sessionKey); + _settingsService.setLastFmUsername(lastFmAuthorized.username); + } else { + final apiKey = _settingsService.lastFmApiKey; + final apiSecret = _settingsService.lastFmSecret; + final sessionKey = _settingsService.lastFmSessionKey; + final username = _settingsService.lastFmUsername; + + if (sessionKey != null && + username != null && + apiKey != null && + apiSecret != null) { + _setLastFm( + LastFMAuthorized( + apiKey, + secret: apiSecret, + sessionKey: sessionKey, + username: username, + ), + ); + } else if (apiKey != null) { + _setLastFm(LastFMUnauthorized(apiKey, apiSecret)); + } + } + } + + Future authorize({ + required String apiKey, + required String apiSecret, + }) async { + _settingsService.setLastFmApiKey(apiKey); + _settingsService.setLastFmSecret(apiSecret); + + final lastfmua = LastFMUnauthorized(apiKey, apiSecret); + launchUrl( + Uri.parse(await lastfmua.authorizeDesktop()), + ); + + const maxWaitDuration = Duration(minutes: 2); // Customize as needed + final startTime = DateTime.now(); + await Future.delayed(const Duration(seconds: 10)); + while (DateTime.now().difference(startTime) < maxWaitDuration) { + try { + final lastfm = await lastfmua.finishAuthorizeDesktop(); + init(lastFmAuthorized: lastfm); + } catch (e) { + printMessageInDebugMode(e); + await Future.delayed(const Duration(seconds: 10)); + } + + if (isAuthorized.value) break; + } + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9c8a7b7e2..4719d2a75 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -37,6 +37,7 @@ "playlistDialogTitleNew": "Wiedergabeliste hinzufügen", "playlistDialogTitleEdit": "Wiedergabeliste editieren", "save": "Speichern", + "saveAndAuthorize": "Speichern und Autorisieren", "title": "Titel", "titles": "Titel", "artist": "Künstler", @@ -357,6 +358,7 @@ "exposeToDiscordTitle": "Discord", "exposeToDiscordSubTitle": "Der Künstler und Titel des Lieds/Radio-Senders/Podcast das du gerade hörst wird online geteilt.", "featureDisabledOnPlatform": "Diese Funktion ist momentan nicht für dieses Betriebssystem aktiviert.", + "exposeToLastfmSubTitle": "Der Künstler und Titel des Lieds/Radio-Senders/Podcast das du gerade hörst wird online geteilt.", "regionNone": "Keine", "onlineArtError": "Die Albumkunst-Online-Suche ist momentan nicht verfügbar", "regionAfghanistan": "Afghanistan", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index af1e5f735..13a58ea11 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -37,6 +37,7 @@ "playlistDialogTitleNew": "Add playlist", "playlistDialogTitleEdit": "Edit playlist", "save": "Save", + "saveAndAuthorize": "Save and authorize", "title": "Title", "titles": "Titles", "artist": "Artist", diff --git a/lib/main.dart b/lib/main.dart index 3a8246ddd..b1e0f02ac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,87 +1,43 @@ import 'dart:io'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:desktop_notifications/desktop_notifications.dart'; -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; -import 'package:github/github.dart'; import 'package:gtk/gtk.dart'; -import 'package:lastfm/lastfm.dart'; import 'package:media_kit/media_kit.dart'; -import 'package:media_kit_video/media_kit_video.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:system_theme/system_theme.dart'; -import 'package:watch_it/watch_it.dart'; import 'package:window_manager/window_manager.dart'; import 'package:yaru/yaru.dart'; -import '../../external_path/external_path_service.dart'; -import '../../library/library_model.dart'; -import 'app/app_model.dart'; -import 'app/connectivity_model.dart'; import 'app/view/app.dart'; import 'app_config.dart'; import 'constants.dart'; -import 'expose/expose_service.dart'; -import 'library/library_service.dart'; -import 'local_audio/local_cover_service.dart'; -import 'local_audio/local_audio_model.dart'; -import 'local_audio/local_audio_service.dart'; -import 'local_audio/local_cover_model.dart'; -import 'notifications/notifications_service.dart'; import 'persistence_utils.dart'; -import 'player/player_model.dart'; -import 'player/player_service.dart'; -import 'podcasts/download_model.dart'; -import 'podcasts/podcast_model.dart'; -import 'podcasts/podcast_service.dart'; -import 'radio/online_art_model.dart'; -import 'radio/online_art_service.dart'; -import 'radio/radio_model.dart'; -import 'radio/radio_service.dart'; -import 'search/search_model.dart'; -import 'settings/settings_model.dart'; -import 'settings/settings_service.dart'; +import 'register.dart'; Future main(List args) async { - WidgetsFlutterBinding.ensureInitialized(); - - if (!isMobile) { + if (isMobile) { + WidgetsFlutterBinding.ensureInitialized(); + } else { + // Note: this includes the `WidgetsFlutterBinding.ensureInitialized()` call await YaruWindowTitleBar.ensureInitialized(); WindowManager.instance ..setMinimumSize(const Size(500, 700)) ..setSize(const Size(950, 820)); if (!Platform.isLinux) { - SystemTheme.fallbackColor = const Color(0xFFed3c63); + SystemTheme.fallbackColor = kMusicPodFallBackColor; await SystemTheme.accentColor.load(); } } MediaKit.ensureInitialized(); - final sharedPreferences = await SharedPreferences.getInstance(); - final version = (await PackageInfo.fromPlatform()).version; - - if (allowDiscordRPC) { - await FlutterDiscordRPC.initialize(kDiscordApplicationId); - di.registerLazySingleton( - () => FlutterDiscordRPC.instance, - dispose: (s) { - s.disconnect(); - s.dispose(); - }, - ); - } - - final downloadsDefaultDir = await getDownloadsDefaultDir(); - registerServicesAndViewModels( - sharedPreferences: sharedPreferences, + await registerServicesAndViewModels( args: args, - version: version, + sharedPreferences: await SharedPreferences.getInstance(), + version: (await PackageInfo.fromPlatform()).version, allowDiscordRPC: allowDiscordRPC, - downloadsDefaultDir: downloadsDefaultDir, + downloadsDefaultDir: await getDownloadsDefaultDir(), ); runApp( @@ -90,191 +46,3 @@ Future main(List args) async { : const MaterialMusicPodApp(), ); } - -void registerServicesAndViewModels({ - required String? downloadsDefaultDir, - required SharedPreferences sharedPreferences, - required List args, - required String version, - required bool allowDiscordRPC, -}) { - di - ..registerLazySingleton(() => sharedPreferences) - ..registerLazySingleton( - () => Dio(), - dispose: (s) => s.close(), - ) - ..registerLazySingleton( - () => OnlineArtService( - dio: di(), - ), - dispose: (s) => s.dispose(), - ) - ..registerFactory(() { - final apiKey = sharedPreferences.getString(kLastFmApiKey) ?? ''; - final apiSecret = sharedPreferences.getString(klastFmSecret) ?? ''; - final sessionKey = sharedPreferences.getString(kLastFmSessionKey); - final username = sharedPreferences.getString(kLastFmUsername); - - if (sessionKey != null && username != null) { - return LastFMAuthorized( - apiKey, - secret: apiSecret, - sessionKey: sessionKey, - username: username, - ); - } else { - return LastFMUnauthorized(apiKey, apiSecret); - } - }) - ..registerLazySingleton( - () { - final sessionKey = sharedPreferences.getString(kLastFmSessionKey); - final lastFMEnabled = - sharedPreferences.getBool(kEnableLastFmScrobbling) ?? false; - if (sessionKey != null) { - return ExposeService( - discordRPC: allowDiscordRPC ? di() : null, - lastFm: lastFMEnabled ? di() as LastFMAuthorized : null, - ); - } else { - return ExposeService( - discordRPC: allowDiscordRPC ? di() : null, - lastFm: null, - ); - } - }, - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton(LocalCoverService.new, dispose: (s) => s.dispose()) - ..registerLazySingleton( - () => PlayerService( - onlineArtService: di(), - controller: VideoController( - Player( - configuration: const PlayerConfiguration(title: kAppTitle), - ), - ), - exposeService: di(), - localCoverService: di(), - )..init(), - dispose: (s) async => s.dispose(), - ) - ..registerLazySingleton( - () => SettingsService( - sharedPreferences: di(), - downloadsDefaultDir: downloadsDefaultDir, - ), - dispose: (s) async => s.dispose(), - ) - ..registerLazySingleton( - () => LibraryService(sharedPreferences: di()), - dispose: (s) async => s.dispose(), - ) - ..registerLazySingleton( - () => LocalAudioService( - settingsService: di(), - localCoverService: di(), - ), - dispose: (s) async => s.dispose(), - ) - ..registerLazySingleton( - () => - NotificationsService(Platform.isLinux ? NotificationsClient() : null), - dispose: (s) async => s.dispose(), - ) - ..registerLazySingleton( - () => PodcastService( - notificationsService: di(), - settingsService: di(), - libraryService: di(), - ), - ) - ..registerLazySingleton(() => Connectivity()) - ..registerLazySingleton( - () => ExternalPathService( - gtkNotifier: Platform.isLinux ? GtkApplicationNotifier(args) : null, - playerService: di(), - ), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - RadioService.new, - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton(() => GitHub()) - ..registerLazySingleton( - () => ConnectivityModel( - playerService: di(), - connectivity: di(), - ), - ) - ..registerLazySingleton( - () => SettingsModel( - service: di(), - externalPathService: di(), - gitHub: di(), - )..init(), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => LocalCoverModel(localCoverService: di())..init(), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => PlayerModel( - service: di(), - onlineArtService: di(), - )..init(), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => OnlineArtModel( - onlineArtService: di(), - )..init(), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => AppModel( - appVersion: version, - gitHub: di(), - settingsService: di(), - exposeService: di(), - allowManualUpdates: Platform.isLinux ? false : true, - ), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => LibraryModel(di()), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => LocalAudioModel(localAudioService: di()), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => PodcastModel(podcastService: di.get()), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => RadioModel( - radioService: di(), - ), - dispose: (s) => s.dispose(), - ) - ..registerLazySingleton( - () => DownloadModel( - settingsService: di(), - libraryService: di(), - dio: di(), - ), - ) - ..registerLazySingleton( - () => SearchModel( - podcastService: di(), - radioService: di(), - libraryService: di(), - localAudioService: di(), - )..init(), - ); -} diff --git a/lib/register.dart b/lib/register.dart new file mode 100644 index 000000000..4ffbe4e50 --- /dev/null +++ b/lib/register.dart @@ -0,0 +1,214 @@ +import 'app/app_model.dart'; +import 'app/connectivity_model.dart'; +import 'constants.dart'; +import 'dart:io'; +import 'expose/expose_service.dart'; +import 'expose/lastfm_service.dart'; +import 'external_path/external_path_service.dart'; +import 'library/library_model.dart'; +import 'library/library_service.dart'; +import 'local_audio/local_audio_model.dart'; +import 'local_audio/local_audio_service.dart'; +import 'local_audio/local_cover_model.dart'; +import 'local_audio/local_cover_service.dart'; +import 'notifications/notifications_service.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:desktop_notifications/desktop_notifications.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; +import 'package:github/github.dart'; +import 'package:gtk/gtk.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:watch_it/watch_it.dart'; +import 'player/player_model.dart'; +import 'player/player_service.dart'; +import 'podcasts/download_model.dart'; +import 'podcasts/podcast_model.dart'; +import 'podcasts/podcast_service.dart'; +import 'radio/online_art_model.dart'; +import 'radio/online_art_service.dart'; +import 'radio/radio_model.dart'; +import 'radio/radio_service.dart'; +import 'search/search_model.dart'; +import 'settings/settings_model.dart'; +import 'settings/settings_service.dart'; + +/// Registers all Services, ViewModels or other non-UI objects +/// Note: we want lazy registration whenever possible, preferable without any async calls above. +/// Sometimes this is not possible and we need to await a Future before we can register. +Future registerServicesAndViewModels({ + required String? downloadsDefaultDir, + required SharedPreferences sharedPreferences, + required List args, + required String version, + required bool allowDiscordRPC, +}) async { + if (allowDiscordRPC) { + await FlutterDiscordRPC.initialize(kDiscordApplicationId); + di.registerLazySingleton( + () => FlutterDiscordRPC.instance, + dispose: (s) { + s.disconnect(); + s.dispose(); + }, + ); + } + + di + ..registerLazySingleton(() => sharedPreferences) + ..registerLazySingleton( + () => Dio(), + dispose: (s) => s.close(), + ) + ..registerLazySingleton( + () => OnlineArtService( + dio: di(), + ), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => SettingsService( + sharedPreferences: di(), + downloadsDefaultDir: downloadsDefaultDir, + ), + dispose: (s) async => s.dispose(), + ) + ..registerLazySingleton( + () => LastfmService( + settingsService: di(), + )..init(), + ) + ..registerLazySingleton( + () => ExposeService( + discordRPC: allowDiscordRPC ? di() : null, + lastFmService: di(), + ), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton(LocalCoverService.new, dispose: (s) => s.dispose()) + ..registerLazySingleton( + () => PlayerService( + onlineArtService: di(), + controller: VideoController( + Player( + configuration: const PlayerConfiguration(title: kAppTitle), + ), + ), + exposeService: di(), + localCoverService: di(), + )..init(), + dispose: (s) async => s.dispose(), + ) + ..registerLazySingleton( + () => LibraryService(sharedPreferences: di()), + dispose: (s) async => s.dispose(), + ) + ..registerLazySingleton( + () => LocalAudioService( + settingsService: di(), + localCoverService: di(), + ), + dispose: (s) async => s.dispose(), + ) + ..registerLazySingleton( + () => + NotificationsService(Platform.isLinux ? NotificationsClient() : null), + dispose: (s) async => s.dispose(), + ) + ..registerLazySingleton( + () => PodcastService( + notificationsService: di(), + settingsService: di(), + libraryService: di(), + ), + ) + ..registerLazySingleton(() => Connectivity()) + ..registerLazySingleton( + () => ExternalPathService( + gtkNotifier: Platform.isLinux ? GtkApplicationNotifier(args) : null, + playerService: di(), + ), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + RadioService.new, + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton(() => GitHub()) + ..registerLazySingleton( + () => ConnectivityModel( + playerService: di(), + connectivity: di(), + ), + ) + ..registerLazySingleton( + () => SettingsModel( + service: di(), + externalPathService: di(), + gitHub: di(), + )..init(), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => LocalCoverModel(localCoverService: di())..init(), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => PlayerModel( + service: di(), + onlineArtService: di(), + )..init(), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => OnlineArtModel( + onlineArtService: di(), + )..init(), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => AppModel( + appVersion: version, + gitHub: di(), + settingsService: di(), + exposeService: di(), + allowManualUpdates: Platform.isLinux ? false : true, + ), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => LibraryModel(di()), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => LocalAudioModel(localAudioService: di()), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => PodcastModel(podcastService: di.get()), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => RadioModel( + radioService: di(), + ), + dispose: (s) => s.dispose(), + ) + ..registerLazySingleton( + () => DownloadModel( + settingsService: di(), + libraryService: di(), + dio: di(), + ), + ) + ..registerLazySingleton( + () => SearchModel( + podcastService: di(), + radioService: di(), + libraryService: di(), + localAudioService: di(), + )..init(), + ); +} diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index d250ee566..073d4a7d2 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -501,9 +501,35 @@ class _LicenseTile extends StatelessWidget { } } -class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { +class _ExposeOnlineSection extends StatefulWidget + with WatchItStatefulWidgetMixin { const _ExposeOnlineSection(); + @override + State<_ExposeOnlineSection> createState() => _ExposeOnlineSectionState(); +} + +class _ExposeOnlineSectionState extends State<_ExposeOnlineSection> { + late TextEditingController _lastFmApiKeyController; + late TextEditingController _lastFmSecretController; + final _formkey = GlobalKey(); + + @override + void initState() { + final model = di(); + _lastFmApiKeyController = TextEditingController(text: model.lastFmApiKey); + _lastFmSecretController = TextEditingController(text: model.lastFmSecret); + + super.initState(); + } + + @override + void dispose() { + _lastFmApiKeyController.dispose(); + _lastFmSecretController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -515,20 +541,6 @@ class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { final lastFmEnabled = watchPropertyValue((SettingsModel m) => m.enableLastFmScrobbling); - final lastFmApiKey = - watchPropertyValue((SettingsModel m) => m.lastFmApiKey); - - final lastFmSecret = - watchPropertyValue((SettingsModel m) => m.lastFmSecret); - - final TextEditingController lastFmApiKeyController = - TextEditingController(text: lastFmApiKey); - - final TextEditingController lastFmSecretController = - TextEditingController(text: lastFmSecret); - - final formkey = GlobalKey(); - return YaruSection( headline: Text(l10n.exposeOnlineHeadline), margin: const EdgeInsets.only( @@ -581,6 +593,9 @@ class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { const Icon( TablerIcons.brand_lastfm, ), + if (lastFmEnabled && + watchValue((AppModel m) => m.isLastFmAuthorized)) + Text(l10n.connectedTo), Text(l10n.exposeToLastfmTitle), ], ), @@ -601,16 +616,19 @@ class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { Padding( padding: const EdgeInsets.all(8), child: Form( - key: formkey, + key: _formkey, + onChanged: _formkey.currentState?.validate, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: space( heightGap: 10, children: [ TextFormField( - controller: lastFmApiKeyController, + obscureText: true, + controller: _lastFmApiKeyController, decoration: InputDecoration( hintText: l10n.lastfmApiKey, + label: Text(l10n.lastfmApiKey), ), validator: (value) { if (value == null || value.isEmpty) { @@ -618,16 +636,19 @@ class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { } return null; }, + onChanged: (_) => _formkey.currentState?.validate(), onFieldSubmitted: (value) async { - if (formkey.currentState!.validate()) { + if (_formkey.currentState!.validate()) { di().setLastFmApiKey(value); } }, ), TextFormField( - controller: lastFmSecretController, + obscureText: true, + controller: _lastFmSecretController, decoration: InputDecoration( hintText: l10n.lastfmSecret, + label: Text(l10n.lastfmSecret), ), validator: (value) { if (value == null || value.isEmpty) { @@ -635,20 +656,28 @@ class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { } return null; }, + onChanged: (_) => _formkey.currentState?.validate(), onFieldSubmitted: (value) async { - if (formkey.currentState!.validate()) { + if (_formkey.currentState!.validate()) { di().setLastFmSecret(value); } }, ), ImportantButton( onPressed: () { - if (lastFmApiKeyController.text.isNotEmpty && - lastFmSecretController.text.isNotEmpty) { - di().setLastFmAuth(); - } + di() + ..setLastFmApiKey( + _lastFmApiKeyController.text, + ) + ..setLastFmSecret( + _lastFmSecretController.text, + ); + di().authorizeLastFm( + apiKey: _lastFmApiKeyController.text, + apiSecret: _lastFmSecretController.text, + ); }, - child: Text(l10n.save), + child: Text(l10n.saveAndAuthorize), ), ], ), diff --git a/needs_translation.json b/needs_translation.json index 5c06d8be9..fb832c14b 100644 --- a/needs_translation.json +++ b/needs_translation.json @@ -1,6 +1,7 @@ { "cs": [ "local", + "saveAndAuthorize", "downloadsDirectory", "downloadsDirectoryDescription", "downloadsChangeWarning", @@ -293,6 +294,7 @@ "da": [ "local", + "saveAndAuthorize", "insertedIntoQueue", "downloadsDirectory", "downloadsDirectoryDescription", @@ -587,7 +589,6 @@ "de": [ "exposeToLastfmTitle", - "exposeToLastfmSubTitle", "lastfmApiKey", "lastfmSecret", "lastfmApiKeyEmpty", @@ -595,6 +596,7 @@ ], "es": [ + "saveAndAuthorize", "exposeToLastfmTitle", "exposeToLastfmSubTitle", "lastfmApiKey", @@ -605,6 +607,7 @@ "fr": [ "local", + "saveAndAuthorize", "downloadsDirectory", "downloadsDirectoryDescription", "downloadsChangeWarning", @@ -887,6 +890,7 @@ ], "it": [ + "saveAndAuthorize", "exposeToLastfmTitle", "exposeToLastfmSubTitle", "lastfmApiKey", @@ -912,6 +916,7 @@ "removeFromFavorites", "share", "local", + "saveAndAuthorize", "genre", "year", "albumArtist", @@ -1424,6 +1429,7 @@ "removeFromFavorites", "share", "local", + "saveAndAuthorize", "stations", "copyToClipBoard", "insertedIntoQueue", @@ -1756,6 +1762,7 @@ "removeFromFavorites", "share", "local", + "saveAndAuthorize", "genre", "year", "albumArtist", @@ -2239,6 +2246,7 @@ "removeFromFavorites", "share", "local", + "saveAndAuthorize", "genre", "year", "albumArtist", @@ -2709,6 +2717,7 @@ "fullScreen", "leaveFullScreen", "local", + "saveAndAuthorize", "copyToClipBoard", "insertedIntoQueue", "downloadsDirectory", @@ -3004,6 +3013,7 @@ ], "sk": [ + "saveAndAuthorize", "downloadsDirectory", "downloadsDirectoryDescription", "downloadsChangeWarning", @@ -3035,6 +3045,7 @@ "removeFromFavorites", "share", "local", + "saveAndAuthorize", "genre", "year", "albumArtist", @@ -3059,6 +3070,7 @@ "tr": [ "local", + "saveAndAuthorize", "downloadsDirectory", "downloadsDirectoryDescription", "downloadsChangeWarning", @@ -3350,6 +3362,7 @@ ], "zh": [ + "saveAndAuthorize", "useMoreAnimationsTitle", "useMoreAnimationsDescription", "exposeToLastfmTitle", @@ -3361,6 +3374,7 @@ ], "zh_CN": [ + "saveAndAuthorize", "useMoreAnimationsTitle", "useMoreAnimationsDescription", "exposeToLastfmTitle", @@ -3373,6 +3387,7 @@ "zh_HK": [ "local", + "saveAndAuthorize", "downloadsDirectory", "downloadsDirectoryDescription", "downloadsChangeWarning", @@ -3657,6 +3672,7 @@ "zh_TW": [ "local", + "saveAndAuthorize", "downloadsDirectory", "downloadsDirectoryDescription", "downloadsChangeWarning",