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

+11 -17
View File
@@ -481,7 +481,7 @@
@foreach($viewsByCountry as $i => $row)
<div class="country-row clickable-seg" style="cursor:pointer" onclick="openDashModal('Viewers from {{ addslashes($row->country_name ?? $row->country ?? 'Unknown') }}','country_viewers',{country:'{{ $row->country }}'},'bi-globe2')">
<div class="country-rank">{{ $i + 1 }}</div>
<div class="country-flag" title="{{ $row->country }}">{{ $row->country ? countryCodeToFlag($row->country) : '🌍' }}</div>
<div class="country-flag" title="{{ $row->country }}">{!! $row->country ? countryCodeToFlag($row->country) : countryCodeToFlag('xx') !!}</div>
<div class="country-name">{{ $row->country_name ?? 'Unknown' }}</div>
<div class="country-bar-wrap">
<div class="country-bar" style="width:{{ round(($row->total / $maxViews) * 100) }}%;"></div>
@@ -677,13 +677,9 @@
@php
function countryCodeToFlag(string $code): string {
$code = strtoupper(trim($code));
if (strlen($code) !== 2) return '🌍';
$chars = '';
foreach (str_split($code) as $c) {
$chars .= mb_chr(0x1F1E6 + ord($c) - ord('A'));
}
return $chars;
$iso = strtolower(trim($code));
$safe = (strlen($iso) === 2 && ctype_alpha($iso)) ? $iso : 'xx';
return '<span class="fi fi-' . $safe . '" style="width:20px;height:15px;border-radius:2px;display:inline-block;vertical-align:middle;"></span>';
}
@endphp
@@ -755,22 +751,20 @@ new Chart(document.getElementById('typeChart'), {
const canvas = document.getElementById('countryChart');
const container = canvas.parentElement; // position:relative — flags overlay here
// Overlay real <img> flags — canvas ctx cannot render emoji flag sequences reliably.
// yAxis.right = left edge of bar area; flags sit in the ticks.padding gap before the bars.
// Overlay flag spans using local flag-icons SVGs — works on all browsers, no CDN.
function placeFlagOverlays() {
container.querySelectorAll('.cty-flag').forEach(e => e.remove());
const yAxis = chart.scales.y;
if (!yAxis) return;
const fW = 22, fH = 15;
// Place flag just to the left of the bar area (inside the ticks.padding gap)
const x = Math.round(yAxis.right - fW - 4);
countryData.forEach((row, i) => {
if (!row.country || row.country.length !== 2) return;
const yPx = Math.round(yAxis.getPixelForTick(i));
const img = document.createElement('img');
img.src = `https://flagcdn.com/w40/${row.country.toLowerCase()}.png`;
img.className = 'cty-flag';
img.style.cssText = [
const code = row.country.toLowerCase();
const el = document.createElement('span');
el.className = `cty-flag fi fi-${code}`;
el.style.cssText = [
'position:absolute',
`width:${fW}px`,
`height:${fH}px`,
@@ -778,10 +772,10 @@ new Chart(document.getElementById('typeChart'), {
`left:${x}px`,
'pointer-events:none',
'border-radius:3px',
'object-fit:cover',
'box-shadow:0 1px 3px rgba(0,0,0,.4)',
'background-size:cover',
].join(';');
container.appendChild(img);
container.appendChild(el);
});
}
+1
View File
@@ -9,6 +9,7 @@
<link rel="icon" type="image/png" href="{{ asset('storage/images/logo.png') }}">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ asset('vendor/flag-icons/css/flag-icons.min.css') }}">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
@@ -5,11 +5,9 @@
@php
function flagEmoji(string $code): string {
$chars = '';
foreach (str_split(strtoupper($code)) as $c) {
$chars .= mb_chr(0x1F1E6 + ord($c) - ord('A'));
}
return $chars;
$iso = strtolower(trim($code));
$safe = (strlen($iso) === 2 && ctype_alpha($iso)) ? $iso : 'xx';
return '<span class="fi fi-' . $safe . '" style="width:20px;height:15px;border-radius:2px;display:inline-block;vertical-align:middle;"></span>';
}
@endphp
@@ -295,7 +293,7 @@ function flagEmoji(string $code): string {
@forelse($viewsByCountry as $i => $row)
<div class="country-row">
<span class="country-rank">{{ $i + 1 }}</span>
<span class="country-flag">{{ flagEmoji($row->country) }}</span>
<span class="country-flag">{!! flagEmoji($row->country) !!}</span>
<span class="country-name">{{ $row->country_name ?? $row->country }}</span>
<div class="country-bar-wrap">
<div class="country-bar" style="width:{{ round(($row->total / $maxCountry) * 100) }}%"></div>
@@ -424,7 +422,7 @@ function flagEmoji(string $code): string {
</td>
<td>
@if($view->country)
<span style="font-size:16px;">{{ flagEmoji($view->country) }}</span>
{!! flagEmoji($view->country) !!}
<span style="font-size:13px; margin-left:4px;">{{ $view->country_name ?? $view->country }}</span>
@else
<span style="color:var(--text-secondary); font-size:12px;">Unknown</span>