Files
plezy/lib/providers/user_profile_provider.dart
T

565 lines
18 KiB
Dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../models/plex_home.dart';
import '../models/plex_home_user.dart';
import '../models/plex_user_profile.dart';
import '../services/plex_auth_service.dart';
import '../services/storage_service.dart';
import '../utils/app_logger.dart';
import '../utils/provider_extensions.dart';
import '../widgets/pin_entry_dialog.dart';
import 'plex_client_provider.dart';
class UserProfileProvider extends ChangeNotifier {
PlexHome? _home;
PlexHomeUser? _currentUser;
PlexUserProfile? _profileSettings;
bool _isLoading = false;
String? _error;
PlexHome? get home => _home;
PlexHomeUser? get currentUser => _currentUser;
PlexUserProfile? get profileSettings => _profileSettings;
bool get isLoading => _isLoading;
String? get error => _error;
bool get hasMultipleUsers {
final result = _home?.hasMultipleUsers ?? false;
appLogger.d(
'hasMultipleUsers: _home=${_home != null}, users count=${_home?.users.length ?? 0}, result=$result',
);
return result;
}
PlexAuthService? _authService;
StorageService? _storageService;
// Callback for data invalidation when switching profiles
VoidCallback? _onDataInvalidationRequested;
/// Set a callback to be called when profile switching requires data invalidation
void setDataInvalidationCallback(VoidCallback? callback) {
_onDataInvalidationRequested = callback;
}
/// Trigger data invalidation for all screens
void _invalidateAllData() {
if (_onDataInvalidationRequested != null) {
_onDataInvalidationRequested!();
appLogger.d('Data invalidation triggered for profile switch');
}
}
Future<void> initialize() async {
appLogger.d('UserProfileProvider: Initializing...');
try {
_authService = await PlexAuthService.create();
_storageService = await StorageService.getInstance();
await _loadCachedData();
// If no cached home data or it's expired, try to load from API
if (_home == null) {
appLogger.d(
'UserProfileProvider: No cached home data, attempting to load from API',
);
try {
await loadHomeUsers();
} catch (e) {
appLogger.w(
'UserProfileProvider: Failed to load home users during initialization',
error: e,
);
// Don't set error here as it's not critical for app startup
}
}
// Fetch fresh profile settings from API
appLogger.d('UserProfileProvider: Fetching profile settings from API');
try {
await refreshProfileSettings();
} catch (e) {
appLogger.w(
'UserProfileProvider: Failed to fetch profile settings during initialization',
error: e,
);
// Don't set error here, cached profile (if any) was already loaded
}
appLogger.d('UserProfileProvider: Initialization complete');
} catch (e) {
appLogger.e(
'UserProfileProvider: Critical initialization failure',
error: e,
);
_setError('Failed to initialize profile services');
// Ensure services are null on failure
_authService = null;
_storageService = null;
}
}
Future<void> _loadCachedData() async {
if (_storageService == null) return;
// Load cached home users
final cachedHomeData = _storageService!.getHomeUsersCache();
if (cachedHomeData != null) {
try {
_home = PlexHome.fromJson(cachedHomeData);
} catch (e) {
appLogger.w('Failed to load cached home data', error: e);
}
}
// Load current user UUID
final currentUserUUID = _storageService!.getCurrentUserUUID();
if (currentUserUUID != null && _home != null) {
_currentUser = _home!.getUserByUUID(currentUserUUID);
}
// Profile settings are NOT cached - they will be fetched fresh from API
// in refreshProfileSettings()
notifyListeners();
}
/// Fetch the user's profile settings from the API
Future<void> refreshProfileSettings() async {
if (_authService == null || _storageService == null) {
appLogger.w('refreshProfileSettings: Services not initialized, skipping');
return;
}
appLogger.d('Fetching user profile settings from Plex API');
try {
final currentToken = _storageService!.getPlexToken();
if (currentToken == null) {
appLogger.w(
'refreshProfileSettings: No Plex token available, cannot fetch profile',
);
return;
}
final profile = await _authService!.getUserProfile(currentToken);
_profileSettings = profile;
appLogger.i('Successfully fetched user profile settings from API');
notifyListeners();
} catch (e) {
appLogger.w('Failed to fetch user profile settings from API', error: e);
// Don't set error state, profile will remain null or keep existing value
}
}
Future<void> loadHomeUsers({bool forceRefresh = false}) async {
appLogger.d('loadHomeUsers called - forceRefresh: $forceRefresh');
// Auto-initialize services if not ready
if (_authService == null || _storageService == null) {
appLogger.d(
'loadHomeUsers: Services not initialized, initializing services...',
);
_authService = await PlexAuthService.create();
_storageService = await StorageService.getInstance();
await _loadCachedData();
// Double-check after initialization
if (_authService == null || _storageService == null) {
appLogger.e('loadHomeUsers: Failed to initialize services');
_setError('Failed to initialize services');
return;
}
}
// Use cached data if available and not forcing refresh
if (!forceRefresh && _home != null) {
appLogger.d(
'loadHomeUsers: Using cached data, users count: ${_home!.users.length}',
);
return;
}
_setLoading(true);
_clearError();
try {
final currentToken = _storageService!.getPlexToken();
if (currentToken == null) {
throw Exception('No Plex.tv authentication token available');
}
appLogger.d('loadHomeUsers: Using Plex.tv token');
appLogger.d('loadHomeUsers: Fetching home users from API');
final home = await _authService!.getHomeUsers(currentToken);
_home = home;
appLogger.i(
'loadHomeUsers: Success! Home users count: ${home.users.length}',
);
appLogger.d(
'loadHomeUsers: Users: ${home.users.map((u) => u.displayName).join(', ')}',
);
// Cache the home data
await _storageService!.saveHomeUsersCache(home.toJson());
// Set current user if not already set
if (_currentUser == null) {
final currentUserUUID = _storageService!.getCurrentUserUUID();
if (currentUserUUID != null) {
_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) {
await _storageService!.saveCurrentUserUUID(_currentUser!.uuid);
appLogger.d(
'loadHomeUsers: Set current user to admin: ${_currentUser?.displayName}',
);
}
}
}
notifyListeners();
} catch (e) {
_setError('Failed to load home users: $e');
appLogger.e('Failed to load home users', error: e);
} finally {
_setLoading(false);
}
}
Future<bool> switchToUser(PlexHomeUser user, BuildContext? context) async {
if (_authService == null || _storageService == null) {
_setError('Services not initialized');
return false;
}
if (user.uuid == _currentUser?.uuid) {
// Already on this user
return true;
}
// Extract client provider before async operations
PlexClientProvider? clientProvider;
if (context != null) {
try {
clientProvider = context.plexClient;
} catch (e) {
appLogger.w('Failed to get PlexClientProvider', error: e);
}
}
_setLoading(true);
_clearError();
return await _attemptUserSwitch(user, context, clientProvider, null);
}
Future<bool> _attemptUserSwitch(
PlexHomeUser user,
BuildContext? context,
PlexClientProvider? clientProvider,
String? errorMessage,
) async {
try {
final currentToken = _storageService!.getPlexToken();
if (currentToken == null) {
throw Exception('No Plex.tv authentication token available');
}
// Check if user requires PIN
String? pin;
if (user.requiresPassword && context != null && context.mounted) {
pin = await showPinEntryDialog(
context,
user.displayName,
errorMessage: errorMessage,
);
// User cancelled the PIN dialog
if (pin == null) {
_setLoading(false);
return false;
}
}
final switchResponse = await _authService!.switchToUser(
user.uuid,
currentToken,
pin: pin,
);
// switchResponse.authToken is the new user's Plex.tv token
// We need to fetch servers with this token to get the proper server access token
appLogger.d('Got new user Plex.tv token, fetching servers...');
final servers = await _authService!.fetchServers(
switchResponse.authToken,
);
if (servers.isEmpty) {
throw Exception('No servers available for this user');
}
// Find the current server from storage
final currentServerData = _storageService!.getServerData();
if (currentServerData == null) {
throw Exception('No current server data found');
}
// Find matching server by name or client identifier
final currentServerName = currentServerData['name'] as String?;
final currentServerClientId =
currentServerData['clientIdentifier'] as String?;
final matchingServer = servers.firstWhere(
(server) =>
server.name == currentServerName ||
server.clientIdentifier == currentServerClientId,
orElse: () => servers.first, // Fallback to first server
);
appLogger.d(
'Found matching server: ${matchingServer.name}, getting access token',
);
// Update storage with new tokens
await _storageService!.updateCurrentUser(
user.uuid,
matchingServer
.accessToken, // Use server access token, not Plex.tv token
);
// Also save the new Plex.tv token for future profile operations
await _storageService!.savePlexToken(switchResponse.authToken);
// Update current user
_currentUser = user;
// Update user profile settings (fresh from API)
_profileSettings = switchResponse.profile;
appLogger.d(
'Updated profile settings for user: ${user.displayName}',
error: {
'defaultAudioLanguage':
_profileSettings?.defaultAudioLanguage ?? 'not set',
'defaultSubtitleLanguage':
_profileSettings?.defaultSubtitleLanguage ?? 'not set',
},
);
// Update PlexClient with proper server access token
if (clientProvider != null) {
try {
clientProvider.updateToken(matchingServer.accessToken);
appLogger.d(
'Updated PlexClient with server access token for user: ${user.displayName}',
);
} catch (e) {
appLogger.w('Failed to update PlexClient token', error: e);
}
}
notifyListeners();
// Invalidate all cached data for the new profile
// This will cause screens to refresh and rebuild widgets with the new token
_invalidateAllData();
appLogger.d(
'Profile switch complete, all data and images should refresh with new token',
);
appLogger.i('Successfully switched to user: ${user.displayName}');
return true;
} catch (e) {
// Check if it's a PIN validation error
if (e is DioException && e.response?.statusCode == 403) {
final errors = e.response?.data['errors'] as List?;
if (errors != null && errors.isNotEmpty) {
final errorCode = errors[0]['code'] as int?;
final errorMessage = errors[0]['message'] as String?;
// Error code 1041 means invalid PIN
if (errorCode == 1041) {
appLogger.w('Invalid PIN for user: ${user.displayName}');
_clearError(); // Clear any previous error state
// Retry with error message if context is still available
if (context != null && context.mounted) {
return await _attemptUserSwitch(
user,
context,
clientProvider,
errorMessage ?? 'Incorrect PIN. Please try again.',
);
}
// If context not available, return false without showing error
appLogger.d('Cannot retry PIN entry - context not available');
return false;
}
}
}
// Only show error for non-PIN validation errors
_setError('Failed to switch user: $e');
appLogger.e('Failed to switch to user: ${user.displayName}', error: e);
return false;
} finally {
_setLoading(false);
}
}
Future<void> refreshCurrentUser() async {
if (_currentUser != null) {
await loadHomeUsers(forceRefresh: true);
// Update current user from refreshed data
if (_home != null) {
_currentUser = _home!.getUserByUUID(_currentUser!.uuid);
notifyListeners();
}
}
}
Future<void> logout() async {
if (_storageService == null) return;
_setLoading(true);
try {
await _storageService!.clearUserData();
// Clear user-specific provider state but keep services for future sign-ins
_home = null;
_currentUser = null;
_profileSettings = null;
_onDataInvalidationRequested = null;
_clearError();
notifyListeners();
appLogger.i('User logged out successfully');
} catch (e) {
appLogger.e('Error during logout', error: e);
} finally {
_setLoading(false);
}
}
/// Refresh provider for new server context
/// Call this when switching servers to ensure provider state is synchronized
Future<void> refreshForNewServer([BuildContext? context]) async {
appLogger.d('UserProfileProvider: Refreshing for new server context');
_setLoading(true);
try {
// Clear cached data from previous server (both memory and storage)
_home = null;
_currentUser = null;
_profileSettings = null;
_clearError();
// Re-initialize services with current storage state
_authService = await PlexAuthService.create();
_storageService = await StorageService.getInstance();
// Clear storage state that's specific to the previous server context
await Future.wait([
// Clear home users cache (server-specific)
_storageService!.clearHomeUsersCache(),
// Clear current user UUID (profile-specific, should not persist across servers)
_storageService!.clearCurrentUserUUID(),
]);
appLogger.d('UserProfileProvider: Cleared previous server storage state');
// Load fresh data for the new server (should be empty after clearing cache)
await _loadCachedData();
// Load from API since we cleared the cache
appLogger.d(
'UserProfileProvider: Loading fresh home users for new server',
);
// Store context reference before async operations to avoid build context warnings
final contextForSwitch = context;
try {
await loadHomeUsers();
// After loading home users, if a current user was set (admin user),
// perform a complete profile switch to ensure tokens are properly updated
if (_currentUser != null && contextForSwitch != null) {
appLogger.d(
'UserProfileProvider: Performing complete profile switch to ${_currentUser!.displayName} for new server',
);
// Perform full profile switch which includes API calls and token updates
final userToSwitchTo = _currentUser!;
// ignore: use_build_context_synchronously
final success = await switchToUser(userToSwitchTo, contextForSwitch);
if (success) {
appLogger.d(
'UserProfileProvider: Successfully switched to admin user for new server',
);
} else {
appLogger.w(
'UserProfileProvider: Failed to complete profile switch for new server',
);
}
} else if (_currentUser != null && contextForSwitch == null) {
appLogger.w(
'UserProfileProvider: Cannot perform complete profile switch - no context provided',
);
// Still try to fetch profile settings even without full switch
try {
await refreshProfileSettings();
} catch (e) {
appLogger.w(
'UserProfileProvider: Failed to refresh profile settings for new server',
error: e,
);
}
}
} catch (e) {
appLogger.w(
'UserProfileProvider: Failed to load home users for new server',
error: e,
);
// Don't set error as it's not critical
}
appLogger.d('UserProfileProvider: Refresh for new server complete');
} catch (e) {
appLogger.e(
'UserProfileProvider: Failed to refresh for new server',
error: e,
);
_setError('Failed to refresh for new server');
} finally {
_setLoading(false);
}
}
void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
void _setError(String error) {
_error = error;
notifyListeners();
}
void _clearError() {
_error = null;
}
}