From 624e8a2991207d29acd6725e4b43f2ebdeab0c33 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 2 May 2026 02:35:07 +0200 Subject: [PATCH] refactor: share settings and profile flows --- lib/screens/profile/profile_delete_flow.dart | 54 +++++++++++++++++++ .../profile/profile_detail_screen.dart | 24 ++------- .../profile/profile_switch_screen.dart | 34 ++---------- .../settings/tracker_settings_loader.dart | 26 +++++++++ .../settings/tracker_settings_screen.dart | 27 +++------- .../settings/trakt_settings_screen.dart | 30 ++++------- 6 files changed, 104 insertions(+), 91 deletions(-) create mode 100644 lib/screens/profile/profile_delete_flow.dart create mode 100644 lib/screens/settings/tracker_settings_loader.dart diff --git a/lib/screens/profile/profile_delete_flow.dart b/lib/screens/profile/profile_delete_flow.dart new file mode 100644 index 00000000..cbcbf44d --- /dev/null +++ b/lib/screens/profile/profile_delete_flow.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../i18n/strings.g.dart'; +import '../../profiles/active_profile_provider.dart'; +import '../../profiles/profile.dart'; +import '../../profiles/profile_connection_registry.dart'; +import '../../profiles/profile_registry.dart'; +import '../../providers/download_provider.dart'; +import '../../utils/app_logger.dart'; +import '../../utils/dialogs.dart'; +import '../../utils/snackbar_helper.dart'; + +Future confirmAndDeleteProfile( + BuildContext context, { + required Profile profile, + required String title, + required String message, + String? confirmText, +}) async { + final confirmed = await showDeleteConfirmation(context, title: title, message: message, confirmText: confirmText); + if (!confirmed || !context.mounted) return false; + + try { + await deleteProfile(context, profile); + return true; + } catch (error, stackTrace) { + appLogger.w('Failed to delete profile ${profile.id}', error: error, stackTrace: stackTrace); + if (context.mounted) { + showErrorSnackBar(context, t.errors.failedToDeleteProfile(displayName: profile.displayName)); + } + return false; + } +} + +Future deleteProfile(BuildContext context, Profile profile) async { + final pcRegistry = context.read(); + final profileRegistry = context.read(); + final downloadProvider = context.read(); + final active = context.read(); + final wasActive = active.activeId == profile.id; + + await downloadProvider.deleteDownloadsForProfile(profile.id); + await pcRegistry.removeAllForProfile(profile.id); + await profileRegistry.remove(profile.id); + + if (!wasActive) return; + final remaining = active.profiles.where((p) => p.id != profile.id).toList(); + if (remaining.isNotEmpty) { + await active.activate(remaining.first); + } else { + await active.clearActiveProfile(); + } +} diff --git a/lib/screens/profile/profile_detail_screen.dart b/lib/screens/profile/profile_detail_screen.dart index 12914db9..d5daf224 100644 --- a/lib/screens/profile/profile_detail_screen.dart +++ b/lib/screens/profile/profile_detail_screen.dart @@ -9,7 +9,6 @@ import '../../connection/connection_registry.dart'; import '../../i18n/strings.g.dart'; import '../../models/plex/plex_home_user.dart'; import '../../profiles/active_profile_binder.dart'; -import '../../profiles/active_profile_provider.dart'; import '../../profiles/plex_home_service.dart'; import '../../profiles/profile.dart'; import '../../profiles/profile_avatar.dart'; @@ -26,6 +25,7 @@ import '../../utils/dialogs.dart'; import '../settings/add_connection_screen.dart'; import 'pin_entry_dialog.dart'; import 'pin_status_row.dart'; +import 'profile_delete_flow.dart'; import 'profile_name_field.dart'; /// Manage one [Profile] — rename, change PIN, list/add/remove @@ -120,30 +120,14 @@ class _ProfileDetailScreenState extends State { } Future _deleteProfile() async { - final confirmed = await showDeleteConfirmation( + final deleted = await confirmAndDeleteProfile( context, + profile: _profile, title: t.profiles.deleteProfileTitle, message: t.profiles.deleteProfileMessage(displayName: _profile.displayName), confirmText: t.common.delete, ); - if (!confirmed || !mounted) return; - final pcRegistry = context.read(); - final profileRegistry = context.read(); - final downloadProvider = context.read(); - final active = context.read(); - final wasActive = active.activeId == _profile.id; - await downloadProvider.deleteDownloadsForProfile(_profile.id); - await pcRegistry.removeAllForProfile(_profile.id); - await profileRegistry.remove(_profile.id); - if (wasActive) { - final remaining = active.profiles.where((p) => p.id != _profile.id).toList(); - if (remaining.isNotEmpty) { - await active.activate(remaining.first); - } else { - await active.clearActiveProfile(); - } - } - if (!mounted) return; + if (!deleted || !mounted) return; Navigator.of(context).pop(true); } diff --git a/lib/screens/profile/profile_switch_screen.dart b/lib/screens/profile/profile_switch_screen.dart index 5ae597cb..7c2693c5 100644 --- a/lib/screens/profile/profile_switch_screen.dart +++ b/lib/screens/profile/profile_switch_screen.dart @@ -19,7 +19,6 @@ import '../../profiles/profile_connection.dart'; import '../../profiles/profile_connection_registry.dart'; import '../../profiles/profile_registry.dart'; import '../../profiles/profiles_view.dart'; -import '../../providers/download_provider.dart'; import '../../services/storage_service.dart'; import '../../utils/app_logger.dart'; import '../../utils/dialogs.dart'; @@ -30,6 +29,7 @@ import '../../widgets/focused_scroll_scaffold.dart'; import '../libraries/state_messages.dart'; import '../auth_screen.dart'; import 'add_local_profile_screen.dart'; +import 'profile_delete_flow.dart'; import 'profile_detail_screen.dart'; /// Flat picker showing every [Profile] in the system — Plex Home users @@ -285,40 +285,12 @@ class _ProfileSwitchScreenState extends State { } Future _deleteProfile(Profile profile) async { - final confirmed = await showDeleteConfirmation( + await confirmAndDeleteProfile( context, + profile: profile, title: t.profiles.deleteThisProfileTitle, message: t.profiles.deleteThisProfileMessage(displayName: profile.displayName), ); - if (!confirmed || !mounted) return; - - final pcRegistry = context.read(); - final registry = context.read(); - final downloadProvider = context.read(); - final active = context.read(); - final wasActive = active.activeId == profile.id; - try { - await downloadProvider.deleteDownloadsForProfile(profile.id); - await pcRegistry.removeAllForProfile(profile.id); - await registry.remove(profile.id); - } catch (e, st) { - appLogger.w('Failed to delete profile ${profile.id}', error: e, stackTrace: st); - if (mounted) { - showErrorSnackBar(context, t.errors.failedToDeleteProfile(displayName: profile.displayName)); - } - return; - } - - // If we just deleted the active profile, hand off to the first - // remaining one — otherwise the binder is left bound to a ghost. - if (wasActive) { - final remaining = active.profiles.where((p) => p.id != profile.id).toList(); - if (remaining.isNotEmpty) { - await active.activate(remaining.first); - } else { - await active.clearActiveProfile(); - } - } } List<_ChipData> _chipsFor(Profile profile, ProfilesView view) { diff --git a/lib/screens/settings/tracker_settings_loader.dart b/lib/screens/settings/tracker_settings_loader.dart new file mode 100644 index 00000000..45f54fbc --- /dev/null +++ b/lib/screens/settings/tracker_settings_loader.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../../services/settings_service.dart'; + +mixin TrackerSettingsLoadMixin on State { + SettingsService? trackerSettings; + bool trackerSettingsLoaded = false; + + @override + void initState() { + super.initState(); + loadTrackerSettings(); + } + + Future loadTrackerSettings() async { + final settings = await SettingsService.getInstance(); + if (!mounted) return; + setState(() { + trackerSettings = settings; + readTrackerSettings(settings); + trackerSettingsLoaded = true; + }); + } + + void readTrackerSettings(SettingsService settings); +} diff --git a/lib/screens/settings/tracker_settings_screen.dart b/lib/screens/settings/tracker_settings_screen.dart index 1983f625..238c1632 100644 --- a/lib/screens/settings/tracker_settings_screen.dart +++ b/lib/screens/settings/tracker_settings_screen.dart @@ -19,6 +19,7 @@ import '../../widgets/oauth_proxy_dialog.dart'; import '../../widgets/settings_section.dart'; import 'tracker_connect_launcher.dart'; import 'tracker_library_filter_screen.dart'; +import 'tracker_settings_loader.dart'; Future startMalConnection(BuildContext context) { final account = context.read(); @@ -134,25 +135,13 @@ class TrackerSettingsScreen extends StatefulWidget { State createState() => _TrackerSettingsScreenState(); } -class _TrackerSettingsScreenState extends State { - SettingsService? _settings; +class _TrackerSettingsScreenState extends State + with TrackerSettingsLoadMixin { bool _scrobbleEnabled = true; - bool _loaded = false; @override - void initState() { - super.initState(); - _loadSettings(); - } - - Future _loadSettings() async { - final s = await SettingsService.getInstance(); - if (!mounted) return; - setState(() { - _settings = s; - _scrobbleEnabled = widget.config.readScrobbleEnabled(s); - _loaded = true; - }); + void readTrackerSettings(SettingsService settings) { + _scrobbleEnabled = widget.config.readScrobbleEnabled(settings); } Future _disconnect(TrackersProvider account) async { @@ -170,7 +159,7 @@ class _TrackerSettingsScreenState extends State { @override Widget build(BuildContext context) { final title = Text(widget.config.displayName); - if (!_loaded) { + if (!trackerSettingsLoaded) { return FocusedScrollScaffold( title: title, slivers: const [SliverFillRemaining(child: Center(child: CircularProgressIndicator()))], @@ -209,13 +198,13 @@ class _TrackerSettingsScreenState extends State { value: _scrobbleEnabled, onChanged: (value) async { setState(() => _scrobbleEnabled = value); - await widget.config.setScrobbleEnabled(_settings!, value); + await widget.config.setScrobbleEnabled(trackerSettings!, value); }, ), ListTile( leading: const AppIcon(Symbols.filter_list_rounded, fill: 1), title: Text(t.trackers.libraryFilter.title), - subtitle: Text(TrackerLibraryFilterScreen.subtitleFor(_settings!, widget.config.service)), + subtitle: Text(TrackerLibraryFilterScreen.subtitleFor(trackerSettings!, widget.config.service)), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), onTap: () async { await Navigator.of(context).push( diff --git a/lib/screens/settings/trakt_settings_screen.dart b/lib/screens/settings/trakt_settings_screen.dart index fcea43c7..6ca838cc 100644 --- a/lib/screens/settings/trakt_settings_screen.dart +++ b/lib/screens/settings/trakt_settings_screen.dart @@ -16,6 +16,7 @@ import '../../widgets/focused_scroll_scaffold.dart'; import '../../widgets/settings_section.dart'; import 'tracker_connect_launcher.dart'; import 'tracker_library_filter_screen.dart'; +import 'tracker_settings_loader.dart'; Future startTraktConnection(BuildContext context) { final account = context.read(); @@ -38,27 +39,14 @@ class TraktSettingsScreen extends StatefulWidget { State createState() => _TraktSettingsScreenState(); } -class _TraktSettingsScreenState extends State { - SettingsService? _settings; +class _TraktSettingsScreenState extends State with TrackerSettingsLoadMixin { bool _scrobbleEnabled = true; bool _watchedSyncEnabled = true; - bool _loaded = false; @override - void initState() { - super.initState(); - _loadSettings(); - } - - Future _loadSettings() async { - final s = await SettingsService.getInstance(); - if (!mounted) return; - setState(() { - _settings = s; - _scrobbleEnabled = s.read(SettingsService.enableTraktScrobble); - _watchedSyncEnabled = s.read(SettingsService.enableTraktWatchedSync); - _loaded = true; - }); + void readTrackerSettings(SettingsService settings) { + _scrobbleEnabled = settings.read(SettingsService.enableTraktScrobble); + _watchedSyncEnabled = settings.read(SettingsService.enableTraktWatchedSync); } Future _disconnect(TraktAccountProvider account) async { @@ -77,7 +65,7 @@ class _TraktSettingsScreenState extends State { @override Widget build(BuildContext context) { - if (!_loaded) { + if (!trackerSettingsLoaded) { return FocusedScrollScaffold( title: Text(t.trakt.title), slivers: const [SliverFillRemaining(child: Center(child: CircularProgressIndicator()))], @@ -118,7 +106,7 @@ class _TraktSettingsScreenState extends State { value: _scrobbleEnabled, onChanged: (value) async { setState(() => _scrobbleEnabled = value); - await _settings!.write(SettingsService.enableTraktScrobble, value); + await trackerSettings!.write(SettingsService.enableTraktScrobble, value); await TraktScrobbleService.instance.setEnabled(value); }, ), @@ -129,14 +117,14 @@ class _TraktSettingsScreenState extends State { value: _watchedSyncEnabled, onChanged: (value) async { setState(() => _watchedSyncEnabled = value); - await _settings!.write(SettingsService.enableTraktWatchedSync, value); + await trackerSettings!.write(SettingsService.enableTraktWatchedSync, value); await TraktSyncService.instance.setEnabled(value); }, ), ListTile( leading: const AppIcon(Symbols.filter_list_rounded, fill: 1), title: Text(t.trackers.libraryFilter.title), - subtitle: Text(TrackerLibraryFilterScreen.subtitleFor(_settings!, TrackerService.trakt)), + subtitle: Text(TrackerLibraryFilterScreen.subtitleFor(trackerSettings!, TrackerService.trakt)), trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), onTap: () async { await Navigator.of(context).push(