Files
wizarr/app/templates/admin/user_modal.html

271 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all w-full max-w-2xl dark:bg-gray-800"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
x-data="{ activeTab: 'tab-{{ related_users[0].id }}' }">
<div class="bg-white dark:bg-gray-800">
<!-- Header -->
<div class="flex justify-between items-center px-4 pt-5 sm:px-6 sm:pt-6 pb-3">
<h2 id="modal-title"
class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
{{ _("Edit User") }} {{ user.username or user.email }}
</h2>
<button 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"
onclick="document.getElementById('modal').classList.add('hidden')"
aria-label="{{ _('Close modal') }}">
<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>
<!-- Tab Navigation -->
<div class="border-b border-gray-200 dark:border-gray-700 px-4 sm:px-6">
<nav class="flex -mb-px space-x-2 overflow-x-auto"
aria-label="{{ _('Tabs') }}"
role="tablist">
{% for related_user in related_users %}
<button type="button"
role="tab"
:aria-selected="activeTab === 'tab-{{ related_user.id }}'"
@click="activeTab = 'tab-{{ related_user.id }}'"
:class="activeTab === 'tab-{{ related_user.id }}' ? 'border-primary text-primary dark:border-primary dark:text-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'"
class="whitespace-nowrap py-3 px-4 border-b-2 font-medium text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition-colors">
{% if related_user.server %}
{{ related_user.server.name }}
{% else %}
{{ _("Local") }}
{% endif %}
</button>
{% endfor %}
<button type="button"
role="tab"
:aria-selected="activeTab === 'tab-notes'"
@click="activeTab = 'tab-notes'"
:class="activeTab === 'tab-notes' ? 'border-primary text-primary dark:border-primary dark:text-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'"
class="whitespace-nowrap py-3 px-4 border-b-2 font-medium text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition-colors">
{{ _("Notes") }}
</button>
</nav>
</div>
<!-- Tab Content -->
<div class="px-4 pb-4 pt-4 sm:p-6 sm:pt-4">
{% for related_user in related_users %}
<div x-show="activeTab === 'tab-{{ related_user.id }}'"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
role="tabpanel"
:aria-hidden="activeTab !== 'tab-{{ related_user.id }}'">
<!-- Server Badge (for multi-server setups) -->
{% if related_users|length > 1 %}
<div class="flex items-center justify-between mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center">
{% if related_user.server %}
{{ related_user.server.server_type|server_name_tag(related_user.server.name) }}
{% else %}
{{ 'local'|server_type_tag }}
{% endif %}
</div>
<span class="text-xs text-gray-500 dark:text-gray-400">ID: {{ related_user.id }}</span>
</div>
{% endif %}
<!-- Permission Management Section (only for supported servers) -->
{% if related_user.server and related_user.server.server_type in ['plex', 'jellyfin', 'emby', 'audiobookshelf'] %}
<div class="mb-6">
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Permission Management") }}</h3>
<!-- Permission Toggles -->
<div class="space-y-3 bg-gray-50 dark:bg-gray-900 rounded-lg p-4"
x-data="{ downloading: {{ (related_user.allow_downloads if related_user.allow_downloads is not none else false)|tojson }}, liveTv: {{ (related_user.allow_live_tv if related_user.allow_live_tv is not none else false)|tojson }}, cameraUpload: {{ (related_user.allow_camera_upload if related_user.allow_camera_upload is not none else false)|tojson }}, updating: { downloads: false, liveTv: false, camera: false }, togglePermission(type, formRef) { const originalValue = this[type]; this.updating[type === 'downloading' ? 'downloads' : (type === 'liveTv' ? 'liveTv' : 'camera')] = true; this[type] = !this[type]; this.$nextTick(() => htmx.trigger(formRef, 'submit')); }, showError(message) { alert('Error: ' + message); } }">
<!-- Downloads Permission (All servers support this) -->
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ _("Allow Downloads") }}</label>
<button type="button"
@click="togglePermission('downloading', $refs.downloadForm)"
:disabled="updating.downloads"
:class="[ downloading ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700', updating.downloads ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer' ]"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-900"
aria-label="{{ _('Toggle downloads permission') }}">
<span :class="downloading ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
<form x-ref="downloadForm"
method="post"
action="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
hx-post="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
hx-swap="none"
@htmx:after-request="updating.downloads = false"
@htmx:response-error="downloading = !downloading; updating.downloads = false; showError($event.detail.xhr.responseText || 'Failed to update permission')"
class="hidden">
<input type="hidden" name="permission_type" value="allow_downloads">
<input type="hidden" name="enabled" :value="downloading ? 'true' : 'false'">
</form>
</div>
<!-- Live TV Permission (Plex, Jellyfin, Emby only) -->
{% if related_user.server and related_user.server.server_type in ['plex', 'jellyfin', 'emby'] %}
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ _("Allow Live TV") }}</label>
<button type="button"
@click="togglePermission('liveTv', $refs.liveTvForm)"
:disabled="updating.liveTv"
:class="[ liveTv ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700', updating.liveTv ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer' ]"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-900"
aria-label="{{ _('Toggle live TV permission') }}">
<span :class="liveTv ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
<form x-ref="liveTvForm"
method="post"
action="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
hx-post="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
hx-swap="none"
@htmx:after-request="updating.liveTv = false"
@htmx:response-error="liveTv = !liveTv; updating.liveTv = false; showError($event.detail.xhr.responseText || 'Failed to update permission')"
class="hidden">
<input type="hidden" name="permission_type" value="allow_live_tv">
<input type="hidden" name="enabled" :value="liveTv ? 'true' : 'false'">
</form>
</div>
{% endif %}
<!-- Camera Upload Permission (Plex only) -->
{% if related_user.server and related_user.server.server_type == 'plex' %}
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ _("Allow Camera Upload") }}</label>
<button type="button"
@click="togglePermission('cameraUpload', $refs.cameraForm)"
:disabled="updating.camera"
:class="[ cameraUpload ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700', updating.camera ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer' ]"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-all duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-900"
aria-label="{{ _('Toggle camera upload permission') }}">
<span :class="cameraUpload ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
<form x-ref="cameraForm"
method="post"
action="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
hx-post="{{ url_for('admin.update_user_permissions', db_id=related_user.id) }}"
hx-swap="none"
@htmx:after-request="updating.camera = false"
@htmx:response-error="cameraUpload = !cameraUpload; updating.camera = false; showError($event.detail.xhr.responseText || 'Failed to update permission')"
class="hidden">
<input type="hidden" name="permission_type" value="allow_camera_upload">
<input type="hidden" name="enabled" :value="cameraUpload ? 'true' : 'false'">
</form>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Library Access Section -->
{% set lib_data = user_libraries_map.get(related_user.id, {}) %}
{% set libraries = lib_data.get('libraries', []) %}
{% set accessible = lib_data.get('accessible_libraries', []) %}
<div class="mb-6">
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Library Access") }}</h3>
{% if libraries|length == 0 %}
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 text-sm text-gray-500 dark:text-gray-400">
<p>{{ _("No libraries available") }}</p>
<p class="text-xs mt-1">{{ _("Scan libraries from the invitation page to enable library restrictions") }}</p>
</div>
{% else %}
<div class="relative" x-data="{ updating: false }">
<form id="library-form-{{ related_user.id }}"
@htmx:before-request="updating = true"
@htmx:after-request="updating = false"
:class="updating ? 'opacity-60 pointer-events-none' : ''"
class="transition-opacity duration-200">
<ul class="h-48 px-3 py-2 overflow-y-auto text-sm text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-900">
{% for library in libraries %}
<li>
<label class="flex items-center ps-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer py-2"
:class="updating ? 'cursor-not-allowed' : 'cursor-pointer'">
<input type="checkbox"
name="library_ids[]"
value="{{ library.id }}"
{% if accessible|length == 0 or library.name in accessible %}checked{% endif %}
hx-post="{{ url_for('admin.update_user_libraries', db_id=related_user.id) }}"
hx-trigger="change"
hx-include="#library-form-{{ related_user.id }}"
hx-swap="none"
:disabled="updating"
class="w-4 h-4 text-primary bg-gray-100 border-gray-300 rounded focus:ring-primary dark:focus:ring-primary dark:ring-offset-gray-900 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<span class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ library.name }}</span>
</label>
</li>
{% endfor %}
</ul>
</form>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
{{ _("Click to toggle library access. Updates save automatically.") }}
</p>
{% endif %}
</div>
<!-- Expiry Management Section -->
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Expiry Management") }}</h3>
<form hx-post="{{ url_for('admin.user_detail', db_id=related_user.id) }}"
class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<div class="flex items-end gap-3">
<div class="flex-1">
<label for="expires_{{ related_user.id }}"
class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ _("Expiry date") }}
</label>
<input id="expires_{{ related_user.id }}"
name="expires"
type="datetime-local"
value="{{ related_user.expires|default('', true) and related_user.expires.strftime('%Y-%m-%dT%H:%M') or '' }}"
class="bg-white 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-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" />
</div>
<button type="submit"
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary_hover focus:ring-4 focus:outline-none focus:ring-primary_focus dark:bg-primary dark:hover:bg-primary_hover dark:focus:ring-primary_focus">
{{ _("Update") }}
</button>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{% if related_user.expires %}
{{ _("Current expiry") }}: {{ related_user.expires|human_date }}
{% else %}
{{ _("No expiry set") }}
{% endif %}
</p>
</form>
</div>
</div>
{% endfor %}
<!-- Notes Tab Content -->
<div x-show="activeTab === 'tab-notes'"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
role="tabpanel"
:aria-hidden="activeTab !== 'tab-notes'">
<h3 class="text-base font-semibold text-gray-900 dark:text-white mb-4">{{ _("Notes") }}</h3>
<form hx-post="{{ url_for('admin.user_detail', db_id=user.id) }}"
class="space-y-4">
<div>
<label for="notes" class="sr-only">{{ _("User notes") }}</label>
<textarea id="notes"
name="notes"
rows="6"
placeholder="{{ _('Add notes about this user...') }}"
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-3 dark:bg-gray-900 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">{{ user.notes|default('', true) }}</textarea>
</div>
<div class="flex justify-end">
<button type="submit"
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary_hover focus:ring-4 focus:outline-none focus:ring-primary_focus dark:bg-primary dark:hover:bg-primary_hover dark:focus:ring-primary_focus">
{{ _("Update Notes") }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>