Files
pulse-signage/src/web/views/settings/media-library/list.hbs
T
lzstealth eb724167bc
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m14s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 41s
Add media filename renaming and preserve library filters
2026-10-17 19:57:03 +01:00

179 lines
12 KiB
Handlebars

<div class="page-header">
<div>
<h2>Media library</h2>
<p>Manage reusable images and videos for slides and templates.</p>
</div>
</div>
<link rel="stylesheet" href="/assets/vendor/cropperjs/cropper.min.css" />
<section class="media-gallery" data-media-gallery-offset="{{assets.length}}" data-media-gallery-has-more="{{#if hasMore}}true{{else}}false{{/if}}">
<div class="media-gallery-toolbar">
{{#if (hasPermission currentUser 'media-library.create')}}
<button type="button" class="btn btn-primary btn-sm text-nowrap" data-media-library-add><i class="bi bi-plus-lg me-1" aria-hidden="true"></i>Add media</button>
{{/if}}
<div class="media-gallery-controls">
<p class="media-gallery-count text-body-secondary small mb-0"><span data-media-gallery-count>{{assetTotal}}</span> item{{#unless (eq assetTotal 1)}}s{{/unless}}</p>
<div class="media-gallery-search d-flex flex-wrap gap-2">
<label class="visually-hidden" for="media-gallery-search-input">Search media</label>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search" aria-hidden="true"></i></span>
<input id="media-gallery-search-input" type="search" class="form-control" placeholder="Search media" autocomplete="off" data-media-gallery-search />
</div>
<label class="visually-hidden" for="media-gallery-type-filter">Filter media type</label>
<select id="media-gallery-type-filter" class="form-select form-select-sm" data-media-gallery-type-filter>
<option value="all">All types</option>
<option value="image">Images</option>
<option value="video">Videos</option>
</select>
<label class="visually-hidden" for="media-gallery-usage-filter">Filter media usage</label>
<select id="media-gallery-usage-filter" class="form-select form-select-sm" data-media-gallery-usage-filter>
<option value="all">All usage</option>
<option value="used">In use</option>
<option value="unused">Unused</option>
</select>
<label class="visually-hidden" for="media-gallery-sort">Sort media</label>
<select id="media-gallery-sort" class="form-select form-select-sm" data-media-gallery-sort>
<option value="date-desc">Newest first</option>
<option value="date-asc">Oldest first</option>
<option value="name-asc">Name A-Z</option>
<option value="name-desc">Name Z-A</option>
<option value="size-desc">Largest first</option>
<option value="size-asc">Smallest first</option>
</select>
{{#if (hasPermission currentUser 'media-library.delete')}}
<button type="button" class="btn btn-secondary btn-sm" data-media-gallery-select-mode aria-pressed="false">Select</button>
<button type="button" class="btn btn-danger btn-sm" data-media-gallery-delete-selected hidden disabled title="Delete selected media">Delete</button>
{{/if}}
</div>
</div>
</div>
<div class="media-gallery-scroll">
<div class="row g-3 g-xl-4">
{{#each assets}}
<div class="col-12 col-sm-6 col-lg-4 col-xl-2" data-media-gallery-column data-media-id="{{id}}" data-media-type="{{media_type}}" data-media-used="{{#if inUse}}true{{else}}false{{/if}}">
<article class="media-gallery-card" data-media-created-at="{{created_at}}" data-media-file-size="{{file_size}}">
<button type="button" class="media-gallery-preview" data-media-gallery-preview data-media-path="{{media_path}}" data-media-type="{{media_type}}" data-media-name="{{original_name}}" aria-label="Preview {{original_name}}">
{{#if (eq media_type 'video')}}
<video src="{{media_path}}" muted playsinline preload="metadata"></video>
<span class="media-gallery-play" aria-hidden="true"><i class="bi bi-play-fill"></i></span>
{{else}}
<img src="{{media_path}}" alt="{{original_name}}" loading="lazy" />
{{/if}}
<span class="media-gallery-type">{{mediaFormat}}</span>
</button>
<div class="media-gallery-details">
<div class="media-gallery-header{{#if (hasPermission ../currentUser 'media-library.delete')}} media-gallery-header-selectable{{/if}}">
<h4 class="media-gallery-name" title="{{original_name}}">{{original_name}}</h4>
{{#if (hasPermission ../currentUser 'media-library.delete')}}
{{#if inUse}}
<button type="button" class="btn btn-outline-secondary btn-sm media-gallery-action" disabled title="Media is in use"><i class="bi bi-lock" aria-hidden="true"></i><span class="visually-hidden">Media is in use</span></button>
{{else}}
<form method="post" action="/media-library/{{id}}/delete" data-confirm-message="Delete this media?" class="media-gallery-action">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Delete media"><i class="bi bi-trash" aria-hidden="true"></i><span class="visually-hidden">Delete media</span></button>
</form>
{{/if}}
{{/if}}
</div>
<div class="media-gallery-meta">{{displayFileSize}} <span aria-hidden="true">&middot;</span> Used {{referenceCount}} time{{#unless (eq referenceCount 1)}}s{{/unless}}</div>
</div>
</article>
</div>
{{/each}}
</div>
<div class="text-center mt-4" data-media-gallery-load-more-wrap hidden>
<button type="button" class="btn btn-primary px-4" data-media-gallery-load-more>Load more...</button>
</div>
</div>
<div class="media-gallery-empty text-body-secondary" data-media-gallery-no-results{{#if assets.length}} hidden{{/if}}>No media matches your search.</div>
</section>
{{#> modal-shell modalId="media-gallery-preview-modal" modalLabelId="media-gallery-preview-modal-label" modalDialogClass="modal-dialog-centered modal-fullscreen-md-down modal-xl"}}
<div class="modal-header">
<h2 class="modal-title fs-5 text-truncate" id="media-gallery-preview-modal-label">Media preview</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body media-gallery-preview-modal-body" data-media-gallery-preview-body></div>
{{#if (hasPermission currentUser 'media-library.update')}}
<form class="modal-footer d-block" data-media-gallery-rename-form>
<label for="media-gallery-filename" class="form-label">Filename</label>
<div class="input-group">
<input id="media-gallery-filename" name="name" type="text" class="form-control" maxlength="255" required data-media-gallery-filename />
<button type="submit" class="btn btn-primary" title="Save filename" data-media-gallery-rename-save><i class="bi bi-floppy" aria-hidden="true"></i><span class="visually-hidden">Save filename</span></button>
</div>
<p class="small mb-0 mt-2" role="status" data-media-gallery-rename-status hidden></p>
</form>
{{/if}}
{{/modal-shell}}
{{#if (hasPermission currentUser 'media-library.create')}}
{{#> modal-shell modalId="media-library-upload-modal" modalLabelId="media-library-upload-modal-label" modalDialogClass="modal-dialog-centered modal-lg"}}
<div class="modal-header">
<h2 class="modal-title fs-5" id="media-library-upload-modal-label">Add media</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="/media-library" enctype="multipart/form-data">
<div class="modal-body">
<label class="slide-image-region-upload-zone media-library-upload-zone" for="media-library-upload" data-media-library-upload-zone>
<input type="file" id="media-library-upload" name="file" class="visually-hidden" accept="{{#each uploadLimits.allowedMimeTypes}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}" data-media-library-upload data-image-max-bytes="{{uploadLimits.imageMaxBytes}}" data-video-max-bytes="{{uploadLimits.videoMaxBytes}}" required />
<span class="slide-image-region-upload-zone-content">
<span class="slide-image-region-upload-zone-icon"><i class="bi bi-cloud-arrow-up" aria-hidden="true"></i></span>
<span class="slide-image-region-upload-zone-copy">
<strong>Drop an image or video here or click to upload</strong>
<span>Images and videos enabled in Media Uploads settings</span>
<span class="slide-image-region-upload-zone-limit">Images max {{uploadLimits.imageMaxLabel}}; videos max {{uploadLimits.videoMaxLabel}}</span>
</span>
</span>
</label>
</div>
</form>
{{/modal-shell}}
{{/if}}
{{#> modal-shell modalId="media-library-crop-modal" modalLabelId="media-library-crop-modal-label" modalDialogClass="modal-dialog-centered modal-xl" modalBackdropStatic=true modalKeyboardDisabled=true}}
<div class="modal-header">
<div>
<h2 class="modal-title fs-5" id="media-library-crop-modal-label">Edit image</h2>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-grid gap-3">
<div class="slide-image-cropper-frame">
<img id="media-library-crop-image" alt="Selected image to crop" />
<div class="slide-image-cropper-loading-overlay" aria-hidden="true">
<div class="spinner-border text-primary slide-image-cropper-loading-spinner" role="status">
<span class="visually-hidden">Loading image editor</span>
</div>
</div>
</div>
<div class="btn-toolbar flex-wrap gap-2 slide-image-cropper-toolbar" role="toolbar" aria-label="Image editing controls">
<div class="btn-group btn-group-sm" role="group" aria-label="Rotate">
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="rotate-left"><i class="bi bi-arrow-counterclockwise" aria-hidden="true"></i> Rotate left</button>
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="rotate-right"><i class="bi bi-arrow-clockwise" aria-hidden="true"></i> Rotate right</button>
</div>
<div class="btn-group btn-group-sm" role="group" aria-label="Flip">
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="flip-horizontal"><i class="bi bi-symmetry-vertical" aria-hidden="true"></i> Flip horizontal</button>
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="flip-vertical"><i class="bi bi-symmetry-horizontal" aria-hidden="true"></i> Flip vertical</button>
</div>
<div class="btn-group btn-group-sm" role="group" aria-label="Crop ratio presets">
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="ratio" data-media-library-crop-ratio="free">Free</button>
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="ratio" data-media-library-crop-ratio="1:1">1:1</button>
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="ratio" data-media-library-crop-ratio="4:3">4:3</button>
<button type="button" class="btn btn-outline-secondary" data-media-library-crop-action="ratio" data-media-library-crop-ratio="16:9">16:9</button>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" data-media-library-crop-action="reset"><i class="bi bi-arrow-repeat" aria-hidden="true"></i> Reset</button>
</div>
<p class="small text-body-secondary mb-0" id="media-library-crop-status"></p>
</div>
<div class="modal-footer justify-content-end">
<div class="btn-group" role="group" aria-label="Crop actions">
<button type="button" class="btn btn-primary" data-media-library-crop-apply>Apply crop</button>
<button type="button" class="btn btn-warning" data-media-library-crop-cancel>Cancel</button>
</div>
</div>
{{/modal-shell}}
<script src="/assets/vendor/cropperjs/cropper.min.js"></script>
<script src="/assets/js/shared/upload-utils.js?v={{appVersion}}"></script>
<script type="module" src="/assets/js/settings/media-library.js?v={{appVersion}}"></script>