2026-07-15 07:20:42 +04:00
|
|
|
<app-dialog
|
|
|
|
|
[open]="open()"
|
|
|
|
|
[titleText]="'mediaLibrary.title' | translate"
|
|
|
|
|
size="lg"
|
|
|
|
|
(closed)="requestClose()"
|
|
|
|
|
>
|
|
|
|
|
<div class="media-picker">
|
|
|
|
|
<div class="media-picker__toolbar">
|
|
|
|
|
<app-input
|
|
|
|
|
[ngModel]="facade.search()"
|
|
|
|
|
(ngModelChange)="facade.setSearch($event)"
|
|
|
|
|
[placeholder]="'mediaLibrary.searchPlaceholder' | translate"
|
|
|
|
|
/>
|
feat(builder): implement reusable media library
Media dashboard (real total/images/SVG/logos/unused/storage/alt-coverage, computed from actual assets + a bootstrap usage scan, no fabricated stats); gallery gets grid/list toggle, type filter, sort, drag-and-drop + multi-file upload with cancel/retry and friendly error mapping, lazy thumbnails, multi-select with bulk delete/download/export-metadata; asset details drawer shows real dimensions/size/format/date/usage locations (walks bootstrap config for exact URL matches, reports not-used rather than guessing) plus editable alt text/caption/description/decorative flag with missing-alt warning; shared MediaPickerComponent (already the one reusable picker used by content management) gains type filter, a recent shortcut, and keyboard grid navigation.
2026-07-18 16:09:47 +04:00
|
|
|
<app-select [options]="kindOptions()" [ngModel]="facade.kind() || 'all'" (ngModelChange)="setKind($event)" [ariaLabel]="'mediaLibrary.allTypes' | translate" [fullWidth]="false" />
|
|
|
|
|
<app-button variant="ghost" size="sm" (click)="showRecentOnly()">{{ 'mediaLibrary.sortRecent' | translate }}</app-button>
|
2026-07-15 07:20:42 +04:00
|
|
|
<input
|
|
|
|
|
#fileInput
|
|
|
|
|
type="file"
|
2026-07-15 10:42:23 +04:00
|
|
|
accept="image/jpeg,image/png,image/webp,image/gif,image/svg+xml,application/pdf"
|
2026-07-15 07:20:42 +04:00
|
|
|
hidden
|
|
|
|
|
(change)="onFileSelected($event)"
|
|
|
|
|
/>
|
|
|
|
|
<app-button variant="secondary" [loading]="facade.uploading()" (click)="triggerFileInput()">
|
|
|
|
|
{{ (facade.uploading() ? 'mediaLibrary.uploading' : 'mediaLibrary.upload') | translate }}
|
|
|
|
|
</app-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-15 10:42:23 +04:00
|
|
|
@if (facade.error()) {
|
|
|
|
|
<p class="media-picker__error">{{ facade.error() }}</p>
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 07:20:42 +04:00
|
|
|
@if (facade.loading()) {
|
|
|
|
|
<div class="media-picker__grid">
|
|
|
|
|
@for (i of [1, 2, 3, 4]; track i) {
|
|
|
|
|
<app-skeleton shape="rect" height="110px" />
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
} @else if (facade.items().length === 0) {
|
|
|
|
|
<app-empty-state
|
|
|
|
|
[title]="'mediaLibrary.emptyTitle' | translate"
|
|
|
|
|
[description]="'mediaLibrary.emptyDescription' | translate"
|
|
|
|
|
/>
|
|
|
|
|
} @else {
|
feat(builder): implement reusable media library
Media dashboard (real total/images/SVG/logos/unused/storage/alt-coverage, computed from actual assets + a bootstrap usage scan, no fabricated stats); gallery gets grid/list toggle, type filter, sort, drag-and-drop + multi-file upload with cancel/retry and friendly error mapping, lazy thumbnails, multi-select with bulk delete/download/export-metadata; asset details drawer shows real dimensions/size/format/date/usage locations (walks bootstrap config for exact URL matches, reports not-used rather than guessing) plus editable alt text/caption/description/decorative flag with missing-alt warning; shared MediaPickerComponent (already the one reusable picker used by content management) gains type filter, a recent shortcut, and keyboard grid navigation.
2026-07-18 16:09:47 +04:00
|
|
|
<div class="media-picker__grid" role="listbox" [attr.aria-label]="'mediaLibrary.title' | translate" tabindex="0" (keydown)="onGridKeydown($event)">
|
|
|
|
|
@for (asset of facade.items(); track asset.id; let i = $index) {
|
|
|
|
|
<app-card
|
|
|
|
|
padding="sm"
|
|
|
|
|
[interactive]="true"
|
|
|
|
|
role="option"
|
|
|
|
|
[attr.aria-selected]="i === focusedIndex()"
|
|
|
|
|
[class.media-picker__card--focused]="i === focusedIndex()"
|
|
|
|
|
(click)="pick(asset)"
|
|
|
|
|
>
|
2026-07-15 07:20:42 +04:00
|
|
|
@if (asset.mimeType.startsWith('image/')) {
|
feat(builder): implement reusable media library
Media dashboard (real total/images/SVG/logos/unused/storage/alt-coverage, computed from actual assets + a bootstrap usage scan, no fabricated stats); gallery gets grid/list toggle, type filter, sort, drag-and-drop + multi-file upload with cancel/retry and friendly error mapping, lazy thumbnails, multi-select with bulk delete/download/export-metadata; asset details drawer shows real dimensions/size/format/date/usage locations (walks bootstrap config for exact URL matches, reports not-used rather than guessing) plus editable alt text/caption/description/decorative flag with missing-alt warning; shared MediaPickerComponent (already the one reusable picker used by content management) gains type filter, a recent shortcut, and keyboard grid navigation.
2026-07-18 16:09:47 +04:00
|
|
|
<img class="media-picker__preview" [src]="asset.url" [alt]="asset.filename" loading="lazy" />
|
2026-07-15 07:20:42 +04:00
|
|
|
} @else {
|
|
|
|
|
<div class="media-picker__preview media-picker__preview--file">{{ asset.mimeType }}</div>
|
|
|
|
|
}
|
|
|
|
|
<span class="media-picker__filename">{{ asset.filename }}</span>
|
|
|
|
|
</app-card>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (totalPages() > 1) {
|
|
|
|
|
<app-pagination [currentPage]="facade.page()" [totalPages]="totalPages()" (pageChange)="facade.setPage($event)" />
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</app-dialog>
|