- Host/swap send refs only; pasted Jellyfin links become refs; the host hands the relay its own app-obtained credential. - Each viewer resolves with their own account (token in a header) or via the relay, else a clear blocked card with Link account. - Per-account progress reporting (Playing/Progress/Stopped), 401 marks the account expired. - Previous/next episode in the controls (< > / Shift+P/N), floating Next episode pill near the end, auto-advance countdown (one designated swapper), track carry-over per series. - Swap panel uses the shared MediaPicker; browser location remembered per server and opens on the current series. - Fix: states are applied one at a time (a stale pause could land after a newer play). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
3.2 KiB
Dart
106 lines
3.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../screens/home/picked_media.dart';
|
|
import '../screens/home/ui.dart';
|
|
import 'accounts.dart';
|
|
import 'browse_scope.dart';
|
|
import 'browser.dart';
|
|
import 'sign_in_panel.dart';
|
|
|
|
/// The media picker's Jellyfin source: the library browser once a server
|
|
/// is linked, else an inline add-server panel.
|
|
class JellyfinPickerSource extends StatefulWidget {
|
|
final JellyfinAccounts accounts;
|
|
final JellyfinMedia? selected;
|
|
final ValueChanged<JellyfinMedia> onPick;
|
|
final VoidCallback? onOpenIntegrations;
|
|
final VoidCallback? onToggleExpanded;
|
|
final bool expanded;
|
|
final BrowseStart? start;
|
|
const JellyfinPickerSource({
|
|
super.key,
|
|
this.start,
|
|
required this.accounts,
|
|
required this.onPick,
|
|
this.selected,
|
|
this.onOpenIntegrations,
|
|
this.onToggleExpanded,
|
|
this.expanded = false,
|
|
});
|
|
|
|
@override
|
|
State<JellyfinPickerSource> createState() => _JellyfinPickerSourceState();
|
|
}
|
|
|
|
class _JellyfinPickerSourceState extends State<JellyfinPickerSource> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
widget.accounts.ensureLoaded();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: widget.accounts,
|
|
builder: (context, _) {
|
|
final a = widget.accounts;
|
|
if (!a.loaded) {
|
|
return const Center(child: CircularProgressIndicator(strokeWidth: 2));
|
|
}
|
|
if (a.isEmpty) return _LinkFirst(accounts: a, onOpenIntegrations: widget.onOpenIntegrations);
|
|
return JellyfinBrowser(
|
|
key: const ValueKey('media-jellyfin'),
|
|
accounts: a,
|
|
selected: widget.selected,
|
|
onPick: widget.onPick,
|
|
onOpenIntegrations: widget.onOpenIntegrations,
|
|
onToggleExpanded: widget.onToggleExpanded,
|
|
expanded: widget.expanded,
|
|
start: widget.start,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LinkFirst extends StatelessWidget {
|
|
final JellyfinAccounts accounts;
|
|
final VoidCallback? onOpenIntegrations;
|
|
const _LinkFirst({required this.accounts, this.onOpenIntegrations});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
key: const ValueKey('media-jellyfin'),
|
|
child: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 560),
|
|
child: Panel(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('Link a Jellyfin server to browse it here. '
|
|
'You can manage linked servers in Integrations.',
|
|
style: TextStyle(color: Colors.grey)),
|
|
const SizedBox(height: kGap),
|
|
JellyfinSignInPanel(accounts: accounts),
|
|
if (onOpenIntegrations != null) ...[
|
|
const SizedBox(height: 8),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: TextButton(
|
|
onPressed: onOpenIntegrations, child: const Text('Open Integrations')),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|