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