seleccionar-idioma
This commit is contained in:
@@ -172,6 +172,28 @@
|
||||
<!-- Source label -->
|
||||
<span id="sourceLabel" class="text-xs text-gray-600 hidden lg:block flex-shrink-0 truncate max-w-32"></span>
|
||||
|
||||
<!-- Country filter dropdown -->
|
||||
<div class="relative flex-shrink-0" id="countryWrap">
|
||||
<button id="countryBtn" onclick="toggleDropdown('country')"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium
|
||||
text-gray-300 transition-colors hover:text-white"
|
||||
style="background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.1);">
|
||||
<span>🌐</span>
|
||||
<span class="hidden sm:inline">Idiomas</span>
|
||||
<span id="countryBtnCount"
|
||||
class="hidden text-[10px] font-bold px-1.5 py-0.5 rounded-full"
|
||||
style="background:rgba(124,58,237,0.4);color:#c4b5fd"></span>
|
||||
<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="countryMenu"
|
||||
class="dropdown-menu hidden absolute right-0 top-full mt-1.5 rounded-xl overflow-hidden z-50"
|
||||
style="background:#13132a;border:1px solid rgba(255,255,255,0.1);
|
||||
box-shadow:0 16px 48px rgba(0,0,0,0.6);min-width:160px;max-height:320px;overflow-y:auto">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import dropdown -->
|
||||
<div class="relative flex-shrink-0" id="importWrap">
|
||||
<button id="importBtn" onclick="toggleDropdown('import')"
|
||||
@@ -367,6 +389,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status popover (fixed, shared across all mirror rows) -->
|
||||
<div id="statusPopover"
|
||||
class="hidden fixed z-[300] rounded-xl overflow-hidden"
|
||||
style="background:#13132a;border:1px solid rgba(255,255,255,0.1);
|
||||
box-shadow:0 16px 48px rgba(0,0,0,0.6);min-width:160px">
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ TOAST ════════ -->
|
||||
<div id="toast"
|
||||
@@ -403,6 +432,7 @@
|
||||
activeGroup: null,
|
||||
activeSubcategory: null,
|
||||
activeTags: new Set(),
|
||||
activeCountries: new Set(),
|
||||
expandedGroups: new Set(),
|
||||
search: '',
|
||||
|
||||
@@ -410,7 +440,8 @@
|
||||
let ch = this.channels;
|
||||
if (this.activeGroup) ch = ch.filter(c => c.group === this.activeGroup);
|
||||
if (this.activeSubcategory) ch = ch.filter(c => c.subcategory === this.activeSubcategory);
|
||||
if (this.activeTags.size) ch = ch.filter(c => c.tags && c.tags.some(t => this.activeTags.has(t)));
|
||||
if (this.activeTags.size) ch = ch.filter(c => c.tags && c.tags.some(t => this.activeTags.has(t)));
|
||||
if (this.activeCountries.size) ch = ch.filter(c => this.activeCountries.has(c.country_code));
|
||||
const q = this.search.toLowerCase().trim();
|
||||
if (q) ch = ch.filter(c =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
@@ -501,11 +532,58 @@
|
||||
renderSidebar();
|
||||
renderGrid();
|
||||
renderTopTags();
|
||||
renderTopCountries();
|
||||
}
|
||||
|
||||
const CHEVRON_DOWN = `<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`;
|
||||
const CHEVRON_RIGHT = `<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/></svg>`;
|
||||
|
||||
function renderTopCountries() {
|
||||
const codes = [...new Set(App.channels.map(c => c.country_code).filter(Boolean))].sort();
|
||||
const wrap = $('countryWrap');
|
||||
if (!wrap) return;
|
||||
if (codes.length === 0) { wrap.classList.add('hidden'); return; }
|
||||
wrap.classList.remove('hidden');
|
||||
|
||||
const count = App.activeCountries.size;
|
||||
const badge = $('countryBtnCount');
|
||||
if (count > 0) { badge.textContent = count; badge.classList.remove('hidden'); }
|
||||
else { badge.classList.add('hidden'); }
|
||||
|
||||
const allActive = count === 0;
|
||||
$('countryMenu').innerHTML =
|
||||
`<button class="flex items-center gap-2.5 w-full px-4 py-2.5 text-sm
|
||||
text-gray-300 hover:text-white hover:bg-white/5 text-left transition-colors"
|
||||
style="${allActive ? 'background:rgba(124,58,237,0.15)' : ''}"
|
||||
data-country-all>
|
||||
<span class="w-5 text-center">🌐</span>
|
||||
<span class="flex-1">Todos</span>
|
||||
${allActive ? '<span class="text-purple-400 text-xs">✓</span>' : ''}
|
||||
</button>
|
||||
<div style="height:1px;background:rgba(255,255,255,0.06);margin:0 12px"></div>`
|
||||
+ codes.map(code => {
|
||||
const isActive = App.activeCountries.has(code);
|
||||
return `<button class="flex items-center gap-2.5 w-full px-4 py-2 text-sm
|
||||
text-gray-300 hover:text-white hover:bg-white/5 transition-colors"
|
||||
style="${isActive ? 'background:rgba(124,58,237,0.1)' : ''}"
|
||||
data-country="${escAttr(code)}">
|
||||
<img src="flags/${escAttr(code)}.svg"
|
||||
class="w-5 h-3.5 rounded-sm object-cover flex-shrink-0"
|
||||
onerror="this.style.display='none'">
|
||||
<span class="flex-1 text-left font-mono text-xs uppercase">${escHTML(code)}</span>
|
||||
${isActive ? '<span class="text-purple-400 text-xs">✓</span>' : ''}
|
||||
</button>`;
|
||||
}).join('')
|
||||
+ (count > 0
|
||||
? `<div style="height:1px;background:rgba(255,255,255,0.06);margin:0 12px"></div>
|
||||
<button class="flex items-center gap-2 w-full px-4 py-2.5 text-sm
|
||||
text-red-400 hover:text-red-300 hover:bg-white/5 transition-colors"
|
||||
data-country-clear>
|
||||
<span>✕</span><span class="ml-1">Borrar selección</span>
|
||||
</button>`
|
||||
: '');
|
||||
}
|
||||
|
||||
function renderTopTags() {
|
||||
const bar = $('topTagsBar');
|
||||
const allTags = [...new Set(App.channels.flatMap(c => c.tags || []))].sort();
|
||||
@@ -704,7 +782,7 @@
|
||||
const safeUrl = escAttr(`acestream://${m.acestream_hash}`);
|
||||
const hashFull = escHTML(m.acestream_hash);
|
||||
const hashShort = m.acestream_hash.slice(0, 16) + '…';
|
||||
const status = getMirrorStatus(m.acestream_hash);
|
||||
const status = m.status || getMirrorStatus(m.acestream_hash);
|
||||
const { emoji, label, bg } = STATUS_CONFIG[status] || STATUS_CONFIG.unknown;
|
||||
const safeHash = escAttr(m.acestream_hash);
|
||||
const safeChId = escAttr(ch.id);
|
||||
@@ -724,7 +802,7 @@
|
||||
style="background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08)"
|
||||
data-status-hash="${safeHash}"
|
||||
data-channel-id="${safeChId}"
|
||||
data-mirror-idx="${i}">${emoji}</button>
|
||||
data-current-status="${escAttr(status)}">${emoji}</button>
|
||||
<button class="flex-shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-lg
|
||||
text-xs font-semibold transition-all w-20 justify-center"
|
||||
style="background:rgba(124,58,237,0.2);color:var(--accent-light);
|
||||
@@ -752,6 +830,57 @@
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// ── Status popover ───────────────────────────────────────────────────────────
|
||||
|
||||
let _activeStatusBtn = null;
|
||||
|
||||
function openStatusPopover(btn) {
|
||||
_activeStatusBtn = btn;
|
||||
const cur = btn.dataset.currentStatus || 'unknown';
|
||||
$('statusPopover').innerHTML = STATUS_CYCLE.map(s => {
|
||||
const { emoji, label } = STATUS_CONFIG[s];
|
||||
const isActive = s === cur;
|
||||
return `<button class="flex items-center gap-2.5 w-full px-4 py-2.5 text-sm
|
||||
text-gray-300 hover:text-white hover:bg-white/5 text-left transition-colors"
|
||||
style="${isActive ? 'background:rgba(124,58,237,0.15)' : ''}"
|
||||
data-set-status="${escAttr(s)}">
|
||||
<span class="w-5 text-center">${emoji}</span>
|
||||
<span>${escHTML(label)}</span>
|
||||
${isActive ? '<span class="ml-auto text-purple-400 text-xs">✓</span>' : ''}
|
||||
</button>`;
|
||||
}).join('');
|
||||
const pop = $('statusPopover');
|
||||
pop.classList.remove('hidden');
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const popH = pop.offsetHeight || 160;
|
||||
const top = (rect.bottom + 4 + popH > window.innerHeight)
|
||||
? rect.top - popH - 4
|
||||
: rect.bottom + 4;
|
||||
pop.style.top = `${top}px`;
|
||||
pop.style.left = `${rect.left}px`;
|
||||
}
|
||||
|
||||
$('statusPopover').addEventListener('click', e => {
|
||||
const opt = e.target.closest('[data-set-status]');
|
||||
if (!opt || !_activeStatusBtn) return;
|
||||
const next = opt.dataset.setStatus;
|
||||
const btn = _activeStatusBtn;
|
||||
const hash = btn.dataset.statusHash;
|
||||
setMirrorStatus(hash, next);
|
||||
btn.textContent = STATUS_CONFIG[next].emoji;
|
||||
btn.title = STATUS_CONFIG[next].label;
|
||||
btn.dataset.currentStatus = next;
|
||||
btn.closest('.mirror-row').style.background = STATUS_CONFIG[next].bg;
|
||||
const ch = App.channels.find(c => c.id === btn.dataset.channelId);
|
||||
if (ch) { const m = ch.mirrors.find(m => m.acestream_hash === hash); if (m) m.status = next; }
|
||||
fetch('/api/mirror/status', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ channel_id: btn.dataset.channelId, acestream_hash: hash, status: next }),
|
||||
}).catch(() => {});
|
||||
$('statusPopover').classList.add('hidden');
|
||||
_activeStatusBtn = null;
|
||||
});
|
||||
|
||||
// ── Event delegation ─────────────────────────────────────────────────────────
|
||||
|
||||
// Channel grid click → open modal
|
||||
@@ -769,6 +898,7 @@
|
||||
App.activeGroup = null;
|
||||
App.activeSubcategory = null;
|
||||
App.activeTags.clear();
|
||||
App.activeCountries.clear();
|
||||
} else if (btn.dataset.expandGroup !== undefined) {
|
||||
const g = btn.dataset.expandGroup;
|
||||
if (App.expandedGroups.has(g)) App.expandedGroups.delete(g);
|
||||
@@ -786,6 +916,27 @@
|
||||
renderAll();
|
||||
});
|
||||
|
||||
// Country dropdown menu
|
||||
$('countryMenu').addEventListener('click', e => {
|
||||
if (e.target.closest('[data-country-all]')) {
|
||||
App.activeCountries.clear();
|
||||
$('countryMenu').classList.add('hidden');
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('[data-country-clear]')) {
|
||||
App.activeCountries.clear();
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
const btn = e.target.closest('[data-country]');
|
||||
if (!btn) return;
|
||||
const code = btn.dataset.country;
|
||||
if (App.activeCountries.has(code)) App.activeCountries.delete(code);
|
||||
else App.activeCountries.add(code);
|
||||
renderAll();
|
||||
});
|
||||
|
||||
// Top bar tag chips
|
||||
$('topTagsBar').addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-top-tag]');
|
||||
@@ -800,21 +951,7 @@
|
||||
$('modalMirrors').addEventListener('click', e => {
|
||||
const statusBtn = e.target.closest('[data-status-hash]');
|
||||
if (statusBtn) {
|
||||
const hash = statusBtn.dataset.statusHash;
|
||||
const cur = getMirrorStatus(hash);
|
||||
const next = STATUS_CYCLE[(STATUS_CYCLE.indexOf(cur) + 1) % STATUS_CYCLE.length];
|
||||
setMirrorStatus(hash, next);
|
||||
statusBtn.textContent = STATUS_CONFIG[next].emoji;
|
||||
statusBtn.title = STATUS_CONFIG[next].label;
|
||||
fetch('/api/mirror/status', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
channel_id: statusBtn.dataset.channelId,
|
||||
mirror_idx: parseInt(statusBtn.dataset.mirrorIdx),
|
||||
status: next,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
openStatusPopover(statusBtn);
|
||||
return;
|
||||
}
|
||||
const btn = e.target.closest('[data-url]');
|
||||
@@ -838,21 +975,21 @@
|
||||
|
||||
// ── Dropdown menus ───────────────────────────────────────────────────────────
|
||||
function toggleDropdown(which) {
|
||||
const menus = { import: $('importMenu'), export: $('exportMenu') };
|
||||
const other = which === 'import' ? 'export' : 'import';
|
||||
menus[other].classList.add('hidden');
|
||||
const menus = { import: $('importMenu'), export: $('exportMenu'), country: $('countryMenu') };
|
||||
Object.keys(menus).forEach(k => { if (k !== which) menus[k].classList.add('hidden'); });
|
||||
const menu = menus[which];
|
||||
const isHidden = menu.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
menu.classList.remove('hidden');
|
||||
} else {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
if (menu.classList.contains('hidden')) menu.classList.remove('hidden');
|
||||
else menu.classList.add('hidden');
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (!$('importWrap').contains(e.target)) $('importMenu').classList.add('hidden');
|
||||
if (!$('exportWrap').contains(e.target)) $('exportMenu').classList.add('hidden');
|
||||
if (!$('importWrap').contains(e.target)) $('importMenu').classList.add('hidden');
|
||||
if (!$('exportWrap').contains(e.target)) $('exportMenu').classList.add('hidden');
|
||||
if (!$('countryWrap').contains(e.target)) $('countryMenu').classList.add('hidden');
|
||||
if (_activeStatusBtn && !$('statusPopover').contains(e.target) && e.target !== _activeStatusBtn) {
|
||||
$('statusPopover').classList.add('hidden');
|
||||
_activeStatusBtn = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Import ───────────────────────────────────────────────────────────────────
|
||||
@@ -883,6 +1020,7 @@
|
||||
App.activeGroup = null;
|
||||
App.activeSubcategory = null;
|
||||
App.activeTags.clear();
|
||||
App.activeCountries.clear();
|
||||
App.expandedGroups.clear();
|
||||
App.search = '';
|
||||
$('searchInput').value = '';
|
||||
|
||||
Reference in New Issue
Block a user