Add multi-language audio tracks and self-hosted flag-icons

Introduce per-video language support and multiple audio tracks
(VideoAudioTrack model + migrations for language, description, title),
a reusable language-select component, and a track-editor form. Bundle
the self-hosted flag-icons v7.2.3 library and a NAS auto-sync command.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-05-22 21:32:52 +03:00
1 parent f8d13457fa
commit 66fd78c10f
314 files changed
+16507 -3172

No files matched your search

@@ -16,7 +16,7 @@
$uid = 'csd_' . ($id ?? $name) . '_' . substr(md5(uniqid()), 0, 8);
$inputId = $id ?? $name;
$selFlag = '🌐';
$selFlag = '';
$selLabel = $placeholder;
if ($value) {
foreach ($options as $opt) {
@@ -47,14 +47,15 @@
}
.csd-btn:hover, .csd-btn:focus-visible { border-color: var(--brand-red, #e61e1e); }
.csd-btn[aria-expanded="true"] { border-color: var(--brand-red, #e61e1e); }
.csd-ico { font-size: 18px; line-height: 1; flex-shrink: 0; user-select: none; }
.csd-ico { font-size: 18px; line-height: 1; flex-shrink: 0; user-select: none; display: flex; align-items: center; }
.csd-ico .fi, .csd-opt-ico .fi { width: 22px; height: 16px; border-radius: 2px; display: inline-block; flex-shrink: 0; }
.csd-val { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.csd-val.ph { color: var(--text-secondary, #aaa); }
.csd-sub { font-size: 12px; color: var(--text-secondary, #aaa); white-space: nowrap; flex-shrink: 0; }
.csd-arr { flex-shrink: 0; font-size: 11px; color: var(--text-secondary, #aaa); margin-left: 4px; transition: transform .2s; }
.csd-btn[aria-expanded="true"] .csd-arr { transform: rotate(180deg); }
.csd-panel {
position: fixed; z-index: 1060; min-width: 220px;
position: fixed; z-index: 1080; min-width: 220px;
background: var(--bg-secondary, #1e1e1e); border: 1px solid var(--border-color, #303030);
border-radius: 10px; box-shadow: 0 12px 32px rgba(0,0,0,.55); overflow: hidden;
}
@@ -161,7 +162,7 @@
_move(i) { const all=[...this.list.querySelectorAll('.csd-opt:not([hidden])')]; if(all[i]){all[i].tabIndex=0;all[i].focus();} }
_pick(opt) {
this.hidden.value = opt.dataset.v;
this.icoEl.textContent = opt.querySelector('.csd-opt-ico').textContent;
this.icoEl.innerHTML = opt.querySelector('.csd-opt-ico').innerHTML;
this.valEl.textContent = opt.querySelector('.csd-opt-main').textContent;
this.valEl.classList.remove('ph');
if (this.subEl) { const s=opt.querySelector('.csd-opt-sub'); this.subEl.textContent=s?s.textContent:''; }
@@ -189,7 +190,7 @@
aria-haspopup="listbox"
aria-expanded="false"
aria-label="{{ $label ?? 'Select country' }}">
<span class="csd-ico">{{ $selFlag }}</span>
<span class="csd-ico"><span class="fi fi-{{ $selFlag ?: 'xx' }}"></span></span>
<span class="csd-val{{ $isPlaceholder ? ' ph' : '' }}">{{ $selLabel }}</span>
<i class="bi bi-chevron-down csd-arr"></i>
</button>
@@ -207,7 +208,7 @@
data-v="{{ $opt['value'] }}"
data-s="{{ $opt['search'] }}"
aria-selected="{{ $value === $opt['value'] ? 'true' : 'false' }}">
<span class="csd-opt-ico">{{ $opt['flag'] }}</span>
<span class="csd-opt-ico"><span class="fi fi-{{ $opt['flag'] }}"></span></span>
<span class="csd-opt-main">{{ $opt['label'] }}</span>
</li>
@endforeach