fix: enforce profile picker on first login

close #302
This commit is contained in:
edde746
2026-02-06 12:51:42 +01:00
parent fc79bc4fb7
commit a004f7e398
3 changed files with 113 additions and 71 deletions
+20 -7
View File
@@ -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<void> 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();
+13
View File
@@ -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<MainScreen> 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<MainScreen> with RouteAware, WindowListener
});
}
Future<void> _promptForInitialProfileSelection(UserProfileProvider userProfileProvider) async {
if (!mounted || !userProfileProvider.needsInitialProfileSelection) return;
await Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => const ProfileSwitchScreen(requireSelection: true)));
}
Future<void> _checkForUpdatesOnStartup() async {
// Delay slightly to allow UI to settle
await Future.delayed(const Duration(milliseconds: 500));
+80 -64
View File
@@ -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<ProfileSwitchScreen> createState() => _ProfileSwitchScreenState();
}
class _ProfileSwitchScreenState extends State<ProfileSwitchScreen> {
bool _allowPop = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer<UserProfileProvider>(
builder: (context, userProvider, child) {
final users = userProvider.home?.users ?? [];
return PopScope(
canPop: !widget.requireSelection || _allowPop,
child: Consumer<UserProfileProvider>(
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));
}