Focus chrome was implemented twice, once in the focusable wrapper and once in the focus builders; both now go through FocusChrome. TvColorPicker's channel row was a copy of TvNumberSpinner and is now that widget in compact density. Also trims unused helpers and fields and simplifies the Jellyfin browse paths.
132 lines
4.9 KiB
Dart
132 lines
4.9 KiB
Dart
import '../connection/connection.dart';
|
|
import '../exceptions/media_server_exceptions.dart';
|
|
import '../i18n/strings.g.dart';
|
|
import '../services/plex_auth_service.dart';
|
|
import '../utils/app_logger.dart';
|
|
import 'profile_connection.dart';
|
|
import 'profile_connection_registry.dart';
|
|
|
|
/// Outcome of a Plex Home user switch attempt.
|
|
enum PlexHomeSwitchStatus { success, cancelled, failed }
|
|
|
|
class PlexHomeSwitchResult {
|
|
final PlexHomeSwitchStatus status;
|
|
final String? userToken;
|
|
|
|
const PlexHomeSwitchResult._(this.status, this.userToken);
|
|
|
|
bool get succeeded => status == PlexHomeSwitchStatus.success;
|
|
}
|
|
|
|
/// Callback that prompts the user for a Plex Home PIN during a single
|
|
/// `/home/users/{uuid}/switch` round-trip. The profile context is captured
|
|
/// by the caller. Returns the PIN string, or `null` if the user cancelled.
|
|
typedef PlexHomeSwitchPinPrompt = Future<String?> Function({String? errorMessage});
|
|
|
|
/// Switch into [homeUserUuid] on the account identified by [accountToken],
|
|
/// looping on Plex error code 1041 (invalid PIN). Returns the freshly minted
|
|
/// user-level token.
|
|
///
|
|
/// Pass [requiresPin] = true when the Home user has Plex's `protected` flag
|
|
/// set; otherwise the call is attempted without a PIN first and the loop
|
|
/// only kicks in if Plex returns 1041 anyway.
|
|
///
|
|
/// Used by both [ActiveProfileBinder] (lazy-fetching a missing token on
|
|
/// activation) and the borrow flow (minting an independent token for a
|
|
/// borrower). See `lib/profiles/active_profile_binder.dart` and
|
|
/// `lib/screens/profile/borrow_connection_screen.dart`.
|
|
Future<PlexHomeSwitchResult> switchPlexHomeUserWithPin({
|
|
required PlexAuthService auth,
|
|
required String accountToken,
|
|
required String homeUserUuid,
|
|
required bool requiresPin,
|
|
required PlexHomeSwitchPinPrompt promptForPin,
|
|
String? logLabel,
|
|
}) async {
|
|
String? pin;
|
|
String? error;
|
|
while (true) {
|
|
if (requiresPin) {
|
|
pin = await promptForPin(errorMessage: error);
|
|
if (pin == null) return const PlexHomeSwitchResult._(PlexHomeSwitchStatus.cancelled, null);
|
|
}
|
|
try {
|
|
final userToken = await auth.switchToUser(homeUserUuid, accountToken, pin: pin);
|
|
return PlexHomeSwitchResult._(PlexHomeSwitchStatus.success, userToken);
|
|
} on MediaServerHttpException catch (e) {
|
|
if (e.statusCode == 403 && _isInvalidPin(e)) {
|
|
error = t.profiles.incorrectPinTryAgain;
|
|
pin = null;
|
|
// Force the next iteration to prompt even when the caller didn't
|
|
// expect a PIN — Plex disagrees about whether one is required.
|
|
requiresPin = true;
|
|
continue;
|
|
}
|
|
appLogger.e('switchPlexHomeUserWithPin failed${logLabel == null ? '' : ' for $logLabel'}', error: e);
|
|
return const PlexHomeSwitchResult._(PlexHomeSwitchStatus.failed, null);
|
|
} catch (e, st) {
|
|
appLogger.e(
|
|
'switchPlexHomeUserWithPin failed${logLabel == null ? '' : ' for $logLabel'}',
|
|
error: e,
|
|
stackTrace: st,
|
|
);
|
|
return const PlexHomeSwitchResult._(PlexHomeSwitchStatus.failed, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One-shot UI-side mint of a Plex Home user-token: create a
|
|
/// [PlexAuthService], run [switchPlexHomeUserWithPin], optionally persist
|
|
/// the minted token as the ([persistProfileId], account) [ProfileConnection]
|
|
/// row, and dispose the service.
|
|
///
|
|
/// This is the flow every UI surface needs (pre-verify on activation,
|
|
/// borrow, source-PIN validation); hand-rolling it drifts on persistence
|
|
/// and lifecycle. [ActiveProfileBinder] deliberately does NOT use this —
|
|
/// it owns a long-lived auth service.
|
|
Future<PlexHomeSwitchResult> mintPlexHomeUserToken({
|
|
required PlexAccountConnection account,
|
|
required String homeUserUuid,
|
|
required bool requiresPin,
|
|
required PlexHomeSwitchPinPrompt promptForPin,
|
|
ProfileConnectionRegistry? persistTo,
|
|
String? persistProfileId,
|
|
String? logLabel,
|
|
}) async {
|
|
assert((persistTo == null) == (persistProfileId == null), 'persistTo and persistProfileId go together');
|
|
final auth = await PlexAuthService.create();
|
|
try {
|
|
final result = await switchPlexHomeUserWithPin(
|
|
auth: auth,
|
|
accountToken: account.accountToken,
|
|
homeUserUuid: homeUserUuid,
|
|
requiresPin: requiresPin,
|
|
promptForPin: promptForPin,
|
|
logLabel: logLabel,
|
|
);
|
|
if (result.succeeded && persistTo != null && persistProfileId != null) {
|
|
await persistTo.upsert(
|
|
ProfileConnection(
|
|
profileId: persistProfileId,
|
|
connectionId: account.id,
|
|
userToken: result.userToken,
|
|
userIdentifier: homeUserUuid,
|
|
tokenAcquiredAt: DateTime.now(),
|
|
),
|
|
);
|
|
}
|
|
return result;
|
|
} finally {
|
|
auth.dispose();
|
|
}
|
|
}
|
|
|
|
bool _isInvalidPin(MediaServerHttpException e) {
|
|
final data = e.responseData;
|
|
if (data is! Map) return false;
|
|
final errors = data['errors'];
|
|
if (errors is! List || errors.isEmpty) return false;
|
|
final first = errors.first;
|
|
return first is Map && first['code'] == 1041;
|
|
}
|