Putting anything up offline starts a solo session: an in-app server bound to 127.0.0.1 only, joined like hosting, so tracking, next episode and the player all work. Invite friends re-hosts on the Host card's port and password with the same media and position; Stop needs no confirmation. Offline, the Library opens with a welcome band (Join a friend, Host, Watch solo); without a Jellyfin account it leads with Files and Link, Jellyfin is an optional card. Play buttons work offline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
3.5 KiB
Dart
112 lines
3.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../screens/home/ui.dart';
|
|
import 'accounts.dart';
|
|
import 'browse_scope.dart';
|
|
import 'browser.dart';
|
|
import 'sign_in_panel.dart';
|
|
|
|
/// The Library's Jellyfin source: the library browser once a server is
|
|
/// linked, else [empty] (default: an inline add-server panel).
|
|
class JellyfinPickerSource extends StatefulWidget {
|
|
final JellyfinAccounts accounts;
|
|
final BrowseController? controller;
|
|
final JellyfinSelection? selected;
|
|
final ValueChanged<JellyfinSelection> onSelect;
|
|
final ValueChanged<JellyfinSelection> onPutUp;
|
|
final VoidCallback? onOpenIntegrations;
|
|
final BrowseStart? start;
|
|
final bool libraryStrip;
|
|
/// Shown while no server is linked.
|
|
final WidgetBuilder? empty;
|
|
const JellyfinPickerSource({
|
|
super.key,
|
|
this.start,
|
|
required this.accounts,
|
|
required this.onSelect,
|
|
required this.onPutUp,
|
|
this.controller,
|
|
this.selected,
|
|
this.onOpenIntegrations,
|
|
this.libraryStrip = true,
|
|
this.empty,
|
|
});
|
|
|
|
@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 && widget.empty != null) return widget.empty!(context);
|
|
if (a.isEmpty) return _LinkFirst(accounts: a, onOpenIntegrations: widget.onOpenIntegrations);
|
|
return JellyfinBrowser(
|
|
key: const ValueKey('media-jellyfin'),
|
|
accounts: a,
|
|
controller: widget.controller,
|
|
selected: widget.selected,
|
|
onSelect: widget.onSelect,
|
|
onPutUp: widget.onPutUp,
|
|
onOpenIntegrations: widget.onOpenIntegrations,
|
|
start: widget.start,
|
|
libraryStrip: widget.libraryStrip,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
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')),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|