@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user