392 lines
14 KiB
Dart
392 lines
14 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 '../screens/profile/pin_entry_dialog.dart';
|
|
|
|
class UserProfileProvider extends ChangeNotifier {
|
|
PlexHome? _home;
|
|
PlexHomeUser? _currentUser;
|
|
PlexUserProfile? _profileSettings;
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
bool _isInitialized = false;
|
|
|
|
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;
|
|
}
|
|
|
|
bool get needsInitialProfileSelection => _home != null && _home!.users.isNotEmpty && _currentUser == null;
|
|
|
|
PlexAuthService? _authService;
|
|
StorageService? _storageService;
|
|
|
|
// Callback for data invalidation when switching profiles
|
|
// Receives the list of servers with new profile tokens for reconnection
|
|
Future<void> Function(List<PlexServer>)? _onDataInvalidationRequested;
|
|
|
|
/// Set a callback to be called when profile switching requires data invalidation
|
|
/// The callback receives the list of servers with the new profile's access tokens
|
|
void setDataInvalidationCallback(Future<void> Function(List<PlexServer>)? callback) {
|
|
_onDataInvalidationRequested = callback;
|
|
}
|
|
|
|
/// Trigger data invalidation for all screens with the new profile's servers
|
|
Future<void> _invalidateAllData(List<PlexServer> servers) async {
|
|
if (_onDataInvalidationRequested != null) {
|
|
await _onDataInvalidationRequested!(servers);
|
|
appLogger.d('Data invalidation triggered for profile switch with ${servers.length} servers');
|
|
}
|
|
}
|
|
|
|
Future<void> initialize() async {
|
|
// 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 {
|
|
_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
|
|
}
|
|
|
|
_isInitialized = true;
|
|
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;
|
|
_isInitialized = false; // Allow retry on failure
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// 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: Auto-selected only unprotected user: ${_currentUser?.displayName}');
|
|
} else {
|
|
appLogger.d('loadHomeUsers: No current user selected yet, waiting for explicit profile selection');
|
|
}
|
|
}
|
|
}
|
|
|
|
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, {bool verifyPin = false}) async {
|
|
if (_authService == null || _storageService == null) {
|
|
_setError('Services not initialized');
|
|
return false;
|
|
}
|
|
|
|
if (user.uuid == _currentUser?.uuid && !(verifyPin && user.requiresPassword)) {
|
|
return true;
|
|
}
|
|
|
|
_setLoading(true);
|
|
_clearError();
|
|
|
|
return await _attemptUserSwitch(user, context, null);
|
|
}
|
|
|
|
Future<bool> _attemptUserSwitch(PlexHomeUser user, BuildContext? context, 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
|
|
// Fetch servers with this token to get the proper server access tokens
|
|
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');
|
|
}
|
|
|
|
appLogger.d('Fetched ${servers.length} servers for new profile');
|
|
|
|
// Save the new Plex.tv token for future profile operations
|
|
await _storageService!.savePlexToken(switchResponse.authToken);
|
|
|
|
// Update current user UUID in storage
|
|
await _storageService!.saveCurrentUserUUID(user.uuid);
|
|
|
|
// 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',
|
|
},
|
|
);
|
|
|
|
notifyListeners();
|
|
|
|
// Invalidate all cached data and reconnect to all servers with new tokens
|
|
// The callback will handle server reconnection using the servers list
|
|
await _invalidateAllData(servers);
|
|
|
|
appLogger.d('Profile switch complete, all servers reconnected with new tokens');
|
|
|
|
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.first['code'] as int?;
|
|
final errorMessage = errors.first['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, 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 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();
|
|
|
|
appLogger.i('User logged out successfully');
|
|
} catch (e) {
|
|
appLogger.e('Error during logout', error: e);
|
|
} finally {
|
|
_setLoading(false);
|
|
}
|
|
}
|
|
|
|
void _setLoading(bool loading) {
|
|
_isLoading = loading;
|
|
notifyListeners();
|
|
}
|
|
|
|
void _setError(String error) {
|
|
_error = error;
|
|
notifyListeners();
|
|
}
|
|
|
|
void _clearError() {
|
|
_error = null;
|
|
}
|
|
}
|