From a004f7e39808422970ba74a2c2f7dda38b5f7c2c Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:51:06 +0100 Subject: [PATCH] fix: enforce profile picker on first login close #302 --- lib/providers/user_profile_provider.dart | 27 +++- lib/screens/main_screen.dart | 13 ++ .../profile/profile_switch_screen.dart | 144 ++++++++++-------- 3 files changed, 113 insertions(+), 71 deletions(-) diff --git a/lib/providers/user_profile_provider.dart b/lib/providers/user_profile_provider.dart index 79294ae3..7560a5b8 100644 --- a/lib/providers/user_profile_provider.dart +++ b/lib/providers/user_profile_provider.dart @@ -27,6 +27,8 @@ class UserProfileProvider extends ChangeNotifier { return result; } + bool get needsInitialProfileSelection => _home != null && _home!.users.isNotEmpty && _currentUser == null; + PlexAuthService? _authService; StorageService? _storageService; @@ -49,11 +51,15 @@ class UserProfileProvider extends ChangeNotifier { } Future initialize() async { - // Prevent duplicate initialization - if (_isInitialized) { + // Prevent duplicate initialization once we have usable data. + // If initialized state exists but home data is missing, retry bootstrap. + if (_isInitialized && _home != null) { appLogger.d('UserProfileProvider: Already initialized, skipping'); return; } + if (_isInitialized && _home == null) { + appLogger.w('UserProfileProvider: Initialized but home data missing, retrying initialization'); + } appLogger.d('UserProfileProvider: Initializing...'); try { @@ -196,11 +202,14 @@ class UserProfileProvider extends ChangeNotifier { _currentUser = home.getUserByUUID(currentUserUUID); appLogger.d('loadHomeUsers: Set current user from UUID: ${_currentUser?.displayName}'); } else { - // Default to admin user if no current user set - _currentUser = home.adminUser; - if (_currentUser != null) { + // Avoid auto-selecting protected profiles on first login. + // If there's exactly one unprotected profile, select it automatically. + if (home.users.length == 1 && !home.users.first.requiresPassword) { + _currentUser = home.users.first; await _storageService!.saveCurrentUserUUID(_currentUser!.uuid); - appLogger.d('loadHomeUsers: Set current user to admin: ${_currentUser?.displayName}'); + appLogger.d('loadHomeUsers: Auto-selected only unprotected user: ${_currentUser?.displayName}'); + } else { + appLogger.d('loadHomeUsers: No current user selected yet, waiting for explicit profile selection'); } } } @@ -346,11 +355,15 @@ class UserProfileProvider extends ChangeNotifier { try { await _storageService!.clearUserData(); - // Clear user-specific provider state but keep services for future sign-ins + // Clear user-specific provider state and reset initialization so + // the next sign-in performs a full bootstrap. _home = null; _currentUser = null; _profileSettings = null; _onDataInvalidationRequested = null; + _authService = null; + _storageService = null; + _isInitialized = false; _clearError(); notifyListeners(); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index e7010cb8..57883fd6 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -22,6 +22,7 @@ import '../providers/hidden_libraries_provider.dart'; import '../providers/libraries_provider.dart'; import '../providers/playback_state_provider.dart'; import '../providers/settings_provider.dart'; +import '../providers/user_profile_provider.dart'; import '../services/offline_watch_sync_service.dart'; import '../providers/offline_mode_provider.dart'; import '../services/plex_auth_service.dart'; @@ -35,6 +36,7 @@ import 'search_screen.dart'; import 'downloads/downloads_screen.dart'; import 'settings/settings_screen.dart'; import 'video_player_screen.dart'; +import 'profile/profile_switch_screen.dart'; import '../services/watch_next_service.dart'; import '../watch_together/watch_together.dart'; @@ -134,6 +136,9 @@ class _MainScreenState extends State with RouteAware, WindowListener // Set up data invalidation callback for profile switching userProfileProvider.setDataInvalidationCallback(_invalidateAllScreens); + + // Ensure first login (or any unset profile state) requires explicit selection. + await _promptForInitialProfileSelection(userProfileProvider); } // Focus content initially (replaces autofocus which caused focus stealing issues) @@ -146,6 +151,14 @@ class _MainScreenState extends State with RouteAware, WindowListener }); } + Future _promptForInitialProfileSelection(UserProfileProvider userProfileProvider) async { + if (!mounted || !userProfileProvider.needsInitialProfileSelection) return; + + await Navigator.of( + context, + ).push(MaterialPageRoute(builder: (context) => const ProfileSwitchScreen(requireSelection: true))); + } + Future _checkForUpdatesOnStartup() async { // Delay slightly to allow UI to settle await Future.delayed(const Duration(milliseconds: 500)); diff --git a/lib/screens/profile/profile_switch_screen.dart b/lib/screens/profile/profile_switch_screen.dart index 64a39846..5f1d860c 100644 --- a/lib/screens/profile/profile_switch_screen.dart +++ b/lib/screens/profile/profile_switch_screen.dart @@ -10,76 +10,89 @@ import '../../widgets/focused_scroll_scaffold.dart'; import '../libraries/state_messages.dart'; import '../../i18n/strings.g.dart'; -class ProfileSwitchScreen extends StatelessWidget { - const ProfileSwitchScreen({super.key}); +class ProfileSwitchScreen extends StatefulWidget { + final bool requireSelection; + + const ProfileSwitchScreen({super.key, this.requireSelection = false}); + + @override + State createState() => _ProfileSwitchScreenState(); +} + +class _ProfileSwitchScreenState extends State { + bool _allowPop = false; @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Consumer( - builder: (context, userProvider, child) { - final users = userProvider.home?.users ?? []; + return PopScope( + canPop: !widget.requireSelection || _allowPop, + child: Consumer( + builder: (context, userProvider, child) { + final users = userProvider.home?.users ?? []; - return FocusedScrollScaffold( - title: Text(t.screens.switchProfile), - slivers: [ - if (userProvider.isLoading) - const SliverFillRemaining(child: Center(child: CircularProgressIndicator())) - else if (userProvider.error != null) - SliverFillRemaining( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - userProvider.error!, - style: TextStyle(color: theme.colorScheme.error), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - userProvider.refreshCurrentUser(); - }, - child: Text(t.common.retry), - ), - ], - ), - ), - ) - else if (users.isEmpty) - const SliverFillRemaining( - child: EmptyStateWidget( - message: 'No profiles available', - subtitle: 'Contact your Plex administrator to add profiles', - icon: Symbols.person_off_rounded, - ), - ) - else - SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final user = users[index]; - final isCurrentUser = user.uuid == userProvider.currentUser?.uuid; - final isFirstSelectable = - !isCurrentUser && !users.take(index).any((u) => u.uuid != userProvider.currentUser?.uuid); - - return Padding( - padding: EdgeInsets.only(left: 16, right: 16, top: index == 0 ? 16 : 0, bottom: 8), - child: Card( - child: ProfileListTile( - user: user, - isCurrentUser: isCurrentUser, - autofocus: isFirstSelectable, - onTap: () => _switchToUser(context, user), - ), + return FocusedScrollScaffold( + title: Text(t.screens.switchProfile), + automaticallyImplyLeading: !widget.requireSelection, + slivers: [ + if (userProvider.isLoading) + const SliverFillRemaining(child: Center(child: CircularProgressIndicator())) + else if (userProvider.error != null) + SliverFillRemaining( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + userProvider.error!, + style: TextStyle(color: theme.colorScheme.error), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + userProvider.refreshCurrentUser(); + }, + child: Text(t.common.retry), + ), + ], ), - ); - }, childCount: users.length), - ), - ], - ); - }, + ), + ) + else if (users.isEmpty) + const SliverFillRemaining( + child: EmptyStateWidget( + message: 'No profiles available', + subtitle: 'Contact your Plex administrator to add profiles', + icon: Symbols.person_off_rounded, + ), + ) + else + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final user = users[index]; + final isCurrentUser = user.uuid == userProvider.currentUser?.uuid; + final isFirstSelectable = + !isCurrentUser && !users.take(index).any((u) => u.uuid != userProvider.currentUser?.uuid); + + return Padding( + padding: EdgeInsets.only(left: 16, right: 16, top: index == 0 ? 16 : 0, bottom: 8), + child: Card( + child: ProfileListTile( + user: user, + isCurrentUser: isCurrentUser, + autofocus: isFirstSelectable, + onTap: () => _switchToUser(context, user), + ), + ), + ); + }, childCount: users.length), + ), + ], + ); + }, + ), ); } @@ -89,7 +102,10 @@ class ProfileSwitchScreen extends StatelessWidget { final success = await userProvider.switchToUser(user, context); if (success) { - navigator.pop(); + if (widget.requireSelection) { + setState(() => _allowPop = true); + } + navigator.pop(true); } else if (context.mounted) { showErrorSnackBar(context, t.errors.failedToSwitchProfile(displayName: user.displayName)); }