Files
sync_replay_project/apps/desktop/lib/src/jellyfin/picker_source.dart
T
foxyzandClaude Opus 5.5 ae4edc29e8 Jellyfin end to end: per-viewer playback, progress, episodes, swap browser
- 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>
2026-10-07 02:38:24 +03:00

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')),
),
],
],
),
),
),
),
);
}
}