refactor: share settings and profile flows
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user