diff --git a/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj b/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj
index 73e954d..55b8641 100644
--- a/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj
+++ b/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj
@@ -6,9 +6,9 @@
latest
Jellyfin.Plugin.ShareLinks
Jellyfin.Plugin.ShareLinks
- 1.0.5.0
- 1.0.5.0
- 1.0.5.0
+ 1.0.6.0
+ 1.0.6.0
+ 1.0.6.0
true
false
disable
diff --git a/Jellyfin.Plugin.ShareLinks/Services/ShareLinkRedemptionService.cs b/Jellyfin.Plugin.ShareLinks/Services/ShareLinkRedemptionService.cs
index 2ee8f3d..349cb17 100644
--- a/Jellyfin.Plugin.ShareLinks/Services/ShareLinkRedemptionService.cs
+++ b/Jellyfin.Plugin.ShareLinks/Services/ShareLinkRedemptionService.cs
@@ -280,6 +280,9 @@ public sealed class ShareLinkRedemptionService
var infoUrlJson = JsonSerializer.Serialize($"{pathBase}/System/Info/Public");
var pathBaseJson = JsonSerializer.Serialize(pathBase);
+ // jellyfin-web resolves the server address from LastConnectionMode; 2 (Manual) is
+ // what it stores after a normal sign-in. Jellyfin 12 builds its client straight from
+ // this entry, so Remote (1) with no RemoteAddress left the guest on the splash screen.
return $$"""
@@ -319,7 +322,7 @@ public sealed class ShareLinkRedemptionService
manualAddressOnly: true,
Name: info.ServerName || "Jellyfin",
Id: info.Id,
- LastConnectionMode: 1,
+ LastConnectionMode: 2,
AccessToken: accessToken,
UserId: userId,
DateLastAccessed: Date.now()
diff --git a/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js b/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js
index 38fc769..3ba5929 100644
--- a/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js
+++ b/Jellyfin.Plugin.ShareLinks/Web/sharelinks.js
@@ -2,7 +2,7 @@
var pluginId = '68540b76-ee74-436d-85ff-2abc884bbea6';
var copyLabel = 'Copy Stream URL';
var actionLabel = 'ShareLink';
- var clientVersion = '1.0.3-ui-2';
+ var clientVersion = '1.0.6-ui-1';
var allowedItemStorageKey = 'sharelinks.allowedItemId';
var guestClassName = 'sharelinks-guest';
var hiddenAttr = 'data-sharelinks-hidden';
@@ -382,6 +382,11 @@
+ ' body.' + guestClassName + ' [data-action="addtoplaylist"],'
+ ' body.' + guestClassName + ' [data-action="addtocollection"],'
+ ' body.' + guestClassName + ' [data-id="playlist"],'
+ // The Modern layout (the default from Jellyfin 12) renders its own header
+ // instead of .skinHeader, so its controls need their own selectors here.
+ + ' body.' + guestClassName + ' .MuiAppBar-root a[href^="#/"],'
+ + ' body.' + guestClassName + ' .MuiAppBar-root [aria-controls="app-user-menu"],'
+ + ' body.' + guestClassName + ' .MuiAppBar-root .MuiToolbar-root > button:first-child,'
+ ' body.' + guestClassName + ' [data-id="addtocollection"] { display: none !important; }'
// Cards stay clickable so guests can navigate season/episode cards on a shared series;
// checkAllowedLocation() verifies the destination server-side and redirects if disallowed.
@@ -841,8 +846,10 @@
/**
* Dismisses the native action sheet so our dialog is not stacked on top of it.
- * Clicking the sheet's own close button keeps Jellyfin's animation and state
- * handling; Escape is the fallback when the sheet renders without one.
+ * The sheet's own close button exists only on TV layouts. On desktop, jellyfin-web
+ * closes a sheet when its dialog container gets a mousedown and a click aimed at
+ * the container itself (a click on the backdrop), and ignores Escape outside the
+ * TV layout. Escape stays as the last fallback.
*/
function closeActionSheet(container) {
var closeButton = container.querySelector('.btnCloseActionSheet');
@@ -851,6 +858,13 @@
return;
}
+ var dialogContainer = container.closest && container.closest('.dialogContainer');
+ if (dialogContainer) {
+ dialogContainer.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
+ dialogContainer.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
+ return;
+ }
+
container.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
keyCode: 27,
diff --git a/Jellyfin.Plugin.ShareLinks/meta.json b/Jellyfin.Plugin.ShareLinks/meta.json
index ba224b3..5b8b1a0 100644
--- a/Jellyfin.Plugin.ShareLinks/meta.json
+++ b/Jellyfin.Plugin.ShareLinks/meta.json
@@ -1,12 +1,12 @@
{
"guid": "68540b76-ee74-436d-85ff-2abc884bbea6",
"name": "ShareLinks",
- "version": "1.0.5.0",
+ "version": "1.0.6.0",
"targetAbi": "10.11.0.0",
"framework": "net9.0",
"owner": "Franciskid",
"overview": "Secure expiring guest-share links for Jellyfin items.",
"description": "Adds secure, expiring share links for Jellyfin items with JSON-backed storage, token hashing, and cleanup scaffolding.",
"category": "General",
- "timestamp": "2026-09-11T12:00:00.0000000Z"
+ "timestamp": "2026-09-18T12:00:00.0000000Z"
}
diff --git a/README.md b/README.md
index 021516a..a0bf674 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Share a movie, episode, season or series with a link. The person you send it to does not need
an account and can only access what you shared. You choose when the link expires.
-`Jellyfin 10.11` · `.NET 9` · `no account for the guest` · `access restricted on the server` · `automatic cleanup`
+`Jellyfin 10.11 and 12` · `.NET 9` · `no account for the guest` · `access restricted on the server` · `automatic cleanup`
ShareLinks adds a **ShareLink** entry to the context menu of any movie, series, season or episode.
Choose an expiry date and copy the link. Whoever opens it goes straight to the shared title,
@@ -181,8 +181,10 @@ it in `token-secret.key`.
## Compatibility
-Jellyfin **10.11** (`targetAbi 10.11.0.0`), .NET 9. Tested on 10.11.8 and 10.11.11. The web-client injection
-targets the shipped client, in English and in French.
+Jellyfin **10.11** and **12** from a single build (`targetAbi 10.11.0.0`, .NET 9, which Jellyfin 12's
+.NET 10 runtime loads as is). Tested on 10.11.0, 10.11.5, 10.11.8, 10.11.9, 10.11.11, 12.0 and 12.1.
+The web-client injection targets the shipped client, in the Legacy and Modern layouts, in English
+and in French.
## Workflow