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:
1 parent
f8d13457fa
commit
66fd78c10f
314 files changed
+16507
-3172
No files matched your search
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user