mirror of
https://github.com/wizarrrr/wizarr.git
synced 2026-07-31 07:17:10 -04:00
560 lines
38 KiB
HTML
560 lines
38 KiB
HTML
<div class="relative modal-container"
|
|
aria-labelledby="modal-title"
|
|
role="dialog"
|
|
aria-modal="true">
|
|
<div class="fixed inset-0 modal-backdrop transition-opacity"></div>
|
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
|
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
|
<div id="inviteModalContent"
|
|
class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4 dark:bg-gray-800">
|
|
<div class="flex justify-between items-center pb-3">
|
|
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
|
{% if not link %}
|
|
{{ _("Create Invitation") }}
|
|
{% else %}
|
|
{{ _("Invitation Created") }}
|
|
{% endif %}
|
|
</h1>
|
|
<button onclick="closeInviteModal()"
|
|
type="button"
|
|
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
|
<svg class="w-3 h-3"
|
|
aria-hidden="true"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 14 14">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
|
</svg>
|
|
<span class="sr-only">Close modal</span>
|
|
</button>
|
|
</div>
|
|
{% if not link %}
|
|
{% if error_message %}
|
|
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-red-800 dark:text-red-200"
|
|
role="alert">{{ error_message }}</div>
|
|
{% endif %}
|
|
<form class="space-y-4 md:space-y-6"
|
|
hx-post="/invite"
|
|
hx-target="#inviteModalContent"
|
|
hx-swap="innerHTML"
|
|
onsubmit="return validateServerSelection()">
|
|
<!-- Invite Code -->
|
|
<div>
|
|
<label for="code"
|
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ _("Invite Code String") }}
|
|
<span class="text-gray-500 ml-1">({{ _("optional") }})</span>
|
|
</label>
|
|
<input minlength="6"
|
|
maxlength="10"
|
|
type="text"
|
|
name="code"
|
|
id="code"
|
|
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary dark:focus:border-primary"
|
|
placeholder="{{ _('E.g. XMFGEJIKNQ') }}">
|
|
</div>
|
|
<!-- Expiration Dropdown -->
|
|
<div>
|
|
<label for="expires"
|
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400">
|
|
{{ _("Expiration") }}
|
|
</label>
|
|
<select id="expires"
|
|
name="expires"
|
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary dark:focus:border-primary">
|
|
<option value="day">{{ _("1 day") }}</option>
|
|
<option value="week">{{ _("1 week") }}</option>
|
|
<option value="month">{{ _("1 month") }}</option>
|
|
<option value="never">{{ _("Never") }}</option>
|
|
</select>
|
|
</div>
|
|
{# Wizard bundle selection #}
|
|
{% if bundles %}
|
|
<div>
|
|
<label for="wizard_bundle_id"
|
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400">
|
|
{{ _("Wizard bundle") }}
|
|
</label>
|
|
<select id="wizard_bundle_id"
|
|
name="wizard_bundle_id"
|
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary dark:focus:border-primary">
|
|
<option value="">{{ _("Automatic") }}</option>
|
|
{% for b in bundles %}<option value="{{ b.id }}">{{ b.name }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% endif %}
|
|
<!-- Advanced Options Toggle -->
|
|
<div>
|
|
<a href="#"
|
|
class="block mb-2 text-sm font-medium text-secondary dark:text-primary cursor-pointer"
|
|
onclick="event.preventDefault(); showAdvanced();">{{ _("Advanced Options") }}</a>
|
|
</div>
|
|
<!-- ADVANCED OPTIONS CONTAINER -->
|
|
<div id="advanced"
|
|
class="hidden space-y-6 border-t border-gray-200 pt-6 dark:border-gray-700">
|
|
<!-- Invite Settings Section -->
|
|
<div class="space-y-4">
|
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200">{{ _("Invite Settings") }}</h2>
|
|
<!-- Unlimited Usage -->
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="unlimited"
|
|
name="unlimited"
|
|
type="checkbox"
|
|
value="true"
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Unlimited Usages") }}</span>
|
|
</label>
|
|
<!-- Duration (in days) -->
|
|
<div>
|
|
<label for="duration"
|
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ _("Duration") }}
|
|
<span class="text-gray-500 ml-1">({{ _("in days") }})</span>
|
|
<button data-popover-target="popover-description"
|
|
data-popover-placement="bottom-end"
|
|
type="button"
|
|
class="ml-1">
|
|
<svg class="w-4 h-4 text-gray-400 hover:text-gray-500"
|
|
aria-hidden="true"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd">
|
|
</path>
|
|
</svg>
|
|
<span class="sr-only">{{ _("Show Information") }}</span>
|
|
</button>
|
|
</label>
|
|
<div data-popover
|
|
id="popover-description"
|
|
role="tooltip"
|
|
class="absolute z-10 invisible inline-block text-sm font-light text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-xs opacity-0 w-72 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
|
|
<div class="p-3 space-y-2">
|
|
<h3 class="font-semibold text-gray-900 dark:text-white">{{ _("Duration") }}</h3>
|
|
<p>{{ _("When set, invited user(s) will be removed from the Plex server after a number of days. (1-999)") }}</p>
|
|
</div>
|
|
<div data-popper-arrow></div>
|
|
</div>
|
|
<input id="duration"
|
|
name="duration"
|
|
type="number"
|
|
min="1"
|
|
max="999"
|
|
class="mt-1 bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary dark:focus:border-primary">
|
|
</div>
|
|
</div>
|
|
<!-- LDAP User Creation Section -->
|
|
{% if ldap_enabled %}
|
|
<div class="space-y-4 mt-6 border-t border-gray-200 pt-6 dark:border-gray-700"
|
|
x-data="{ createLdapUser: true }">
|
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200">{{ _("LDAP Integration") }}</h2>
|
|
<!-- Create LDAP User Toggle -->
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="create_ldap_user"
|
|
name="create_ldap_user"
|
|
type="checkbox"
|
|
value="true"
|
|
checked
|
|
x-model="createLdapUser"
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Create LDAP User") }}</span>
|
|
</label>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 -mt-2">
|
|
{{ _("Automatically create an LDAP user when this invitation is accepted") }}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<!-- Server Selection OUTSIDE advanced -->
|
|
<div class="space-y-2 mt-6">
|
|
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ _("Target servers") }}</label>
|
|
<div id="server-grid" class="flex flex-col space-y-4">
|
|
{% for s in servers %}
|
|
<div class="relative flex flex-col items-start w-full rounded-lg border bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 p-4 shadow-sm transition hover:border-primary hover:shadow peer-checked:border-primary peer-checked:ring-2 peer-checked:ring-primary focus-within:ring-2 focus-within:ring-primary">
|
|
<input type="checkbox"
|
|
class="sr-only peer"
|
|
id="srv{{ s.id }}"
|
|
name="server_ids"
|
|
value="{{ s.id }}"
|
|
{% if s.id == chosen_server_id %}checked{% endif %}
|
|
onchange="updatePrimaryServer(); handleServerToggle(this, {{ s.id }})">
|
|
<label for="srv{{ s.id }}" class="w-full grow cursor-pointer">
|
|
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ s.name }}</p>
|
|
{{ s.server_type|server_type_tag }}
|
|
</label>
|
|
<svg class="absolute top-2 right-2 w-5 h-5 text-primary opacity-0 peer-checked:opacity-100"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414L8.414 15l-4.121-4.12a1 1 0 011.414-1.415L8.414 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd">
|
|
</path>
|
|
</svg>
|
|
<!-- Media-specific options & library picker -->
|
|
<div class="w-full mt-4 space-y-2 hidden peer-checked:block">
|
|
{% if s.server_type == "plex" %}
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="plex_home_{{ s.id }}"
|
|
name="plex_home"
|
|
type="checkbox"
|
|
value="true"
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Invite to Plex Home") }}</span>
|
|
</label>
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_downloads_{{ s.id }}"
|
|
name="allow_downloads"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_downloads %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Downloads") }}</span>
|
|
</label>
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_live_tv_{{ s.id }}"
|
|
name="allow_live_tv"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_live_tv %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Live TV Access") }}</span>
|
|
</label>
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_mobile_uploads_{{ s.id }}"
|
|
name="allow_mobile_uploads"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_mobile_uploads %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Mobile Uploads") }}</span>
|
|
</label>
|
|
{% elif s.server_type == "jellyfin" %}
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_downloads_{{ s.id }}"
|
|
name="allow_downloads"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_downloads %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Downloads") }}</span>
|
|
</label>
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_live_tv_{{ s.id }}"
|
|
name="allow_live_tv"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_live_tv %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Live TV Access") }}</span>
|
|
</label>
|
|
<!-- Max Active Sessions - ID is server-specific to prevent duplicate DOM IDs,
|
|
but name is shared since max_active_sessions is an invitation-level setting.
|
|
When multiple Jellyfin servers are selected, the first value is used. -->
|
|
<div>
|
|
<label for="max_active_sessions_{{ s.id }}"
|
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ _("Max Active Sessions") }}
|
|
<span class="text-gray-500 ml-1">({{ _("simultaneous devices") }})</span>
|
|
</label>
|
|
<input id="max_active_sessions_{{ s.id }}"
|
|
name="max_active_sessions"
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
placeholder="0 = unlimited"
|
|
class="mt-1 bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary dark:focus:border-primary">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{{ _("Number of devices that can stream simultaneously. Set to 0 for unlimited.") }}
|
|
</p>
|
|
</div>
|
|
{% elif s.server_type == "emby" %}
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_downloads_{{ s.id }}"
|
|
name="allow_downloads"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_downloads %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Downloads") }}</span>
|
|
</label>
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_live_tv_{{ s.id }}"
|
|
name="allow_live_tv"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_live_tv %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Live TV Access") }}</span>
|
|
</label>
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_mobile_uploads_{{ s.id }}"
|
|
name="allow_mobile_uploads"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_mobile_uploads %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Mobile Uploads") }}</span>
|
|
</label>
|
|
{% elif s.server_type == "audiobookshelf" %}
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_downloads_{{ s.id }}"
|
|
name="allow_downloads"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_downloads %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Downloads") }}</span>
|
|
</label>
|
|
{% elif s.server_type == "komga" %}
|
|
<label class="flex items-center cursor-pointer">
|
|
<input id="allow_downloads_{{ s.id }}"
|
|
name="allow_downloads"
|
|
type="checkbox"
|
|
value="true"
|
|
{% if s.allow_downloads %}checked{% endif %}
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary">
|
|
</div>
|
|
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Downloads") }}</span>
|
|
</label>
|
|
{% endif %}
|
|
<!-- Library dropdown-search -->
|
|
<div class="mt-3 w-full relative">
|
|
<button id="libBtn{{ s.id }}"
|
|
type="button"
|
|
class="text-white bg-primary hover:bg-primary_hover focus:ring-4 focus:outline-none focus:ring-primary_hover font-medium rounded-lg text-xs px-3 py-2 inline-flex items-center"
|
|
aria-controls="libMenu{{ s.id }}"
|
|
aria-expanded="false"
|
|
hx-post="/invite/scan-libraries"
|
|
hx-vals='{"server_ids":"{{ s.id }}"}'
|
|
hx-target="#libMenu{{ s.id }} .dropdown-content"
|
|
hx-swap="innerHTML"
|
|
hx-trigger="click[!isLibraryDropdownLoaded('libMenu{{ s.id }}')]"
|
|
onclick="event.stopPropagation(); toggleDropdown('libMenu{{ s.id }}')">
|
|
{{ _("Select Libraries") }}
|
|
<svg class="w-2.5 h-2.5 ms-2"
|
|
aria-hidden="true"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 10 6">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4" />
|
|
</svg>
|
|
</button>
|
|
<!-- Dropdown menu -->
|
|
<div id="libMenu{{ s.id }}"
|
|
class="z-10 hidden mt-2 bg-white rounded-lg shadow-lg w-60 dark:bg-gray-700"
|
|
onclick="event.stopPropagation()">
|
|
<div class="dropdown-content p-3 text-sm text-gray-500 dark:text-gray-300">{{ _("Loading…") }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-gray-800">
|
|
<button type="submit"
|
|
class="inline-flex w-full justify-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-primary_hover sm:ml-3 sm:w-auto">
|
|
{{ _("Create Invitation") }}
|
|
</button>
|
|
<button onclick="closeInviteModal()"
|
|
type="button"
|
|
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600">
|
|
{{ _("Cancel") }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{% endif %}
|
|
{% if link %}
|
|
<div class="space-y-4">
|
|
<div class="text-center">
|
|
<svg class="w-16 h-16 mx-auto mb-4 text-green-500"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
</svg>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
|
{{ _("Successfully generated an Invitation!") }}
|
|
</h3>
|
|
</div>
|
|
<div>
|
|
<label for="link"
|
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ _("Invitation Link") }}
|
|
</label>
|
|
<div class="flex items-center space-x-2">
|
|
<input readonly
|
|
type="text"
|
|
name="link"
|
|
id="link"
|
|
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
value="{{ link }}">
|
|
<button onclick="copyLink()"
|
|
type="button"
|
|
class="text-white bg-primary hover:bg-primary_hover focus:ring-4 focus:outline-hidden focus:ring-amber-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center dark:bg-primary dark:hover:bg-primary_hover dark:focus:ring-primary_hover">
|
|
{{ _("Copy") }}
|
|
</button>
|
|
</div>
|
|
<p id="successfully_copied"
|
|
class="mt-2 text-sm text-green-600 dark:text-green-500"
|
|
hidden>{{ _("Link has been copied to clipboard!") }}</p>
|
|
</div>
|
|
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-gray-800">
|
|
<button onclick="createAnother()"
|
|
type="button"
|
|
class="inline-flex w-full justify-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-primary_hover sm:ml-3 sm:w-auto">
|
|
{{ _("Create Another") }}
|
|
</button>
|
|
<button onclick="closeInviteModal()"
|
|
type="button"
|
|
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600">
|
|
{{ _("Close") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function copyLink() {
|
|
const copyText = document.getElementById("link");
|
|
copyToClipboard(copyText.value, (success) => {
|
|
if (success) {
|
|
document.getElementById("successfully_copied").hidden = false;
|
|
} else {
|
|
// Fallback already shows alert, but we can add additional feedback here if needed
|
|
console.error("Copy failed");
|
|
}
|
|
});
|
|
}
|
|
|
|
function showAdvanced() {
|
|
const adv = document.getElementById("advanced");
|
|
adv.classList.toggle("hidden");
|
|
}
|
|
|
|
function updatePrimaryServer() {
|
|
// This function is no longer needed since we removed the hidden server_id field
|
|
// Keeping it for now to avoid breaking existing checkbox onchange handlers
|
|
}
|
|
|
|
function handleServerToggle(checkbox, serverId) {
|
|
// When a server is unchecked, clear its library dropdown to prevent duplicate submissions
|
|
if (!checkbox.checked) {
|
|
const dropdown = document.getElementById(`libMenu${serverId}`);
|
|
const dropdownContent = dropdown?.querySelector('.dropdown-content');
|
|
if (dropdownContent) {
|
|
// Clear the dropdown content to remove any library checkboxes
|
|
dropdownContent.innerHTML = {{ _("Loading…") | tojson | safe }};
|
|
}
|
|
if (dropdown) {
|
|
dropdown.classList.add('hidden');
|
|
delete dropdown.dataset.loaded;
|
|
}
|
|
const button = document.getElementById(`libBtn${serverId}`);
|
|
button?.setAttribute('aria-expanded', 'false');
|
|
}
|
|
}
|
|
|
|
function validateServerSelection() {
|
|
const checkboxes = document.querySelectorAll('#server-grid input[name="server_ids"]');
|
|
const isAnySelected = Array.from(checkboxes).some(checkbox => checkbox.checked);
|
|
|
|
if (!isAnySelected) {
|
|
alert({{ _("Please select at least one server for the invitation.") | tojson | safe }});
|
|
return false;
|
|
}
|
|
|
|
// Disable library checkboxes for unselected servers to prevent them from being submitted
|
|
checkboxes.forEach(checkbox => {
|
|
const serverId = checkbox.value;
|
|
const libraryCheckboxes = document.querySelectorAll(`#libMenu${serverId} input[name="libraries"]`);
|
|
libraryCheckboxes.forEach(libCheckbox => {
|
|
libCheckbox.disabled = !checkbox.checked;
|
|
});
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
function toggleDropdown(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.classList.toggle('hidden');
|
|
const button = document.querySelector(`[aria-controls="${id}"]`);
|
|
button?.setAttribute('aria-expanded', String(!el.classList.contains('hidden')));
|
|
}
|
|
}
|
|
|
|
function isLibraryDropdownLoaded(id) {
|
|
return document.getElementById(id)?.dataset.loaded === 'true';
|
|
}
|
|
|
|
function createAnother() {
|
|
// Reload the invite form
|
|
htmx.ajax('GET', '/invite', {
|
|
target: '#inviteModalContent',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
|
|
function closeInviteModal() {
|
|
document.getElementById('invite-modal').innerHTML = '';
|
|
}
|
|
|
|
// Handle successful form submission in modal context
|
|
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
|
if (evt.detail.target.classList?.contains('dropdown-content')) {
|
|
const dropdown = evt.detail.target.closest('[id^="libMenu"]');
|
|
if (dropdown) {
|
|
dropdown.dataset.loaded = 'true';
|
|
}
|
|
}
|
|
|
|
if (evt.detail.target.id === 'inviteModalContent') {
|
|
// Check if this is the success view
|
|
const link = document.getElementById('link');
|
|
if (link) {
|
|
// Refresh the invitations table on the main page
|
|
htmx.ajax('POST', '/invite/table', {
|
|
target: '#invite_table',
|
|
swap: 'outerHTML',
|
|
values: {
|
|
server: document.getElementById('invite_server_filter')?.value || ''
|
|
}
|
|
});
|
|
|
|
// Removed auto-close to allow users ample time to copy the link manually
|
|
}
|
|
}
|
|
});
|
|
</script>
|