Side-app use: copy/open links and titles everywhere, version on the Settings tab, known links preview instantly, Ctrl+V adds to Up next

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-11 20:50:49 +03:00
1 parent 9572e8457d
commit 1ec4fc8b13
22 files changed
+1190 -236

No files matched your search

@@ -194,6 +194,13 @@ class JellyfinAccounts extends ChangeNotifier {
JellyfinAccount? account(String serverId) =>
_accounts.where((a) => a.serverId == serverId).firstOrNull;
/// [itemId]'s page in the web UI of linked server [serverId] ("Open in
/// Jellyfin"); null when that server is not linked here.
String? webUrlOf(String serverId, String itemId) {
final a = account(serverId);
return a == null ? null : jellyfinWebUrl(a.baseUrl, itemId, serverId: serverId);
}
/// The chosen default, else the first linked server.
JellyfinAccount? get defaultAccount =>
(_defaultServerId == null ? null : account(_defaultServerId!)) ?? _accounts.firstOrNull;
+27 -11
View File
@@ -7,6 +7,7 @@ import 'package:sync_relay_core/core.dart';
import 'accounts.dart';
import 'format.dart';
import '../widgets/app_menu.dart';
import '../widgets/copy_open.dart';
/// One level of the library browser's back stack.
sealed class BrowseRoute {
@@ -271,6 +272,14 @@ class BrowseController extends ChangeNotifier {
}
}
/// An item's title to copy: an episode with its series ("Show · Name").
String jellyfinCopyTitle(JellyfinItem item) {
final series = item.seriesName;
return item.kind == JellyfinItemKind.episode && series != null && series.isNotEmpty
? '$series · ${item.name}'
: item.name;
}
/// What every browser page needs, provided by [JellyfinBrowser].
class BrowseScope extends InheritedWidget {
final JellyfinAccounts accounts;
@@ -284,7 +293,7 @@ class BrowseScope extends InheritedWidget {
/// Double-click: put it up (a series: its next episode).
final ValueChanged<JellyfinItem> onPutUp;
/// Adds it to Up next (with next: right after what plays); null: no
/// server, the cards have no menu.
/// server (the cards' menu then only copies and opens).
final void Function(JellyfinItem item, {bool next})? onAddToQueue;
/// Home shows the library chips itself (the Library tab has them in its
/// header instead).
@@ -309,23 +318,30 @@ class BrowseScope extends InheritedWidget {
void onOpen(BrowseRoute route) => controller.open(route);
/// Right-click on a playable card ([context]: the card): Put it up, Add
/// to queue, Play next. Nothing without a server.
/// Right-click on a card ([context]: the card): Put it up, Add to queue,
/// Play next (playable, in a server), Copy title, Open in Jellyfin.
Future<void> cardMenu(BuildContext context, JellyfinItem item) async {
final queue = onAddToQueue;
if (queue == null || !isSelectable(item)) return;
final pick = await showAppMenu<int>(context, items: const [
AppMenuItem(value: 0, label: 'Put it up'),
AppMenuItem(value: 1, label: 'Add to queue'),
AppMenuItem(value: 2, label: 'Play next'),
final queue = isSelectable(item) ? onAddToQueue : null;
final pick = await showAppMenu<int>(context, items: [
if (queue != null) ...const [
AppMenuItem(value: 0, label: 'Put it up'),
AppMenuItem(value: 1, label: 'Add to queue'),
AppMenuItem(value: 2, label: 'Play next'),
],
const AppMenuItem(value: 3, label: ReuseText.copyTitle),
const AppMenuItem(value: 4, label: ReuseText.openInJellyfin),
]);
switch (pick) {
case 0:
onPutUp(item);
case 1:
queue(item);
queue!(item);
case 2:
queue(item, next: true);
queue!(item, next: true);
case 3:
await copyToClipboard(jellyfinCopyTitle(item), done: 'Title copied');
case 4:
await openLink(jellyfinWebUrl(client.baseUrl, item.id, serverId: account.serverId));
}
}
@@ -3,6 +3,7 @@ import 'package:sync_relay_core/core.dart';
import '../shell/motion.dart';
import '../widgets/app_menu.dart';
import '../widgets/copy_open.dart';
import 'accounts.dart';
import 'browse_scope.dart';
import 'cards.dart' show doubleClickTime, kindIcon;
@@ -148,14 +149,19 @@ class _JellyfinDetailsPanelState extends State<JellyfinDetailsPanel> {
}
/// Right-click on an episode ([context]: its row): Put it up, Add to
/// queue, Play next.
/// queue, Play next (in a server), Copy title, Open in Jellyfin.
Future<void> _episodeMenu(BuildContext context, JellyfinItem e) async {
if (widget.onAddToQueue == null) return;
setState(() => _targetId = e.id);
final queue = widget.onAddToQueue != null;
if (queue) setState(() => _targetId = e.id);
final client = _client;
final pick = await showAppMenu<int>(context, items: [
AppMenuItem(value: 0, label: widget.solo ? 'Play' : 'Put it up'),
const AppMenuItem(value: 1, label: 'Add to queue'),
const AppMenuItem(value: 2, label: 'Play next'),
if (queue) ...[
AppMenuItem(value: 0, label: widget.solo ? 'Play' : 'Put it up'),
const AppMenuItem(value: 1, label: 'Add to queue'),
const AppMenuItem(value: 2, label: 'Play next'),
],
const AppMenuItem(value: 3, label: ReuseText.copyTitle),
if (client != null) const AppMenuItem(value: 4, label: ReuseText.openInJellyfin),
]);
switch (pick) {
case 0:
@@ -164,6 +170,10 @@ class _JellyfinDetailsPanelState extends State<JellyfinDetailsPanel> {
await _queue(e);
case 2:
await _queue(e, next: true);
case 3:
await copyToClipboard(jellyfinCopyTitle(e), done: 'Title copied');
case 4:
await openLink(jellyfinWebUrl(client!.baseUrl, e.id, serverId: _serverId));
}
}
@@ -383,6 +393,13 @@ class _JellyfinDetailsPanelState extends State<JellyfinDetailsPanel> {
onPressed: () => _c.open(SeriesRoute(d.series!, seasonId: seasonId)),
icon: const Icon(Icons.open_in_new),
),
if (client != null)
IconButton(
key: const ValueKey('details-open-jellyfin'),
tooltip: ReuseText.openInJellyfin,
onPressed: () => openLink(jellyfinWebUrl(client.baseUrl, shown.id, serverId: _serverId)),
icon: const Icon(Icons.open_in_browser_rounded),
),
],
),
if (_error != null) ...[