Fichiers
jellyfin-plugin-sharelinks/Jellyfin.Plugin.ShareLinks/Web/configPage.html
T

1040 lignes
41 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>ShareLinks</title>
<style>
#ShareLinksConfigPage .sl-section {
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(127, 127, 127, 0.18);
}
#ShareLinksConfigPage .sl-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem 1rem;
}
#ShareLinksConfigPage .sl-field .fieldDescription {
margin-top: 0.25rem;
}
#ShareLinksConfigPage .sl-muted {
color: var(--theme-secondary-color, #666);
}
#ShareLinksConfigPage .sl-actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
#ShareLinksConfigPage .sl-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
#ShareLinksConfigPage .sl-table th,
#ShareLinksConfigPage .sl-table td {
padding: 0.45rem 0.4rem;
border-bottom: 1px solid rgba(127, 127, 127, 0.14);
vertical-align: top;
text-align: left;
word-break: break-word;
}
#ShareLinksConfigPage .sl-table th {
font-size: 0.88rem;
font-weight: 600;
}
#ShareLinksConfigPage .sl-table .sl-right {
text-align: right;
white-space: nowrap;
}
#ShareLinksConfigPage .sl-inline {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
#ShareLinksConfigPage .sl-list-status {
min-height: 1.25rem;
}
@media (max-width: 900px) {
#ShareLinksConfigPage .sl-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div
id="ShareLinksConfigPage"
data-role="page"
class="page type-interior pluginConfigurationPage"
data-require="emby-input,emby-button,emby-select,emby-checkbox"
>
<div data-role="content">
<div class="content-primary">
<form id="ShareLinksConfigForm">
<div class="sl-section">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">ShareLinks</h2>
</div>
<p class="fieldDescription" data-i18n="intro">
Creates short-lived guest links for movies and episodes, then
keeps the active records visible for revocation.
</p>
</div>
<div class="sl-section">
<h3 class="sectionTitle" data-i18n="sectionGeneral">General</h3>
<div class="sl-grid">
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input is="emby-checkbox" type="checkbox" id="Enabled" />
<span data-i18n="enabled">Enable ShareLinks</span>
</label>
</div>
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
type="checkbox"
id="OneUseDefault"
/>
<span data-i18n="oneUseDefault"
>New links are single use by default</span
>
</label>
<div
class="fieldDescription checkboxFieldDescription"
data-i18n="oneUseDefaultDesc"
>
This only decides how the "Let several people use this link"
box starts out in the create popup; you can change it for
every link you make. A single-use link stops working the
moment the first person opens it, and only that person keeps
access until it expires. A multi-use link can be opened by
everyone you send it to, for as long as it is valid.
</div>
</div>
<div class="sl-field inputContainer">
<input
is="emby-input"
type="number"
id="MaxConcurrentViewers"
data-i18n-label="maxConcurrentViewers"
label="Maximum viewers per multi-use link"
min="0"
max="500"
/>
<div
class="fieldDescription"
data-i18n="maxConcurrentViewersDesc"
>
How many people may watch a multi-use link at the same time.
0 means no limit. Someone arriving once the limit is reached
is asked to try again later; nobody already watching is
disturbed. Single-use links are always one viewer.
</div>
</div>
<div class="sl-field inputContainer">
<input
is="emby-input"
type="number"
id="DefaultExpiryHours"
data-i18n-label="defaultExpiryHours"
label="Default expiry (hours)"
min="1"
max="8760"
/>
<div
class="fieldDescription"
data-i18n="defaultExpiryHoursDesc"
>
Used by the menu action when the admin accepts the default.
</div>
</div>
<div class="sl-field inputContainer">
<input
is="emby-input"
type="number"
id="MaxExpiryHours"
data-i18n-label="maxExpiryHours"
label="Maximum expiry (hours)"
min="1"
max="8760"
/>
</div>
<div class="sl-field inputContainer">
<input
is="emby-input"
type="text"
id="PublicBaseUrlOverride"
data-i18n-label="publicBaseUrlOverride"
label="Public base URL override"
/>
<div
class="fieldDescription"
data-i18n="publicBaseUrlOverrideDesc"
>
Leave empty to derive the public URL from the current
request.
</div>
</div>
<div class="sl-field inputContainer">
<input
is="emby-input"
type="text"
id="GuestUsernamePrefix"
data-i18n-label="guestUsernamePrefix"
label="Guest username prefix"
/>
</div>
<div
class="sl-field inputContainer"
style="grid-column: 1 / -1"
>
<input
is="emby-input"
type="text"
id="GuestHiddenSelectors"
data-i18n-label="guestHiddenSelectors"
label="Cosmetic: hide elements from guests (CSS, comma-separated)"
/>
<div
class="fieldDescription"
data-i18n="guestHiddenSelectorsDesc"
>
Tidies the guest's view only. This runs in the browser and
blocks nothing, so do not rely on it to keep a guest out of
anything. Use the plugin access section below for that.
</div>
</div>
<div class="sl-field inputContainer">
<input
is="emby-input"
type="number"
id="CleanupIntervalMinutes"
data-i18n-label="cleanupIntervalMinutes"
label="Cleanup interval (minutes)"
min="5"
max="10080"
/>
</div>
</div>
</div>
<div class="sl-section">
<h3 class="sectionTitle" data-i18n="sectionPlayback">
Playback policy
</h3>
<div class="sl-grid">
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
type="checkbox"
id="AllowTranscoding"
/>
<span data-i18n="allowTranscoding">Allow transcoding</span>
</label>
</div>
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
type="checkbox"
id="AllowRemuxing"
/>
<span data-i18n="allowRemuxing">Allow remuxing</span>
</label>
</div>
<div
class="checkboxContainer checkboxContainer-withDescription"
>
<label>
<input
is="emby-checkbox"
type="checkbox"
id="GuestModeLockdownEnabled"
/>
<span data-i18n="guestLockdown">Guest lockdown</span>
</label>
<div class="fieldDescription" data-i18n="guestLockdownDesc">
Hides primary navigation for guest sessions in the web
client. This is UX only, not the security boundary.
</div>
</div>
</div>
</div>
<div class="sl-section">
<h3 class="sectionTitle" data-i18n="sectionPluginAccess">
Plugin access for guests
</h3>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input
is="emby-checkbox"
type="checkbox"
id="GuestPluginApiGuardEnabled"
/>
<span data-i18n="blockOtherPlugins"
>Block other plugins for guests</span
>
</label>
<div class="fieldDescription" data-i18n="blockOtherPluginsDesc">
Refuses guest accounts on other plugins' API endpoints, on the
server. A guest holds a real Jellyfin token, so without this
any installed plugin answers them directly, whatever the web
client shows. Jellyfin's own API stays available: the share
tag already limits it to the shared title, and playback needs
it.
</div>
</div>
<div
class="sl-muted"
style="margin-top: 0.75rem"
data-i18n="pluginAccessHint"
>
Tick a plugin to let guests reach it anyway. Leave everything
unticked unless a plugin needs to serve guests during playback,
such as an intro skipper.
</div>
<div id="GuestAllowedPlugins" style="margin-top: 0.5rem">
<div class="sl-muted" data-i18n="loadingPlugins">
Loading plugins…
</div>
</div>
</div>
<div class="sl-section">
<div class="sl-inline">
<h3 class="sectionTitle" data-i18n="sectionShareLinks">
Share links
</h3>
<button
is="emby-button"
type="button"
id="RefreshLinks"
class="raised"
>
<span data-i18n="refresh">Refresh</span>
</button>
<button
is="emby-button"
type="button"
id="CleanupLinks"
class="raised"
>
<span data-i18n="cleanupFinished"
>Clean up finished links</span
>
</button>
<span
id="LinksStatus"
class="sl-muted sl-list-status"
data-i18n="loading"
>Loading…</span
>
</div>
<div style="overflow-x: auto; margin-top: 0.5rem">
<table class="sl-table">
<thead>
<tr>
<th style="width: 9rem" data-i18n="colStatus">Status</th>
<th data-i18n="colItem">Item</th>
<th style="width: 14rem" data-i18n="colLink">Link</th>
<th style="width: 11rem" data-i18n="colGuest">Guest</th>
<th style="width: 11rem" data-i18n="colExpires">
Expires
</th>
<th
style="width: 9rem"
class="sl-right"
data-i18n="colActions"
>
Actions
</th>
</tr>
</thead>
<tbody id="LinksBody">
<tr>
<td colspan="6" class="sl-muted" data-i18n="noLinksYet">
No links loaded yet.
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<button
is="emby-button"
type="submit"
class="raised button-submit block"
>
<span data-i18n="save">Save</span>
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">
(function () {
var ShareLinksPluginId = "68540b76-ee74-436d-85ff-2abc884bbea6";
var page;
function isFrench() {
var lang =
document.documentElement.getAttribute("lang") ||
(navigator && (navigator.language || navigator.userLanguage)) ||
"en";
return /^fr/i.test(lang);
}
var STRINGS = {
en: {
intro:
"Creates short-lived guest links for movies and episodes, then keeps the active records visible for revocation.",
sectionGeneral: "General",
enabled: "Enable ShareLinks",
oneUseDefault: "New links are single use by default",
oneUseDefaultDesc:
'This only decides how the "Let several people use this link" box starts out in the create popup; you can change it for every link you make. A single-use link stops working the moment the first person opens it, and only that person keeps access until it expires. A multi-use link can be opened by everyone you send it to, for as long as it is valid.',
maxConcurrentViewers: "Maximum viewers per multi-use link",
maxConcurrentViewersDesc:
"How many people may watch a multi-use link at the same time. 0 means no limit. Someone arriving once the limit is reached is asked to try again later; nobody already watching is disturbed. Single-use links are always one viewer.",
defaultExpiryHours: "Default expiry (hours)",
defaultExpiryHoursDesc:
"Used by the menu action when the admin accepts the default.",
maxExpiryHours: "Maximum expiry (hours)",
publicBaseUrlOverride: "Public base URL override",
publicBaseUrlOverrideDesc:
"Leave empty to derive the public URL from the current request.",
guestUsernamePrefix: "Guest username prefix",
guestHiddenSelectors:
"Cosmetic: hide elements from guests (CSS, comma-separated)",
guestHiddenSelectorsDesc:
"Tidies the guest's view only. This runs in the browser and blocks nothing, so do not rely on it to keep a guest out of anything. Use the plugin access section below for that.",
cleanupIntervalMinutes: "Cleanup interval (minutes)",
sectionPlayback: "Playback policy",
allowTranscoding: "Allow transcoding",
allowRemuxing: "Allow remuxing",
guestLockdown: "Guest lockdown",
guestLockdownDesc:
"Hides primary navigation for guest sessions in the web client. This is UX only, not the security boundary.",
sectionPluginAccess: "Plugin access for guests",
blockOtherPlugins: "Block other plugins for guests",
blockOtherPluginsDesc:
"Refuses guest accounts on other plugins' API endpoints, on the server. A guest holds a real Jellyfin token, so without this any installed plugin answers them directly, whatever the web client shows. Jellyfin's own API stays available: the share tag already limits it to the shared title, and playback needs it.",
pluginAccessHint:
"Tick a plugin to let guests reach it anyway. Leave everything unticked unless a plugin needs to serve guests during playback, such as an intro skipper.",
loadingPlugins: "Loading plugins…",
noOtherPlugins: "No other plugins installed.",
couldNotLoadPlugins: "Could not load the plugin list.",
sectionShareLinks: "Share links",
refresh: "Refresh",
cleanupFinished: "Clean up finished links",
loading: "Loading…",
colStatus: "Status",
colItem: "Item",
colLink: "Link",
colGuest: "Guest",
colExpires: "Expires",
colActions: "Actions",
noLinksYet: "No links loaded yet.",
noActiveLinks: "No active links.",
noShareLinks: "No share links.",
save: "Save",
copy: "Copy",
copied: "Copied",
open: "Open",
revoke: "Revoke",
confirmRevoke: "Confirm revoke",
notAvailable: "n/a",
loadingShareLinks: "Loading share links…",
couldNotLoadLinks: "Could not load share links.",
loadFailed: "Load failed.",
cleaningUp: "Cleaning up…",
cleanupFailed: "Cleanup failed.",
linkCountOne: "1 link loaded.",
linkCountOther: "{count} links loaded.",
removedCountOne: "1 finished link removed.",
removedCountOther: "{count} finished links removed.",
},
fr: {
intro:
"Crée des liens invités temporaires pour les films et épisodes, et garde les liens actifs visibles pour pouvoir les révoquer.",
sectionGeneral: "Général",
enabled: "Activer ShareLinks",
oneUseDefault:
"Les nouveaux liens sont à usage unique par défaut",
oneUseDefaultDesc:
"Ceci ne détermine que l'état initial de la case \"Autoriser plusieurs personnes à utiliser ce lien\" dans la fenêtre de création ; vous pouvez le changer pour chaque lien créé. Un lien à usage unique cesse de fonctionner dès qu'une première personne l'ouvre, et seule cette personne garde l'accès jusqu'à expiration. Un lien multi-usage peut être ouvert par toutes les personnes à qui vous l'envoyez, tant qu'il reste valide.",
maxConcurrentViewers:
"Nombre maximal de spectateurs par lien multi-usage",
maxConcurrentViewersDesc:
"Combien de personnes peuvent regarder un lien multi-usage en même temps. 0 signifie aucune limite. Une personne arrivant une fois la limite atteinte est invitée à réessayer plus tard ; personne parmi celles déjà en train de regarder n'est interrompu. Les liens à usage unique sont toujours limités à un spectateur.",
defaultExpiryHours: "Expiration par défaut (heures)",
defaultExpiryHoursDesc:
"Utilisé par l'action du menu lorsque l'administrateur accepte la valeur par défaut.",
maxExpiryHours: "Expiration maximale (heures)",
publicBaseUrlOverride: "URL publique personnalisée",
publicBaseUrlOverrideDesc:
"Laissez vide pour déduire l'URL publique à partir de la requête en cours.",
guestUsernamePrefix: "Préfixe du nom d'utilisateur invité",
guestHiddenSelectors:
"Esthétique : masquer des éléments pour les invités (CSS, séparés par des virgules)",
guestHiddenSelectorsDesc:
"N'améliore que l'apparence pour l'invité. Ceci s'exécute dans le navigateur et ne bloque rien : ne vous en servez pas pour empêcher un invité d'accéder à quoi que ce soit. Utilisez la section \"Accès aux extensions\" ci-dessous pour cela.",
cleanupIntervalMinutes: "Intervalle de nettoyage (minutes)",
sectionPlayback: "Politique de lecture",
allowTranscoding: "Autoriser le transcodage",
allowRemuxing: "Autoriser le remuxage",
guestLockdown: "Verrouillage invité",
guestLockdownDesc:
"Masque la navigation principale pour les sessions invitées dans le client web. Ceci est purement cosmétique, ce n'est pas la barrière de sécurité.",
sectionPluginAccess: "Accès aux extensions pour les invités",
blockOtherPlugins:
"Bloquer les autres extensions pour les invités",
blockOtherPluginsDesc:
"Refuse les comptes invités sur les points d'accès API des autres extensions, côté serveur. Un invité possède un vrai jeton Jellyfin : sans cette option, toute extension installée lui répond directement, quoi qu'affiche le client web. L'API propre à Jellyfin reste accessible : le tag de partage la limite déjà au titre partagé, et la lecture en a besoin.",
pluginAccessHint:
'Cochez une extension pour permettre malgré tout aux invités d\'y accéder. Laissez tout décoché sauf si une extension doit servir les invités pendant la lecture, comme un "intro skipper".',
loadingPlugins: "Chargement des extensions…",
noOtherPlugins: "Aucune autre extension installée.",
couldNotLoadPlugins:
"Impossible de charger la liste des extensions.",
sectionShareLinks: "Liens de partage",
refresh: "Actualiser",
cleanupFinished: "Nettoyer les liens terminés",
loading: "Chargement…",
colStatus: "Statut",
colItem: "Élément",
colLink: "Lien",
colGuest: "Invité",
colExpires: "Expire",
colActions: "Actions",
noLinksYet: "Aucun lien chargé pour le moment.",
noActiveLinks: "Aucun lien actif.",
noShareLinks: "Aucun lien de partage.",
save: "Enregistrer",
copy: "Copier",
copied: "Copié",
open: "Ouvrir",
revoke: "Révoquer",
confirmRevoke: "Confirmer la révocation",
notAvailable: "n/d",
loadingShareLinks: "Chargement des liens de partage…",
couldNotLoadLinks: "Impossible de charger les liens de partage.",
loadFailed: "Échec du chargement.",
cleaningUp: "Nettoyage en cours…",
cleanupFailed: "Échec du nettoyage.",
linkCountOne: "1 lien chargé.",
linkCountOther: "{count} liens chargés.",
removedCountOne: "1 lien terminé supprimé.",
removedCountOther: "{count} liens terminés supprimés.",
},
};
function t(key, replacements) {
var lang = isFrench() ? "fr" : "en";
var value =
(STRINGS[lang] && STRINGS[lang][key]) || STRINGS.en[key] || key;
if (replacements) {
Object.keys(replacements).forEach(function (name) {
value = value.replace("{" + name + "}", replacements[name]);
});
}
return value;
}
function applyStaticTranslations() {
Array.prototype.forEach.call(
page.querySelectorAll("[data-i18n]"),
function (node) {
node.textContent = t(node.getAttribute("data-i18n"));
}
);
Array.prototype.forEach.call(
page.querySelectorAll("[data-i18n-label]"),
function (node) {
node.setAttribute(
"label",
t(node.getAttribute("data-i18n-label"))
);
}
);
}
function loadConfig() {
Dashboard.showLoadingMsg();
return ApiClient.getPluginConfiguration(ShareLinksPluginId)
.then(function (cfg) {
page.querySelector("#Enabled").checked = cfg.Enabled !== false;
page.querySelector("#DefaultExpiryHours").value =
cfg.DefaultExpiryHours || 24;
page.querySelector("#MaxExpiryHours").value =
cfg.MaxExpiryHours || 720;
page.querySelector("#PublicBaseUrlOverride").value =
cfg.PublicBaseUrlOverride || "";
page.querySelector("#GuestUsernamePrefix").value =
cfg.GuestUsernamePrefix || "share-";
page.querySelector("#GuestHiddenSelectors").value =
cfg.GuestHiddenSelectors != null
? cfg.GuestHiddenSelectors
: "";
page.querySelector("#AllowTranscoding").checked =
cfg.AllowTranscoding !== false;
page.querySelector("#AllowRemuxing").checked =
cfg.AllowRemuxing !== false;
page.querySelector("#CleanupIntervalMinutes").value =
cfg.CleanupIntervalMinutes || 60;
page.querySelector("#OneUseDefault").checked =
cfg.OneUseDefault !== false;
page.querySelector("#MaxConcurrentViewers").value =
cfg.MaxConcurrentViewers === undefined
? 10
: cfg.MaxConcurrentViewers;
page.querySelector("#GuestModeLockdownEnabled").checked =
cfg.GuestModeLockdownEnabled !== false;
page.querySelector("#GuestPluginApiGuardEnabled").checked =
cfg.GuestPluginApiGuardEnabled !== false;
})
.finally(function () {
Dashboard.hideLoadingMsg();
});
}
// The ticked state comes from the server rather than from the config we
// just loaded, so a plugin that has since been uninstalled drops out of
// the list instead of lingering as a stale checkbox.
function loadPlugins() {
var host = page.querySelector("#GuestAllowedPlugins");
return ApiClient.ajax({
type: "GET",
url: ApiClient.getUrl("ShareLinks/Admin/Plugins"),
dataType: "json",
})
.then(function (list) {
var items = Array.isArray(list) ? list : [];
if (!items.length) {
host.innerHTML =
'<div class="sl-muted">' + t("noOtherPlugins") + "</div>";
return;
}
host.innerHTML = items
.map(function (plugin) {
return (
'<div class="checkboxContainer">' +
"<label>" +
'<input is="emby-checkbox" type="checkbox" class="sl-plugin-allow" data-plugin-id="' +
escapeHtml(plugin.Id) +
'"' +
(plugin.AllowedForGuests ? " checked" : "") +
" />" +
"<span>" +
escapeHtml(plugin.Name) +
"</span>" +
"</label>" +
"</div>"
);
})
.join("");
})
.catch(function () {
host.innerHTML =
'<div class="sl-muted">' +
t("couldNotLoadPlugins") +
"</div>";
});
}
function collectAllowedPluginIds() {
// A failed plugin load leaves no checkboxes to read. Returning the saved
// config untouched in that case avoids silently clearing the exceptions.
var boxes = page.querySelectorAll(".sl-plugin-allow");
if (!boxes.length) {
return null;
}
return Array.prototype.filter
.call(boxes, function (box) {
return box.checked;
})
.map(function (box) {
return box.getAttribute("data-plugin-id");
});
}
function fmtDate(value) {
if (!value) {
return t("notAvailable");
}
return new Date(value).toLocaleString();
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function renderLinks(list) {
var body = page.querySelector("#LinksBody");
var items = Array.isArray(list)
? list.slice().sort(function (a, b) {
return (
new Date(b.CreatedAtUtc || b.CreatedAt || 0) -
new Date(a.CreatedAtUtc || a.CreatedAt || 0)
);
})
: [];
if (!items.length) {
body.innerHTML =
'<tr><td colspan="6" class="sl-muted">' +
t("noActiveLinks") +
"</td></tr>";
page.querySelector("#LinksStatus").textContent = t(
"noShareLinks"
);
return;
}
body.innerHTML = items
.map(function (record) {
var status = record.Status || "Unknown";
var itemName = escapeHtml(
record.ItemNameSnapshot || record.ItemId || ""
);
var guestName = escapeHtml(
record.GuestUserName || t("notAvailable")
);
var expires = fmtDate(record.ExpiresAtUtc || record.ExpiresAt);
var shareUrl = record.ShareUrl
? escapeHtml(record.ShareUrl)
: "";
var linkCell = shareUrl
? '<button is="emby-button" type="button" class="raised" data-copy="' +
shareUrl +
'"><span>' +
t("copy") +
"</span></button> " +
'<a href="' +
shareUrl +
'" target="_blank" rel="noopener">' +
t("open") +
"</a>"
: '<span class="sl-muted">' + t("notAvailable") + "</span>";
return [
"<tr>",
"<td>",
escapeHtml(status),
"</td>",
"<td>",
"<div><strong>",
itemName,
"</strong></div>",
"</td>",
"<td>",
linkCell,
"</td>",
"<td>",
guestName,
"</td>",
"<td>",
escapeHtml(expires),
"</td>",
'<td class="sl-right">',
'<button is="emby-button" type="button" class="raised" data-id="',
escapeHtml(record.Id),
'">',
"<span>",
t("revoke"),
"</span>",
"</button>",
"</td>",
"</tr>",
].join("");
})
.join("");
Array.from(body.querySelectorAll("button[data-id]")).forEach(
function (button) {
button.addEventListener("click", function () {
revokeLink(button.getAttribute("data-id"));
});
}
);
Array.from(body.querySelectorAll("button[data-copy]")).forEach(
function (button) {
button.addEventListener("click", function () {
copyShareUrl(button);
});
}
);
page.querySelector("#LinksStatus").textContent =
items.length === 1
? t("linkCountOne")
: t("linkCountOther", { count: items.length });
}
function loadLinks() {
var status = page.querySelector("#LinksStatus");
status.textContent = t("loadingShareLinks");
return ApiClient.ajax({
type: "GET",
url: ApiClient.getUrl("ShareLinks/Admin/List"),
dataType: "json",
})
.then(function (list) {
renderLinks(list || []);
})
.catch(function (error) {
status.textContent = t("couldNotLoadLinks");
page.querySelector("#LinksBody").innerHTML =
'<tr><td colspan="6" class="sl-muted">' +
escapeHtml(
error && error.message ? error.message : t("loadFailed")
) +
"</td></tr>";
});
}
function copyShareUrl(button) {
var url = button.getAttribute("data-copy");
if (!url) {
return;
}
copyText(url).then(function () {
var span = button.querySelector("span");
if (!span) {
return;
}
span.textContent = t("copied");
window.setTimeout(function () {
span.textContent = t("copy");
}, 2000);
});
}
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).catch(function () {
return fallbackCopyText(text);
});
}
return fallbackCopyText(text);
}
function fallbackCopyText(text) {
var textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "readonly");
textarea.style.position = "fixed";
textarea.style.top = "-1000px";
textarea.style.left = "-1000px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
} catch (error) {
// Best effort only.
}
textarea.remove();
return Promise.resolve();
}
function revokeLink(id) {
if (!id) {
return;
}
var button = page.querySelector('button[data-id="' + id + '"]');
if (button && button.getAttribute("data-confirm") !== "1") {
button.setAttribute("data-confirm", "1");
button.querySelector("span").textContent = t("confirmRevoke");
window.setTimeout(function () {
if (
button.isConnected &&
button.getAttribute("data-confirm") === "1"
) {
button.removeAttribute("data-confirm");
button.querySelector("span").textContent = t("revoke");
}
}, 3500);
return;
}
Dashboard.showLoadingMsg();
ApiClient.ajax({
type: "POST",
url: ApiClient.getUrl("ShareLinks/Admin/Revoke/" + id),
dataType: "json",
})
.then(function () {
return loadLinks();
})
.finally(function () {
Dashboard.hideLoadingMsg();
});
}
function save(e) {
e.preventDefault();
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(ShareLinksPluginId).then(function (
cfg
) {
cfg.Enabled = page.querySelector("#Enabled").checked;
cfg.DefaultExpiryHours =
parseInt(page.querySelector("#DefaultExpiryHours").value, 10) ||
24;
cfg.MaxExpiryHours =
parseInt(page.querySelector("#MaxExpiryHours").value, 10) ||
720;
cfg.PublicBaseUrlOverride = page
.querySelector("#PublicBaseUrlOverride")
.value.trim();
cfg.GuestUsernamePrefix =
page.querySelector("#GuestUsernamePrefix").value.trim() ||
"share-";
cfg.GuestHiddenSelectors = page.querySelector(
"#GuestHiddenSelectors"
).value;
cfg.AllowTranscoding = page.querySelector(
"#AllowTranscoding"
).checked;
cfg.AllowRemuxing = page.querySelector("#AllowRemuxing").checked;
cfg.CleanupIntervalMinutes =
parseInt(
page.querySelector("#CleanupIntervalMinutes").value,
10
) || 60;
cfg.OneUseDefault = page.querySelector("#OneUseDefault").checked;
cfg.MaxConcurrentViewers = Math.max(
parseInt(
page.querySelector("#MaxConcurrentViewers").value,
10
) || 0,
0
);
cfg.GuestModeLockdownEnabled = page.querySelector(
"#GuestModeLockdownEnabled"
).checked;
cfg.GuestPluginApiGuardEnabled = page.querySelector(
"#GuestPluginApiGuardEnabled"
).checked;
var allowedPluginIds = collectAllowedPluginIds();
if (allowedPluginIds !== null) {
cfg.GuestAllowedPluginIds = allowedPluginIds;
}
ApiClient.updatePluginConfiguration(ShareLinksPluginId, cfg)
.then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
return loadLinks();
})
.finally(function () {
Dashboard.hideLoadingMsg();
});
});
return false;
}
document
.querySelector("#ShareLinksConfigPage")
.addEventListener("pageshow", function () {
page = this;
applyStaticTranslations();
loadConfig().then(loadPlugins).then(loadLinks);
});
document
.querySelector("#ShareLinksConfigForm")
.addEventListener("submit", save);
document
.querySelector("#RefreshLinks")
.addEventListener("click", function () {
loadLinks();
});
document
.querySelector("#CleanupLinks")
.addEventListener("click", function () {
var button = page.querySelector("#CleanupLinks");
var status = page.querySelector("#LinksStatus");
button.disabled = true;
status.textContent = t("cleaningUp");
ApiClient.ajax({
type: "POST",
url: ApiClient.getUrl("ShareLinks/Admin/Cleanup"),
dataType: "json",
})
.then(function (result) {
var removed =
result &&
(result.removed !== undefined
? result.removed
: result.Removed);
removed = parseInt(removed, 10) || 0;
return loadLinks().then(function () {
page.querySelector("#LinksStatus").textContent =
removed === 1
? t("removedCountOne")
: t("removedCountOther", { count: removed });
});
})
.catch(function () {
status.textContent = t("cleanupFailed");
})
.finally(function () {
button.disabled = false;
});
});
})();
</script>
</div>
</body>
</html>