101 lines
3.3 KiB
Dart
101 lines
3.3 KiB
Dart
import 'package:flutter/material.dart';
|
|||
|
|
import 'package:material_symbols_icons/symbols.dart';
|
||
|
|
|
||
|
|
import '../focus/focusable_button.dart';
|
||
|
|
import '../i18n/strings.g.dart';
|
||
|
|
import 'app_icon.dart';
|
||
|
|
import 'loading_indicator_box.dart';
|
||
|
|
|
||
|
|
/// The Quick Connect waiting panel: the code to type into Jellyfin, a
|
||
|
|
/// "waiting for approval" line, and a cancel affordance.
|
||
|
|
///
|
||
|
|
/// Shared by the MediaBrowser add-server flow and the Seerr connect flow —
|
||
|
|
/// the same panel for the same interaction, only the server polling the code
|
||
|
|
/// differs. Callers place it in a filling slot (`SliverFillRemaining` + a
|
||
|
|
/// centering `Padding`) and own the poll itself.
|
||
|
|
class QuickConnectCodePanel extends StatelessWidget {
|
||
|
|
/// Code the user types into Jellyfin's Quick Connect screen.
|
||
|
|
final String code;
|
||
|
|
|
||
|
|
/// Focused after the panel appears so a remote can dismiss it.
|
||
|
|
final FocusNode? cancelFocusNode;
|
||
|
|
|
||
|
|
final VoidCallback onCancel;
|
||
|
|
|
||
|
|
/// Inline failure text under the cancel button, styled like
|
||
|
|
/// `AsyncFormStateMixin.buildInlineError`.
|
||
|
|
final String? errorText;
|
||
|
|
|
||
|
|
const QuickConnectCodePanel({
|
||
|
|
super.key,
|
||
|
|
required this.code,
|
||
|
|
required this.onCancel,
|
||
|
|
this.cancelFocusNode,
|
||
|
|
this.errorText,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
final muted = theme.colorScheme.onSurface.withValues(alpha: 0.7);
|
||
|
|
return ConstrainedBox(
|
||
|
|
constraints: const BoxConstraints(maxWidth: 420),
|
||
|
|
child: Column(
|
||
|
|
mainAxisSize: .min,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
t.auth.quickConnectInstructions,
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
style: theme.textTheme.bodyLarge?.copyWith(color: muted),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 32),
|
||
|
|
FittedBox(
|
||
|
|
fit: BoxFit.scaleDown,
|
||
|
|
child: Padding(
|
||
|
|
// letterSpacing adds a trailing gap after the last glyph;
|
||
|
|
// matching left padding keeps the code optically centered.
|
||
|
|
padding: const EdgeInsets.only(left: 12),
|
||
|
|
child: Text(
|
||
|
|
code,
|
||
|
|
style: theme.textTheme.displayLarge?.copyWith(
|
||
|
|
fontFamily: 'monospace',
|
||
|
|
fontWeight: .bold,
|
||
|
|
letterSpacing: 12,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 32),
|
||
|
|
Row(
|
||
|
|
mainAxisSize: .min,
|
||
|
|
children: [
|
||
|
|
const LoadingIndicatorBox(size: 16),
|
||
|
|
const SizedBox(width: 10),
|
||
|
|
Text(t.auth.quickConnectWaiting, style: theme.textTheme.bodyMedium?.copyWith(color: muted)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 32),
|
||
|
|
FocusableButton(
|
||
|
|
focusNode: cancelFocusNode,
|
||
|
|
useBackgroundFocus: true,
|
||
|
|
onPressed: onCancel,
|
||
|
|
child: OutlinedButton.icon(
|
||
|
|
onPressed: onCancel,
|
||
|
|
icon: const AppIcon(Symbols.close_rounded, fill: 1),
|
||
|
|
label: Text(t.auth.quickConnectCancel),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
if (errorText != null) ...[
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
Text(
|
||
|
|
errorText!,
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.error),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|