refactor: share settings and profile flows

This commit is contained in:
edde746
2026-05-02 02:35:07 +02:00
parent 0ac5f49e5e
commit 624e8a2991
6 changed files with 104 additions and 91 deletions
@@ -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<bool> 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<void> deleteProfile(BuildContext context, Profile profile) async {
final pcRegistry = context.read<ProfileConnectionRegistry>();
final profileRegistry = context.read<ProfileRegistry>();
final downloadProvider = context.read<DownloadProvider>();
final active = context.read<ActiveProfileProvider>();
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();
}
}
+4 -20
View File
@@ -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<ProfileDetailScreen> {
}
Future<void> _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<ProfileConnectionRegistry>();
final profileRegistry = context.read<ProfileRegistry>();
final downloadProvider = context.read<DownloadProvider>();
final active = context.read<ActiveProfileProvider>();
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);
}
+3 -31
View File
@@ -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<ProfileSwitchScreen> {
}
Future<void> _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<ProfileConnectionRegistry>();
final registry = context.read<ProfileRegistry>();
final downloadProvider = context.read<DownloadProvider>();
final active = context.read<ActiveProfileProvider>();
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) {
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import '../../services/settings_service.dart';
mixin TrackerSettingsLoadMixin<T extends StatefulWidget> on State<T> {
SettingsService? trackerSettings;
bool trackerSettingsLoaded = false;
@override
void initState() {
super.initState();
loadTrackerSettings();
}
Future<void> loadTrackerSettings() async {
final settings = await SettingsService.getInstance();
if (!mounted) return;
setState(() {
trackerSettings = settings;
readTrackerSettings(settings);
trackerSettingsLoaded = true;
});
}
void readTrackerSettings(SettingsService settings);
}
@@ -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<void> startMalConnection(BuildContext context) {
final account = context.read<TrackersProvider>();
@@ -134,25 +135,13 @@ class TrackerSettingsScreen extends StatefulWidget {
State<TrackerSettingsScreen> createState() => _TrackerSettingsScreenState();
}
class _TrackerSettingsScreenState extends State<TrackerSettingsScreen> {
SettingsService? _settings;
class _TrackerSettingsScreenState extends State<TrackerSettingsScreen>
with TrackerSettingsLoadMixin<TrackerSettingsScreen> {
bool _scrobbleEnabled = true;
bool _loaded = false;
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _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<void> _disconnect(TrackersProvider account) async {
@@ -170,7 +159,7 @@ class _TrackerSettingsScreenState extends State<TrackerSettingsScreen> {
@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<TrackerSettingsScreen> {
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(
@@ -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<void> startTraktConnection(BuildContext context) {
final account = context.read<TraktAccountProvider>();
@@ -38,27 +39,14 @@ class TraktSettingsScreen extends StatefulWidget {
State<TraktSettingsScreen> createState() => _TraktSettingsScreenState();
}
class _TraktSettingsScreenState extends State<TraktSettingsScreen> {
SettingsService? _settings;
class _TraktSettingsScreenState extends State<TraktSettingsScreen> with TrackerSettingsLoadMixin<TraktSettingsScreen> {
bool _scrobbleEnabled = true;
bool _watchedSyncEnabled = true;
bool _loaded = false;
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _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<void> _disconnect(TraktAccountProvider account) async {
@@ -77,7 +65,7 @@ class _TraktSettingsScreenState extends State<TraktSettingsScreen> {
@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<TraktSettingsScreen> {
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<TraktSettingsScreen> {
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(