fix(profiles): reuse switch flow for menu
This commit is contained in:
@@ -3,8 +3,10 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../connection/connection.dart';
|
||||
import '../connection/connection_registry.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../screens/profile/pin_entry_dialog.dart';
|
||||
import '../services/plex_auth_service.dart';
|
||||
import '../utils/snackbar_helper.dart';
|
||||
import 'active_profile_binder.dart';
|
||||
import 'active_profile_provider.dart';
|
||||
import 'plex_home_switch.dart';
|
||||
@@ -57,6 +59,27 @@ Future<bool> activateProfileWithPin(BuildContext context, Profile profile) async
|
||||
}
|
||||
}
|
||||
|
||||
/// Activate [profile] from a UI surface, then wait until the active profile's
|
||||
/// server/token binding has settled. Shows the standard switch failure message
|
||||
/// for both activation and binding failures.
|
||||
Future<bool> switchProfileFromUi(BuildContext context, Profile profile) async {
|
||||
final activeProvider = context.read<ActiveProfileProvider>();
|
||||
final ok = await activateProfileWithPin(context, profile);
|
||||
if (!context.mounted) return false;
|
||||
if (!ok) {
|
||||
showErrorSnackBar(context, t.errors.failedToSwitchProfile(displayName: profile.displayName));
|
||||
return false;
|
||||
}
|
||||
|
||||
final bound = await activeProvider.awaitBindingSettle();
|
||||
if (!context.mounted) return false;
|
||||
if (!bound) {
|
||||
showErrorSnackBar(context, t.errors.failedToSwitchProfile(displayName: profile.displayName));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Validate [profile]'s PIN with Plex via `/home/users/{uuid}/switch`. On
|
||||
/// success, persist the minted user-token and mark the profile as
|
||||
/// pre-verified so [ActiveProfileBinder] reuses the cached token instead
|
||||
|
||||
@@ -26,6 +26,7 @@ import '../providers/libraries_provider.dart';
|
||||
import '../providers/playback_state_provider.dart';
|
||||
import '../widgets/hub_section.dart';
|
||||
import '../widgets/loading_indicator_box.dart';
|
||||
import '../widgets/profile_switching_overlay.dart';
|
||||
import 'profile/profile_switch_screen.dart';
|
||||
import '../connection/connection_registry.dart';
|
||||
import '../profiles/active_profile_provider.dart';
|
||||
@@ -116,6 +117,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
List<MediaHub> _hubs = [];
|
||||
bool _isLoading = true;
|
||||
bool _areHubsLoading = true;
|
||||
bool _switchingProfile = false;
|
||||
String? _errorMessage;
|
||||
final PageController _heroController = PageController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
@@ -898,8 +900,9 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final profiles = activeProvider.profiles;
|
||||
|
||||
return FocusableAction(
|
||||
onPressed: () => _showUserMenu(context),
|
||||
onPressed: _switchingProfile ? null : () => _showUserMenu(context),
|
||||
child: PopupMenuButton<String>(
|
||||
enabled: !_switchingProfile,
|
||||
icon: active != null
|
||||
? ProfileAvatar(profile: active, size: 32)
|
||||
: const AppIcon(Symbols.account_circle_rounded, fill: 1, size: 32, color: Colors.white),
|
||||
@@ -955,6 +958,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
}
|
||||
|
||||
Future<void> _handleUserMenuAction(BuildContext context, String value) async {
|
||||
if (_switchingProfile) return;
|
||||
if (value == 'logout') {
|
||||
unawaited(_handleLogout());
|
||||
return;
|
||||
@@ -968,12 +972,25 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final active = context.read<ActiveProfileProvider>();
|
||||
final target = active.profiles.where((p) => p.id == id).firstOrNull;
|
||||
if (target == null) return;
|
||||
await activateProfileWithPin(context, target);
|
||||
await _switchProfileFromMenu(target);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _switchProfileFromMenu(Profile profile) async {
|
||||
if (_switchingProfile) return;
|
||||
setState(() => _switchingProfile = true);
|
||||
try {
|
||||
await switchProfileFromUi(context, profile);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _switchingProfile = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show user menu programmatically (for D-pad select)
|
||||
void _showUserMenu(BuildContext context) {
|
||||
if (_switchingProfile) return;
|
||||
final actionBar = _actionBarKey.currentState;
|
||||
if (actionBar == null) return;
|
||||
final lastNode = actionBar.getFocusNode(actionBar.widget.actions.length - 1);
|
||||
@@ -1295,6 +1312,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
// initial/tab-switch focus lands on content (hero/hubs), not the toolbar.
|
||||
// Toolbar buttons are still reachable via explicit UP from hero section.
|
||||
Positioned(top: 0, left: 0, right: 0, child: ExcludeFocusTraversal(child: _buildOverlaidAppBar())),
|
||||
if (_switchingProfile) const ProfileSwitchingOverlay(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/backend_badge.dart';
|
||||
import '../../widgets/focusable_popup_menu_button.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/profile_switching_overlay.dart';
|
||||
import '../libraries/state_messages.dart';
|
||||
import '../auth_screen.dart';
|
||||
import 'add_local_profile_screen.dart';
|
||||
@@ -145,33 +146,7 @@ class _ProfileSwitchScreenState extends State<ProfileSwitchScreen> with MountedS
|
||||
),
|
||||
],
|
||||
),
|
||||
// Modal busy overlay so users see the switch is in flight.
|
||||
// Without this the screen visually freezes for a few seconds
|
||||
// while the binder fetches user tokens and rebuilds servers.
|
||||
// `Positioned.fill` is required: a non-positioned `ColoredBox`
|
||||
// sizes itself to its child (just the Card+Center), leaving
|
||||
// the rest of the screen un-dimmed and tappable.
|
||||
if (_switching)
|
||||
Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: Colors.black54,
|
||||
child: Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 56, height: 56, child: CircularProgressIndicator()),
|
||||
const SizedBox(height: 16),
|
||||
Text(t.profiles.switchingProfile),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_switching) const ProfileSwitchingOverlay(),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -384,28 +359,8 @@ class _ProfileSwitchScreenState extends State<ProfileSwitchScreen> with MountedS
|
||||
setState(() => _switching = true);
|
||||
try {
|
||||
final navigator = Navigator.of(context);
|
||||
final activeProvider = context.read<ActiveProfileProvider>();
|
||||
final ok = await activateProfileWithPin(context, profile);
|
||||
if (!mounted) return;
|
||||
if (!ok) {
|
||||
if (context.mounted) {
|
||||
showErrorSnackBar(context, t.errors.failedToSwitchProfile(displayName: profile.displayName));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Stay on the picker while the binder mints the per-user token,
|
||||
// fetches servers, and pushes them into MultiServerManager. The
|
||||
// PIN dialog (if any) overlays the picker via the root navigator,
|
||||
// so popping early would briefly expose the previous profile's
|
||||
// empty-state screen behind the dialog.
|
||||
final bound = await activeProvider.awaitBindingSettle();
|
||||
if (!mounted) return;
|
||||
if (!bound) {
|
||||
if (context.mounted) {
|
||||
showErrorSnackBar(context, t.errors.failedToSwitchProfile(displayName: profile.displayName));
|
||||
}
|
||||
return;
|
||||
}
|
||||
final switched = await switchProfileFromUi(context, profile);
|
||||
if (!mounted || !switched) return;
|
||||
if (widget.requireSelection) {
|
||||
setState(() => _allowPop = true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// Modal Stack child shown while a profile activation is rebinding servers.
|
||||
class ProfileSwitchingOverlay extends StatelessWidget {
|
||||
const ProfileSwitchingOverlay({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned.fill(
|
||||
child: Stack(
|
||||
children: [
|
||||
const ModalBarrier(color: Colors.black54, dismissible: false),
|
||||
Center(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 56, height: 56, child: CircularProgressIndicator()),
|
||||
const SizedBox(height: 16),
|
||||
Text(t.profiles.switchingProfile),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user