Files
2026-07-30 16:34:14 +03:00

275 lines
19 KiB
Twig
Raw Permalink 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.
{% extends '@adminx/main.twig' %}
{% block title %}Медиа{% endblock %}
{% block content %}
<nav class="breadcrumbs" aria-label="Хлебные крошки">
<a href="{{ ADMINX_BASE }}/">Главная</a><i class="ti ti-chevron-right"></i>
<span>Медиа</span>
</nav>
<div class="page-header">
<div class="between">
<div>
<h1>Медиа</h1>
<p class="text-secondary" style="margin-top:5px">Файловый браузер каталога uploads</p>
</div>
{% if can_manage %}
<div class="cluster">
<button class="btn btn-secondary" type="button" data-media-presets-open data-open-drawer="mediaPresetsDrawer"><i class="ti ti-aspect-ratio"></i>Виды изображений</button>
<button class="btn btn-secondary" type="button" data-media-folder><i class="ti ti-folder-plus"></i>Папка</button>
<button class="btn btn-danger-soft" type="button" data-media-clear-thumbs data-path="{{ dir }}" data-name="{{ dir }}" data-tooltip="Удалить превью в текущей и вложенных папках"><i class="ti ti-photo-off"></i>Очистить превью</button>
<button class="btn btn-primary" type="button" data-media-upload-trigger><i class="ti ti-upload"></i>Загрузить</button>
</div>
{% endif %}
</div>
</div>
<aside class="drawer drawer-right drawer-lg" id="mediaPresetsDrawer" role="dialog" aria-modal="true" aria-labelledby="mediaPresetsTitle" hidden>
<div class="drawer-header">
<div>
<h3 id="mediaPresetsTitle">Виды изображений</h3>
<p class="text-secondary text-sm">Готовые размеры для подгонки картинок — новости, баннеры, галерея. Выбираются в редакторе изображения.</p>
</div>
<button class="btn btn-ghost btn-icon" type="button" data-close-drawer aria-label="Закрыть"><i class="ti ti-x"></i></button>
</div>
<div class="drawer-body">
<div class="media-presets-list" data-preset-list><div class="empty-state">Загрузка...</div></div>
{% if can_manage %}
<form id="mediaPresetForm" class="form-grid media-presets-form" style="margin-top:16px">
<input type="hidden" name="id" value="">
<label class="field col-12"><span class="field-label">Название</span><input class="input" name="title" placeholder="Баннер шапки" required><span class="field-error" data-error="title"></span></label>
<label class="field col-12"><span class="field-label">Раздел (необязательно)</span><input class="input" name="group_label" placeholder="Новости, Баннеры, Галерея"></label>
<label class="field col-6"><span class="field-label">Режим</span><select class="select" name="mode" data-preset-modes></select><span class="field-error" data-error="mode"></span></label>
<label class="field col-6"><span class="field-label">Формат</span><select class="select" name="format" data-preset-formats></select><span class="field-error" data-error="format"></span></label>
<label class="field col-4"><span class="field-label">Ширина, px</span><input class="input" type="number" name="width" min="0" max="2400" value="0"><span class="field-error" data-error="width"></span></label>
<label class="field col-4"><span class="field-label">Высота, px</span><input class="input" type="number" name="height" min="0" max="2400" value="0"><span class="field-error" data-error="height"></span></label>
<label class="field col-4"><span class="field-label">Качество</span><input class="input" type="number" name="quality" min="1" max="100" value="82"><span class="field-error" data-error="quality"></span></label>
<label class="field col-12 media-preset-check"><input type="checkbox" name="webp_twin" value="1" checked> <span>Создавать WebP рядом с результатом (webp + исходный формат для fallback)</span></label>
</form>
{% endif %}
</div>
{% if can_manage %}
<div class="drawer-footer">
<button class="btn btn-ghost" type="button" data-close-drawer><i class="ti ti-x"></i>Закрыть</button>
<button class="btn btn-ghost" type="button" data-preset-reset style="margin-left:auto"><i class="ti ti-plus"></i>Новый</button>
<button class="btn btn-primary" type="button" data-preset-save><i class="ti ti-check"></i>Сохранить</button>
</div>
{% endif %}
</aside>
<div class="media-summary">
<div class="card media-stat">
<span class="icon-tile" style="--tile-bg:#e8f3ff;--tile-fg:#2563eb"><i class="ti ti-files"></i></span>
<div><b>{{ stats.files }}</b><span>Файлов</span></div>
</div>
<div class="card media-stat">
<span class="icon-tile" style="--tile-bg:#ecfdf3;--tile-fg:#16a34a"><i class="ti ti-photo"></i></span>
<div><b>{{ stats.images }}</b><span>Изображений</span></div>
</div>
<div class="card media-stat">
<span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-folders"></i></span>
<div><b>{{ stats.folders }}</b><span>Папок</span></div>
</div>
<div class="card media-stat">
<span class="icon-tile" style="--tile-bg:#f3e8ff;--tile-fg:#7e22ce"><i class="ti ti-database"></i></span>
<div><b>{{ stats.size_label }}</b><span>Объём</span></div>
</div>
</div>
<section class="tab-panel active media-browser">
<div class="section-header media-panel-header">
<div class="section-icon"><i class="ti ti-folder-open"></i></div>
<div>
<div class="section-eyebrow">Файлы</div>
<h2>{{ dir }}</h2>
<p class="section-desc">Папки и файлы физически читаются из каталога uploads.</p>
</div>
<div class="section-header-right">
<div class="media-crumbs" aria-label="Путь">
{% for crumb in breadcrumbs %}
{% if loop.last %}
<span>{{ crumb.title }}</span>
{% else %}
<a href="{{ ADMINX_BASE }}/media?dir={{ crumb.path|url_encode }}&view={{ filters.view }}">{{ crumb.title }}</a><i class="ti ti-chevron-right"></i>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<div class="card media-card">
<div class="media-section-head">
<div class="media-section-title">
<span class="icon-tile media-head-icon" style="--tile-bg:#e0f2fe;--tile-fg:#0284c7"><i class="ti ti-layout-grid"></i></span>
<div>
<h2>Содержимое папки</h2>
<p class="text-secondary">{{ folders|length }} папок, {{ total }} файлов в текущем фильтре.</p>
</div>
</div>
<span class="badge badge-blue">{{ filters.view == 'grid' ? 'Сетка' : 'Список' }}</span>
</div>
<div class="table-toolbar media-toolbar">
<form class="media-filter" method="get" action="{{ ADMINX_BASE }}/media">
<input type="hidden" name="dir" value="{{ dir }}">
<input type="hidden" name="view" value="{{ filters.view }}">
<div class="input-wrap">
<i class="ti ti-search"></i>
<input class="input" type="search" name="q" value="{{ filters.q }}" placeholder="Поиск по имени">
</div>
<select class="select" name="type">
<option value="">Все типы</option>
<option value="image"{{ filters.type == 'image' ? ' selected' : '' }}>Изображения</option>
<option value="file"{{ filters.type == 'file' ? ' selected' : '' }}>Файлы</option>
</select>
<button class="btn btn-secondary" type="submit"><i class="ti ti-filter"></i>Показать</button>
{% if filters.q or filters.type %}
<a class="btn btn-ghost" href="{{ ADMINX_BASE }}/media?dir={{ dir|url_encode }}&view={{ filters.view }}"><i class="ti ti-x"></i>Сбросить</a>
{% endif %}
</form>
<div class="segmented media-view" role="group" aria-label="Вид">
<a href="{{ ADMINX_BASE }}/media?dir={{ dir|url_encode }}&view=grid&q={{ filters.q|url_encode }}&type={{ filters.type }}" aria-pressed="{{ filters.view == 'grid' ? 'true' : 'false' }}" data-tooltip="Сетка" aria-label="Сетка"><i class="ti ti-layout-grid"></i></a>
<a href="{{ ADMINX_BASE }}/media?dir={{ dir|url_encode }}&view=table&q={{ filters.q|url_encode }}&type={{ filters.type }}" aria-pressed="{{ filters.view == 'table' ? 'true' : 'false' }}" data-tooltip="Список" aria-label="Список"><i class="ti ti-list"></i></a>
</div>
</div>
{% if can_manage %}
<form id="mediaUploadForm" class="media-upload" method="post" action="{{ ADMINX_BASE }}/media/upload" enctype="multipart/form-data" data-dir="{{ dir }}">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="hidden" name="dir" value="{{ dir }}">
<input type="file" name="files[]" multiple hidden>
<button class="dropzone media-dropzone" type="button" data-media-drop>
<i class="ti ti-cloud-upload"></i>
<span><b>Перетащите файлы сюда</b><small>или нажмите для выбора на компьютере</small></span>
<small data-media-upload-status></small>
</button>
</form>
{% endif %}
{% if folders is empty and files is empty and not parent_dir %}
<div class="error-screen media-empty">
<div class="error-ic"><i class="ti ti-folder-open"></i></div>
<h4>В uploads пусто</h4>
<p class="text-sm text-secondary">Создайте папку или загрузите файлы.</p>
</div>
{% elseif filters.view == 'grid' %}
<div class="media-grid">
{% if parent_dir %}
<a class="media-tile is-folder is-parent" href="{{ ADMINX_BASE }}/media?dir={{ parent_dir|url_encode }}&view=grid&q={{ filters.q|url_encode }}&type={{ filters.type }}">
<span class="media-thumb"><i class="ti ti-arrow-up"></i></span>
<b>..</b><small>Вверх</small>
</a>
{% endif %}
{% for folder in folders %}
<article class="media-tile is-folder">
<a class="media-thumb" href="{{ ADMINX_BASE }}/media?dir={{ folder.path|url_encode }}&view=grid&q={{ filters.q|url_encode }}&type={{ filters.type }}"><i class="ti ti-folder"></i></a>
<a class="media-name" href="{{ ADMINX_BASE }}/media?dir={{ folder.path|url_encode }}&view=grid&q={{ filters.q|url_encode }}&type={{ filters.type }}">{{ folder.name }}</a>
<small>{% if filters.q %}{{ folder.path }} · {% endif %}{{ folder.count }} объектов</small>
{% if can_manage %}
<div class="media-actions">
<button class="btn btn-ghost btn-icon btn-sm media-action media-action-thumbs" type="button" data-media-clear-thumbs data-path="{{ folder.path }}" data-name="{{ folder.name }}" data-tooltip="Очистить превью во вложенных папках" aria-label="Очистить превью"><i class="ti ti-photo-off"></i></button>
<button class="btn btn-ghost btn-icon btn-sm media-action media-action-edit" type="button" data-media-rename data-path="{{ folder.path }}" data-name="{{ folder.name }}" data-kind="folder" data-tooltip="Переименовать" aria-label="Переименовать"><i class="ti ti-pencil"></i></button>
<button class="btn btn-ghost btn-icon btn-sm media-action media-action-danger" type="button" data-media-delete data-path="{{ folder.path }}" data-name="{{ folder.name }}" data-kind="folder" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>
</div>
{% endif %}
</article>
{% endfor %}
{% for f in files %}
<article class="media-tile is-file">
{% if f.is_image %}
<button class="media-thumb" type="button" data-media-preview="{{ f.url }}" data-media-preview-name="{{ f.name }}" data-media-preview-meta="{{ f.meta ?: f.extension }}" aria-label="Предпросмотр {{ f.name }}">
<img src="{{ f.thumb_url ?: f.url }}" alt="">
</button>
{% else %}
<a class="media-thumb" href="{{ ADMINX_BASE }}/media/file?path={{ f.path|url_encode }}">
<i class="ti ti-file"></i>
</a>
{% endif %}
<a class="media-name" href="{{ ADMINX_BASE }}/media/file?path={{ f.path|url_encode }}">{{ f.name }}</a>
<small>{{ f.meta ?: f.extension }}</small>
<div class="media-actions">
<a class="btn btn-ghost btn-icon btn-sm media-action media-action-info" href="{{ ADMINX_BASE }}/media/file?path={{ f.path|url_encode }}" data-tooltip="Детали" aria-label="Детали"><i class="ti ti-info-circle"></i></a>
<button class="btn btn-ghost btn-icon btn-sm media-action media-action-copy" type="button" data-copy="{{ f.path }}" data-tooltip="Копировать путь" aria-label="Копировать путь"><i class="ti ti-copy"></i></button>
{% if can_manage %}
<button class="btn btn-ghost btn-icon btn-sm media-action media-action-danger" type="button" data-media-delete data-path="{{ f.path }}" data-name="{{ f.name }}" data-kind="file" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>
{% endif %}
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="table-scroll">
<table class="table table-compact media-table">
<colgroup><col class="media-col-name"><col class="media-col-type"><col class="media-col-size"><col class="media-col-date"><col class="media-col-actions"></colgroup>
<thead><tr><th>Название</th><th>Тип</th><th>Размер</th><th>Изменён</th><th></th></tr></thead>
<tbody>
{% if parent_dir %}
<tr>
<td><a class="media-row-name" href="{{ ADMINX_BASE }}/media?dir={{ parent_dir|url_encode }}&view=table&q={{ filters.q|url_encode }}&type={{ filters.type }}"><span class="media-row-icon"><i class="ti ti-arrow-up"></i></span><b>..</b></a></td>
<td>Папка</td><td>-</td><td>-</td><td></td>
</tr>
{% endif %}
{% for folder in folders %}
<tr>
<td><a class="media-row-name" href="{{ ADMINX_BASE }}/media?dir={{ folder.path|url_encode }}&view=table&q={{ filters.q|url_encode }}&type={{ filters.type }}"><span class="media-row-icon is-folder"><i class="ti ti-folder"></i></span><b>{{ folder.name }}</b></a></td>
<td>Папка</td><td>{% if filters.q %}<span class="media-folder-path">{{ folder.path }}</span>{% else %}{{ folder.count }} объектов{% endif %}</td><td>-</td>
<td>{% if can_manage %}<div class="cluster media-row-actions"><button class="btn btn-ghost btn-icon btn-sm media-action media-action-thumbs" type="button" data-media-clear-thumbs data-path="{{ folder.path }}" data-name="{{ folder.name }}" data-tooltip="Очистить превью во вложенных папках" aria-label="Очистить превью"><i class="ti ti-photo-off"></i></button><button class="btn btn-ghost btn-icon btn-sm media-action media-action-edit" type="button" data-media-rename data-path="{{ folder.path }}" data-name="{{ folder.name }}" data-kind="folder" data-tooltip="Переименовать" aria-label="Переименовать"><i class="ti ti-pencil"></i></button><button class="btn btn-ghost btn-icon btn-sm media-action media-action-danger" type="button" data-media-delete data-path="{{ folder.path }}" data-name="{{ folder.name }}" data-kind="folder" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button></div>{% endif %}</td>
</tr>
{% endfor %}
{% for f in files %}
<tr>
<td><a class="media-row-name" href="{{ ADMINX_BASE }}/media/file?path={{ f.path|url_encode }}"><span class="media-row-preview">{% if f.is_image %}<img src="{{ f.thumb_url ?: f.url }}" alt="" data-media-preview="{{ f.url }}" data-media-preview-name="{{ f.name }}" data-media-preview-meta="{{ f.meta ?: f.extension }}">{% else %}<i class="ti ti-file"></i>{% endif %}</span><b>{{ f.name }}</b></a></td>
<td>{{ f.is_image ? 'Изображение' : 'Файл' }}{% if f.extension %} / {{ f.extension }}{% endif %}</td>
<td class="mono">{{ f.size_label }}</td>
<td class="mono text-muted">{{ f.mtime_label }}</td>
<td><div class="cluster media-row-actions"><a class="btn btn-ghost btn-icon btn-sm media-action media-action-info" href="{{ ADMINX_BASE }}/media/file?path={{ f.path|url_encode }}" data-tooltip="Детали" aria-label="Детали"><i class="ti ti-info-circle"></i></a><button class="btn btn-ghost btn-icon btn-sm media-action media-action-copy" type="button" data-copy="{{ f.path }}" data-tooltip="Копировать путь" aria-label="Копировать путь"><i class="ti ti-copy"></i></button>{% if can_manage %}<button class="btn btn-ghost btn-icon btn-sm media-action media-action-danger" type="button" data-media-delete data-path="{{ f.path }}" data-name="{{ f.name }}" data-kind="file" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>{% endif %}</div></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if pages > 1 %}
<div class="pagination media-pagination">
{% for p in 1..pages %}
{% if p == page %}
<span class="page current">{{ p }}</span>
{% elseif p <= 2 or p > pages - 2 or (p >= page - 2 and p <= page + 2) %}
<a class="page" href="{{ ADMINX_BASE }}/media?page={{ p }}&dir={{ dir|url_encode }}&view={{ filters.view }}&q={{ filters.q|url_encode }}&type={{ filters.type }}">{{ p }}</a>
{% elseif p == 3 or p == pages - 2 %}
<span class="page gap">...</span>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</section>
{% if can_manage %}
<form id="mediaActionForm" method="post">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="hidden" name="path" value="">
<input type="hidden" name="name" value="">
<input type="hidden" name="dir" value="{{ dir }}">
</form>
{% endif %}
<div class="media-lightbox" data-media-lightbox hidden>
<div class="media-lightbox-panel" role="dialog" aria-modal="true" aria-label="Предпросмотр изображения">
<div class="media-lightbox-head">
<div>
<h2 data-media-lightbox-title>Предпросмотр</h2>
<p class="text-secondary" data-media-lightbox-meta></p>
</div>
<button class="btn btn-ghost btn-icon" type="button" data-media-lightbox-close aria-label="Закрыть"><i class="ti ti-x"></i></button>
</div>
<div class="media-lightbox-stage">
<img src="" alt="" data-media-lightbox-image>
</div>
</div>
</div>
{% endblock %}