added manual matching on anime
This commit is contained in:
@@ -96,7 +96,7 @@ export async function searchInExtension(req: any, reply: FastifyReply) {
|
|||||||
|
|
||||||
export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) {
|
export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { animeId, episode, server, category, ext, source } = req.query;
|
const { animeId, episode, server, category, ext, source, extensionAnimeId } = req.query;
|
||||||
|
|
||||||
const extension = getExtension(ext);
|
const extension = getExtension(ext);
|
||||||
if (!extension) return { error: "Extension not found" };
|
if (!extension) return { error: "Extension not found" };
|
||||||
@@ -107,7 +107,8 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl
|
|||||||
animeId,
|
animeId,
|
||||||
source,
|
source,
|
||||||
server,
|
server,
|
||||||
category
|
category,
|
||||||
|
extensionAnimeId
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
|
|||||||
@@ -276,7 +276,6 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string):
|
|||||||
if (!ext) return { error: "not found" };
|
if (!ext) return { error: "not found" };
|
||||||
|
|
||||||
const extName = ext.constructor.name;
|
const extName = ext.constructor.name;
|
||||||
|
|
||||||
const cached = await getCachedExtension(extName, id);
|
const cached = await getCachedExtension(extName, id);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
try {
|
try {
|
||||||
@@ -341,6 +340,7 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string
|
|||||||
averageScore: m.rating || m.score || null,
|
averageScore: m.rating || m.score || null,
|
||||||
format: 'ANIME',
|
format: 'ANIME',
|
||||||
seasonYear: null,
|
seasonYear: null,
|
||||||
|
url: m.url,
|
||||||
isExtensionResult: true,
|
isExtensionResult: true,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -387,22 +387,41 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str
|
|||||||
startDate: { year: 0, month: 0, day: 0 }
|
startDate: { year: 0, month: 0, day: 0 }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!matches || matches.length === 0) return [];
|
if (!matches || matches.length === 0) return [];
|
||||||
|
|
||||||
const res = matches[0];
|
const normalizedQuery = normalize(query);
|
||||||
|
const scored = matches.map(match => {
|
||||||
|
const normalizedTitle = normalize(match.title);
|
||||||
|
const score = similarity(normalizedQuery, normalizedTitle);
|
||||||
|
|
||||||
|
let bonus = 0;
|
||||||
|
if (normalizedTitle === normalizedQuery) {
|
||||||
|
bonus = 0.5;
|
||||||
|
} else if (normalizedTitle.toLowerCase().includes(normalizedQuery.toLowerCase())) {
|
||||||
|
bonus = 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalScore = score + bonus;
|
||||||
|
|
||||||
|
return {
|
||||||
|
match,
|
||||||
|
score: finalScore
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
scored.sort((a, b) => b.score - a.score);
|
||||||
|
const bestMatches = scored.filter(s => s.score > 0.4);
|
||||||
|
|
||||||
|
if (bestMatches.length === 0) return [];
|
||||||
|
const res = bestMatches[0].match;
|
||||||
if (!res?.id) return [];
|
if (!res?.id) return [];
|
||||||
|
|
||||||
mediaId = res.id;
|
mediaId = res.id;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
mediaId = query;
|
mediaId = query;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chapterList = await ext.findEpisodes(mediaId);
|
const chapterList = await ext.findEpisodes(mediaId);
|
||||||
|
|
||||||
if (!Array.isArray(chapterList)) return [];
|
if (!Array.isArray(chapterList)) return [];
|
||||||
|
|
||||||
const result: Episode[] = chapterList.map(ep => ({
|
const result: Episode[] = chapterList.map(ep => ({
|
||||||
id: ep.id,
|
id: ep.id,
|
||||||
number: ep.number,
|
number: ep.number,
|
||||||
@@ -421,11 +440,11 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string): Promise<StreamData> {
|
export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string, extensionAnimeId?: string): Promise<StreamData> {
|
||||||
const providerName = extension.constructor.name;
|
const providerName = extension.constructor.name;
|
||||||
|
|
||||||
const cacheKey = `anime:stream:${providerName}:${id}:${episode}:${server || 'default'}:${category || 'sub'}`;
|
const cacheKey = `anime:stream:${providerName}:${id}:${episode}:${server || 'default'}:${category || 'sub'}`;
|
||||||
|
if (!extensionAnimeId) {
|
||||||
const cached = await getCache(cacheKey);
|
const cached = await getCache(cacheKey);
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
@@ -438,8 +457,7 @@ export async function getStreamData(extension: Extension, episode: string, id: s
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[${providerName}] Error parsing cached stream data:`, e);
|
console.error(`[${providerName}] Error parsing cached stream data:`, e);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
console.log(`[${providerName}] Stream data cache expired for episode ${episode}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,13 +466,13 @@ export async function getStreamData(extension: Extension, episode: string, id: s
|
|||||||
}
|
}
|
||||||
let episodes;
|
let episodes;
|
||||||
|
|
||||||
if (source === "anilist"){
|
if (source === "anilist" && !extensionAnimeId) {
|
||||||
const anime: any = await getAnimeById(id)
|
const anime: any = await getAnimeById(id);
|
||||||
episodes = await searchEpisodesInExtension(extension, extension.constructor.name, anime.title.romaji);
|
episodes = await searchEpisodesInExtension(extension, extension.constructor.name, anime.title.romaji);
|
||||||
|
} else {
|
||||||
|
episodes = await extension.findEpisodes(extensionAnimeId ?? id);
|
||||||
}
|
}
|
||||||
else{
|
|
||||||
episodes = await extension.findEpisodes(id);
|
|
||||||
}
|
|
||||||
const targetEp = episodes.find(e => e.number === parseInt(episode));
|
const targetEp = episodes.find(e => e.number === parseInt(episode));
|
||||||
|
|
||||||
if (!targetEp) {
|
if (!targetEp) {
|
||||||
@@ -467,3 +485,46 @@ export async function getStreamData(extension: Extension, episode: string, id: s
|
|||||||
await setCache(cacheKey, streamData, CACHE_TTL_MS);
|
await setCache(cacheKey, streamData, CACHE_TTL_MS);
|
||||||
return streamData;
|
return streamData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function similarity(s1: string, s2: string): number {
|
||||||
|
const str1 = normalize(s1);
|
||||||
|
const str2 = normalize(s2);
|
||||||
|
|
||||||
|
const longer = str1.length > str2.length ? str1 : str2;
|
||||||
|
const shorter = str1.length > str2.length ? str2 : str1;
|
||||||
|
|
||||||
|
if (longer.length === 0) return 1.0;
|
||||||
|
|
||||||
|
const editDistance = levenshteinDistance(longer, shorter);
|
||||||
|
return (longer.length - editDistance) / longer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function levenshteinDistance(s1: string, s2: string): number {
|
||||||
|
const costs: number[] = [];
|
||||||
|
for (let i = 0; i <= s1.length; i++) {
|
||||||
|
let lastValue = i;
|
||||||
|
for (let j = 0; j <= s2.length; j++) {
|
||||||
|
if (i === 0) {
|
||||||
|
costs[j] = j;
|
||||||
|
} else if (j > 0) {
|
||||||
|
let newValue = costs[j - 1];
|
||||||
|
if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
|
||||||
|
newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
|
||||||
|
}
|
||||||
|
costs[j - 1] = lastValue;
|
||||||
|
lastValue = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i > 0) costs[s2.length] = lastValue;
|
||||||
|
}
|
||||||
|
return costs[s2.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(str: string): string {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/'/g, "'") // decodificar entidades HTML
|
||||||
|
.replace(/[^\w\s]/g, ' ') // convertir puntuación a espacios
|
||||||
|
.replace(/\s+/g, ' ') // normalizar espacios
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ export interface ExtensionSearchOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionSearchResult {
|
export interface ExtensionSearchResult {
|
||||||
|
url: string;
|
||||||
format: string;
|
format: string;
|
||||||
headers: any;
|
headers: any;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -158,6 +159,7 @@ export interface WatchStreamQuery {
|
|||||||
server?: string;
|
server?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
|
extensionAnimeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookParams {
|
export interface BookParams {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const AnimePlayer = (function() {
|
|||||||
let plyrInstance = null;
|
let plyrInstance = null;
|
||||||
let hlsInstance = null;
|
let hlsInstance = null;
|
||||||
|
|
||||||
|
let _manualExtensionId = null;
|
||||||
|
let _searchTimeout = null;
|
||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
wrapper: null,
|
wrapper: null,
|
||||||
playerWrapper: null,
|
playerWrapper: null,
|
||||||
@@ -41,7 +44,12 @@ const AnimePlayer = (function() {
|
|||||||
dlAudioList: null,
|
dlAudioList: null,
|
||||||
dlSubsList: null,
|
dlSubsList: null,
|
||||||
dlConfirmBtn: null,
|
dlConfirmBtn: null,
|
||||||
dlCancelBtn: null
|
dlCancelBtn: null,
|
||||||
|
manualMatchBtn: null,
|
||||||
|
matchModal: null,
|
||||||
|
matchInput: null,
|
||||||
|
matchList: null,
|
||||||
|
closeMatchModalBtn: null
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(animeId, initialSource, isLocal, animeData) {
|
function init(animeId, initialSource, isLocal, animeData) {
|
||||||
@@ -74,6 +82,32 @@ const AnimePlayer = (function() {
|
|||||||
els.dlSubsList = document.getElementById('dl-subs-list');
|
els.dlSubsList = document.getElementById('dl-subs-list');
|
||||||
els.dlConfirmBtn = document.getElementById('confirm-dl-btn');
|
els.dlConfirmBtn = document.getElementById('confirm-dl-btn');
|
||||||
els.dlCancelBtn = document.getElementById('cancel-dl-btn');
|
els.dlCancelBtn = document.getElementById('cancel-dl-btn');
|
||||||
|
els.manualMatchBtn = document.getElementById('manual-match-btn');
|
||||||
|
els.matchModal = document.getElementById('match-modal');
|
||||||
|
els.matchInput = document.getElementById('match-search-input');
|
||||||
|
els.matchList = document.getElementById('match-results-list');
|
||||||
|
els.closeMatchModalBtn = document.getElementById('close-match-modal');
|
||||||
|
|
||||||
|
// Event Listeners para Manual Match
|
||||||
|
if (els.manualMatchBtn) els.manualMatchBtn.addEventListener('click', openMatchModal);
|
||||||
|
if (els.closeMatchModalBtn) els.closeMatchModalBtn.addEventListener('click', closeMatchModal);
|
||||||
|
|
||||||
|
// Cerrar modal al hacer click fuera
|
||||||
|
if (els.matchModal) {
|
||||||
|
els.matchModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === els.matchModal) closeMatchModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input de búsqueda con Debounce
|
||||||
|
if (els.matchInput) {
|
||||||
|
els.matchInput.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(_searchTimeout);
|
||||||
|
_searchTimeout = setTimeout(() => {
|
||||||
|
executeMatchSearch(e.target.value);
|
||||||
|
}, 500); // Esperar 500ms tras dejar de escribir
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const closeDlModalBtn = document.getElementById('close-download-modal');
|
const closeDlModalBtn = document.getElementById('close-download-modal');
|
||||||
|
|
||||||
@@ -138,6 +172,108 @@ const AnimePlayer = (function() {
|
|||||||
loadExtensionsList();
|
loadExtensionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openMatchModal() {
|
||||||
|
if (!els.matchModal) return;
|
||||||
|
|
||||||
|
// Limpiar contenido previo
|
||||||
|
els.matchInput.value = '';
|
||||||
|
els.matchList.innerHTML = `<div style="padding:20px; text-align:center; color:#777;">Type to search in ${els.extSelect.value}...</div>`;
|
||||||
|
|
||||||
|
// 1. Mostrar el contenedor (para que el navegador calcule el layout)
|
||||||
|
els.matchModal.style.display = 'flex';
|
||||||
|
|
||||||
|
// 2. Pequeño delay o forzar reflow para que la transición de opacidad funcione
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
els.matchModal.classList.add('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => els.matchInput.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMatchModal() {
|
||||||
|
if (!els.matchModal) return;
|
||||||
|
els.matchModal.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!els.matchModal.classList.contains('show')) {
|
||||||
|
els.matchModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeMatchSearch(query) {
|
||||||
|
if (!query || query.trim().length < 2) return;
|
||||||
|
|
||||||
|
const ext = els.extSelect.value;
|
||||||
|
if (!ext || ext === 'local') return;
|
||||||
|
|
||||||
|
els.matchList.innerHTML = '<div class="spinner" style="margin: 20px auto;"></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/search/${ext}?q=${encodeURIComponent(query)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
renderMatchResults(data.results || []);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Match Search Error:", e);
|
||||||
|
els.matchList.innerHTML = '<p style="color:#ef4444; text-align:center;">Error searching extension.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMatchResults(results) {
|
||||||
|
els.matchList.innerHTML = '';
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
els.matchList.innerHTML = '<p style="text-align:center; color:#999;">No results found.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach(item => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'match-item dl-item';
|
||||||
|
|
||||||
|
const img = (item.coverImage && item.coverImage.large) ? item.coverImage.large : "/public/assets/placeholder.svg";
|
||||||
|
const title = item.title.english || item.title.romaji || item.title || 'Unknown';
|
||||||
|
const externalUrl = item.url || '#'; // El parámetro URL del JSON
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<img src="${img}" alt="cover">
|
||||||
|
<div class="match-info">
|
||||||
|
<span class="match-title">${title}</span>
|
||||||
|
<span class="match-meta">${item.releaseDate || item.year || ''}</span>
|
||||||
|
</div>
|
||||||
|
${item.url ? `
|
||||||
|
<a href="${externalUrl}" target="_blank" class="btn-view-source" title="View Source">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||||
|
<polyline points="15 3 21 3 21 9"></polyline>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
div.onclick = (e) => {
|
||||||
|
if (e.target.closest('.btn-view-source')) return;
|
||||||
|
selectManualMatch(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
els.matchList.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectManualMatch(item) {
|
||||||
|
// 1. Guardar el ID de la extensión
|
||||||
|
_manualExtensionId = item.id;
|
||||||
|
|
||||||
|
console.log("Manual Match Selected:", _manualExtensionId, "for extension:", els.extSelect.value);
|
||||||
|
|
||||||
|
// 2. Cerrar modal
|
||||||
|
closeMatchModal();
|
||||||
|
|
||||||
|
// 3. Recargar el stream con el nuevo ID
|
||||||
|
loadStream();
|
||||||
|
}
|
||||||
|
|
||||||
async function openInMPV() {
|
async function openInMPV() {
|
||||||
if (!_rawVideoData) {
|
if (!_rawVideoData) {
|
||||||
alert("No video loaded yet.");
|
alert("No video loaded yet.");
|
||||||
@@ -579,6 +715,11 @@ const AnimePlayer = (function() {
|
|||||||
if (shouldPlay && _currentEpisode > 0) loadStream();
|
if (shouldPlay && _currentEpisode > 0) loadStream();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (els.manualMatchBtn) {
|
||||||
|
// No mostrar en local, sí en extensiones
|
||||||
|
els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex';
|
||||||
|
}
|
||||||
|
_manualExtensionId = null;
|
||||||
|
|
||||||
setLoading("Loading Extension Settings...");
|
setLoading("Loading Extension Settings...");
|
||||||
try {
|
try {
|
||||||
@@ -690,14 +831,19 @@ const AnimePlayer = (function() {
|
|||||||
const extParam = `&ext=${currentExt}`;
|
const extParam = `&ext=${currentExt}`;
|
||||||
const realSource = _entrySource === 'local' ? 'anilist' : _entrySource;
|
const realSource = _entrySource === 'local' ? 'anilist' : _entrySource;
|
||||||
|
|
||||||
const url =
|
// AQUÍ AGREGAMOS EL PARÁMETRO OPCIONAL extensionAnimeId
|
||||||
`/api/watch/stream?animeId=${_animeId}` +
|
let url = `/api/watch/stream?animeId=${_animeId}` +
|
||||||
`&episode=${_currentEpisode}` +
|
`&episode=${_currentEpisode}` +
|
||||||
`&server=${encodeURIComponent(server)}` +
|
`&server=${encodeURIComponent(server)}` +
|
||||||
`&category=${_audioMode}` +
|
`&category=${_audioMode}` +
|
||||||
`${extParam}` +
|
`${extParam}` +
|
||||||
`&source=${realSource}`;
|
`&source=${realSource}`;
|
||||||
|
|
||||||
|
// INYECCIÓN DEL ID MANUAL
|
||||||
|
if (_manualExtensionId) {
|
||||||
|
url += `&extensionAnimeId=${encodeURIComponent(_manualExtensionId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@@ -87,6 +87,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>MPV</span>
|
<span>MPV</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-icon-glass" id="manual-match-btn" title="Manual Match" style="display: none;">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<div class="sd-toggle" id="sd-toggle" data-state="sub">
|
<div class="sd-toggle" id="sd-toggle" data-state="sub">
|
||||||
<div class="sd-bg"></div>
|
<div class="sd-bg"></div>
|
||||||
<div class="sd-option active" id="opt-sub">Sub</div>
|
<div class="sd-option active" id="opt-sub">Sub</div>
|
||||||
@@ -97,6 +103,20 @@
|
|||||||
<select id="extension-select" class="glass-select"></select>
|
<select id="extension-select" class="glass-select"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-overlay" id="match-modal">
|
||||||
|
<div class="modal-content download-settings-content">
|
||||||
|
<button class="modal-close" id="close-match-modal">✕</button>
|
||||||
|
<h2 class="modal-title">Manual Match</h2>
|
||||||
|
|
||||||
|
<div class="dl-section">
|
||||||
|
<input type="text" id="match-search-input" class="glass-input" placeholder="Search in this extension..." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-sections-wrapper">
|
||||||
|
<div id="match-results-list" class="dl-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-frame">
|
<div class="video-frame">
|
||||||
|
|||||||
@@ -709,7 +709,7 @@ body.stop-scrolling {
|
|||||||
MODAL DE DESCARGAS - REDISEÑO "GLASS"
|
MODAL DE DESCARGAS - REDISEÑO "GLASS"
|
||||||
========================================= */
|
========================================= */
|
||||||
|
|
||||||
#download-modal {
|
#download-modal, #match-modal {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -727,7 +727,7 @@ body.stop-scrolling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Estado visible activado por JS */
|
/* Estado visible activado por JS */
|
||||||
#download-modal.show {
|
#download-modal.show, #match-modal.show {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
@@ -752,7 +752,8 @@ body.stop-scrolling {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#download-modal.show .download-settings-content {
|
#download-modal.show .download-settings-content,
|
||||||
|
#match-modal.show .download-settings-content {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,3 +918,95 @@ body.stop-scrolling {
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glass-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-input:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: var(--brand-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reutilización para los resultados de búsqueda */
|
||||||
|
.match-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item img {
|
||||||
|
width: 40px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estado activo (seleccionado actualmente si quisieras marcarlo) */
|
||||||
|
.match-item.active {
|
||||||
|
border: 1px solid var(--brand-color);
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-source {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: auto; /* Empuja el botón a la derecha */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-source:hover {
|
||||||
|
background: var(--brand-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--brand-color-light);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajuste para que el texto no choque con el botón */
|
||||||
|
.match-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@@ -87,7 +87,7 @@ export async function searchInExtension(req: any, reply: FastifyReply) {
|
|||||||
|
|
||||||
export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) {
|
export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { animeId, episode, server, category, ext, source } = req.query;
|
const { animeId, episode, server, category, ext, source, extensionAnimeId } = req.query;
|
||||||
|
|
||||||
const extension = getExtension(ext);
|
const extension = getExtension(ext);
|
||||||
if (!extension) return { error: "Extension not found" };
|
if (!extension) return { error: "Extension not found" };
|
||||||
@@ -98,7 +98,8 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl
|
|||||||
animeId,
|
animeId,
|
||||||
source,
|
source,
|
||||||
server,
|
server,
|
||||||
category
|
category,
|
||||||
|
extensionAnimeId
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string
|
|||||||
averageScore: m.rating || m.score || null,
|
averageScore: m.rating || m.score || null,
|
||||||
format: 'ANIME',
|
format: 'ANIME',
|
||||||
seasonYear: null,
|
seasonYear: null,
|
||||||
|
url: m.url,
|
||||||
isExtensionResult: true,
|
isExtensionResult: true,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -439,11 +440,11 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string): Promise<StreamData> {
|
export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string, extensionAnimeId?: string): Promise<StreamData> {
|
||||||
const providerName = extension.constructor.name;
|
const providerName = extension.constructor.name;
|
||||||
|
|
||||||
const cacheKey = `anime:stream:${providerName}:${id}:${episode}:${server || 'default'}:${category || 'sub'}`;
|
const cacheKey = `anime:stream:${providerName}:${id}:${episode}:${server || 'default'}:${category || 'sub'}`;
|
||||||
|
if (!extensionAnimeId) {
|
||||||
const cached = await getCache(cacheKey);
|
const cached = await getCache(cacheKey);
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
@@ -456,8 +457,7 @@ export async function getStreamData(extension: Extension, episode: string, id: s
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[${providerName}] Error parsing cached stream data:`, e);
|
console.error(`[${providerName}] Error parsing cached stream data:`, e);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
console.log(`[${providerName}] Stream data cache expired for episode ${episode}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,13 +466,13 @@ export async function getStreamData(extension: Extension, episode: string, id: s
|
|||||||
}
|
}
|
||||||
let episodes;
|
let episodes;
|
||||||
|
|
||||||
if (source === "anilist"){
|
if (source === "anilist" && !extensionAnimeId) {
|
||||||
const anime: any = await getAnimeById(id)
|
const anime: any = await getAnimeById(id);
|
||||||
episodes = await searchEpisodesInExtension(extension, extension.constructor.name, anime.title.romaji);
|
episodes = await searchEpisodesInExtension(extension, extension.constructor.name, anime.title.romaji);
|
||||||
|
} else {
|
||||||
|
episodes = await extension.findEpisodes(extensionAnimeId ?? id);
|
||||||
}
|
}
|
||||||
else{
|
|
||||||
episodes = await extension.findEpisodes(id);
|
|
||||||
}
|
|
||||||
const targetEp = episodes.find(e => e.number === parseInt(episode));
|
const targetEp = episodes.find(e => e.number === parseInt(episode));
|
||||||
|
|
||||||
if (!targetEp) {
|
if (!targetEp) {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export interface ExtensionSearchOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionSearchResult {
|
export interface ExtensionSearchResult {
|
||||||
|
url: string;
|
||||||
format: string;
|
format: string;
|
||||||
headers: any;
|
headers: any;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -158,6 +159,7 @@ export interface WatchStreamQuery {
|
|||||||
server?: string;
|
server?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
|
extensionAnimeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookParams {
|
export interface BookParams {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const AnimePlayer = (function() {
|
|||||||
let plyrInstance = null;
|
let plyrInstance = null;
|
||||||
let hlsInstance = null;
|
let hlsInstance = null;
|
||||||
|
|
||||||
|
let _manualExtensionId = null;
|
||||||
|
let _searchTimeout = null;
|
||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
wrapper: null,
|
wrapper: null,
|
||||||
playerWrapper: null,
|
playerWrapper: null,
|
||||||
@@ -41,7 +44,12 @@ const AnimePlayer = (function() {
|
|||||||
dlAudioList: null,
|
dlAudioList: null,
|
||||||
dlSubsList: null,
|
dlSubsList: null,
|
||||||
dlConfirmBtn: null,
|
dlConfirmBtn: null,
|
||||||
dlCancelBtn: null
|
dlCancelBtn: null,
|
||||||
|
manualMatchBtn: null,
|
||||||
|
matchModal: null,
|
||||||
|
matchInput: null,
|
||||||
|
matchList: null,
|
||||||
|
closeMatchModalBtn: null
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(animeId, initialSource, isLocal, animeData) {
|
function init(animeId, initialSource, isLocal, animeData) {
|
||||||
@@ -74,6 +82,32 @@ const AnimePlayer = (function() {
|
|||||||
els.dlSubsList = document.getElementById('dl-subs-list');
|
els.dlSubsList = document.getElementById('dl-subs-list');
|
||||||
els.dlConfirmBtn = document.getElementById('confirm-dl-btn');
|
els.dlConfirmBtn = document.getElementById('confirm-dl-btn');
|
||||||
els.dlCancelBtn = document.getElementById('cancel-dl-btn');
|
els.dlCancelBtn = document.getElementById('cancel-dl-btn');
|
||||||
|
els.manualMatchBtn = document.getElementById('manual-match-btn');
|
||||||
|
els.matchModal = document.getElementById('match-modal');
|
||||||
|
els.matchInput = document.getElementById('match-search-input');
|
||||||
|
els.matchList = document.getElementById('match-results-list');
|
||||||
|
els.closeMatchModalBtn = document.getElementById('close-match-modal');
|
||||||
|
|
||||||
|
// Event Listeners para Manual Match
|
||||||
|
if (els.manualMatchBtn) els.manualMatchBtn.addEventListener('click', openMatchModal);
|
||||||
|
if (els.closeMatchModalBtn) els.closeMatchModalBtn.addEventListener('click', closeMatchModal);
|
||||||
|
|
||||||
|
// Cerrar modal al hacer click fuera
|
||||||
|
if (els.matchModal) {
|
||||||
|
els.matchModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === els.matchModal) closeMatchModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input de búsqueda con Debounce
|
||||||
|
if (els.matchInput) {
|
||||||
|
els.matchInput.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(_searchTimeout);
|
||||||
|
_searchTimeout = setTimeout(() => {
|
||||||
|
executeMatchSearch(e.target.value);
|
||||||
|
}, 500); // Esperar 500ms tras dejar de escribir
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const closeDlModalBtn = document.getElementById('close-download-modal');
|
const closeDlModalBtn = document.getElementById('close-download-modal');
|
||||||
|
|
||||||
@@ -138,6 +172,108 @@ const AnimePlayer = (function() {
|
|||||||
loadExtensionsList();
|
loadExtensionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openMatchModal() {
|
||||||
|
if (!els.matchModal) return;
|
||||||
|
|
||||||
|
// Limpiar contenido previo
|
||||||
|
els.matchInput.value = '';
|
||||||
|
els.matchList.innerHTML = `<div style="padding:20px; text-align:center; color:#777;">Type to search in ${els.extSelect.value}...</div>`;
|
||||||
|
|
||||||
|
// 1. Mostrar el contenedor (para que el navegador calcule el layout)
|
||||||
|
els.matchModal.style.display = 'flex';
|
||||||
|
|
||||||
|
// 2. Pequeño delay o forzar reflow para que la transición de opacidad funcione
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
els.matchModal.classList.add('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => els.matchInput.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMatchModal() {
|
||||||
|
if (!els.matchModal) return;
|
||||||
|
els.matchModal.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!els.matchModal.classList.contains('show')) {
|
||||||
|
els.matchModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeMatchSearch(query) {
|
||||||
|
if (!query || query.trim().length < 2) return;
|
||||||
|
|
||||||
|
const ext = els.extSelect.value;
|
||||||
|
if (!ext || ext === 'local') return;
|
||||||
|
|
||||||
|
els.matchList.innerHTML = '<div class="spinner" style="margin: 20px auto;"></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/search/${ext}?q=${encodeURIComponent(query)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
renderMatchResults(data.results || []);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Match Search Error:", e);
|
||||||
|
els.matchList.innerHTML = '<p style="color:#ef4444; text-align:center;">Error searching extension.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMatchResults(results) {
|
||||||
|
els.matchList.innerHTML = '';
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
els.matchList.innerHTML = '<p style="text-align:center; color:#999;">No results found.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach(item => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'match-item dl-item';
|
||||||
|
|
||||||
|
const img = (item.coverImage && item.coverImage.large) ? item.coverImage.large : "/public/assets/placeholder.svg";
|
||||||
|
const title = item.title.english || item.title.romaji || item.title || 'Unknown';
|
||||||
|
const externalUrl = item.url || '#'; // El parámetro URL del JSON
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<img src="${img}" alt="cover">
|
||||||
|
<div class="match-info">
|
||||||
|
<span class="match-title">${title}</span>
|
||||||
|
<span class="match-meta">${item.releaseDate || item.year || ''}</span>
|
||||||
|
</div>
|
||||||
|
${item.url ? `
|
||||||
|
<a href="${externalUrl}" target="_blank" class="btn-view-source" title="View Source">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||||
|
<polyline points="15 3 21 3 21 9"></polyline>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
div.onclick = (e) => {
|
||||||
|
if (e.target.closest('.btn-view-source')) return;
|
||||||
|
selectManualMatch(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
els.matchList.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectManualMatch(item) {
|
||||||
|
// 1. Guardar el ID de la extensión
|
||||||
|
_manualExtensionId = item.id;
|
||||||
|
|
||||||
|
console.log("Manual Match Selected:", _manualExtensionId, "for extension:", els.extSelect.value);
|
||||||
|
|
||||||
|
// 2. Cerrar modal
|
||||||
|
closeMatchModal();
|
||||||
|
|
||||||
|
// 3. Recargar el stream con el nuevo ID
|
||||||
|
loadStream();
|
||||||
|
}
|
||||||
|
|
||||||
async function openInMPV() {
|
async function openInMPV() {
|
||||||
if (!_rawVideoData) {
|
if (!_rawVideoData) {
|
||||||
alert("No video loaded yet.");
|
alert("No video loaded yet.");
|
||||||
@@ -579,6 +715,11 @@ const AnimePlayer = (function() {
|
|||||||
if (shouldPlay && _currentEpisode > 0) loadStream();
|
if (shouldPlay && _currentEpisode > 0) loadStream();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (els.manualMatchBtn) {
|
||||||
|
// No mostrar en local, sí en extensiones
|
||||||
|
els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex';
|
||||||
|
}
|
||||||
|
_manualExtensionId = null;
|
||||||
|
|
||||||
setLoading("Loading Extension Settings...");
|
setLoading("Loading Extension Settings...");
|
||||||
try {
|
try {
|
||||||
@@ -690,14 +831,19 @@ const AnimePlayer = (function() {
|
|||||||
const extParam = `&ext=${currentExt}`;
|
const extParam = `&ext=${currentExt}`;
|
||||||
const realSource = _entrySource === 'local' ? 'anilist' : _entrySource;
|
const realSource = _entrySource === 'local' ? 'anilist' : _entrySource;
|
||||||
|
|
||||||
const url =
|
// AQUÍ AGREGAMOS EL PARÁMETRO OPCIONAL extensionAnimeId
|
||||||
`/api/watch/stream?animeId=${_animeId}` +
|
let url = `/api/watch/stream?animeId=${_animeId}` +
|
||||||
`&episode=${_currentEpisode}` +
|
`&episode=${_currentEpisode}` +
|
||||||
`&server=${encodeURIComponent(server)}` +
|
`&server=${encodeURIComponent(server)}` +
|
||||||
`&category=${_audioMode}` +
|
`&category=${_audioMode}` +
|
||||||
`${extParam}` +
|
`${extParam}` +
|
||||||
`&source=${realSource}`;
|
`&source=${realSource}`;
|
||||||
|
|
||||||
|
// INYECCIÓN DEL ID MANUAL
|
||||||
|
if (_manualExtensionId) {
|
||||||
|
url += `&extensionAnimeId=${encodeURIComponent(_manualExtensionId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@@ -61,13 +61,15 @@
|
|||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button class="btn-icon-glass" id="download-btn" title="Download Episode" style="display: none;">
|
<button class="btn-icon-glass" id="download-btn" title="Download Episode" style="display: none;">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<button class="btn-icon-glass" id="manual-match-btn" title="Manual Match" style="display: none;">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="settings-group">
|
|
||||||
<div class="sd-toggle" id="sd-toggle" data-state="sub">
|
<div class="sd-toggle" id="sd-toggle" data-state="sub">
|
||||||
<div class="sd-bg"></div>
|
<div class="sd-bg"></div>
|
||||||
<div class="sd-option active" id="opt-sub">Sub</div>
|
<div class="sd-option active" id="opt-sub">Sub</div>
|
||||||
@@ -78,6 +80,21 @@
|
|||||||
<select id="extension-select" class="glass-select"></select>
|
<select id="extension-select" class="glass-select"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="match-modal">
|
||||||
|
<div class="modal-content download-settings-content">
|
||||||
|
<button class="modal-close" id="close-match-modal">✕</button>
|
||||||
|
<h2 class="modal-title">Manual Match</h2>
|
||||||
|
|
||||||
|
<div class="dl-section">
|
||||||
|
<input type="text" id="match-search-input" class="glass-input" placeholder="Search in this extension..." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-sections-wrapper">
|
||||||
|
<div id="match-results-list" class="dl-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-frame">
|
<div class="video-frame">
|
||||||
|
|||||||
@@ -709,7 +709,7 @@ body.stop-scrolling {
|
|||||||
MODAL DE DESCARGAS - REDISEÑO "GLASS"
|
MODAL DE DESCARGAS - REDISEÑO "GLASS"
|
||||||
========================================= */
|
========================================= */
|
||||||
|
|
||||||
#download-modal {
|
#download-modal, #match-modal {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -727,7 +727,7 @@ body.stop-scrolling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Estado visible activado por JS */
|
/* Estado visible activado por JS */
|
||||||
#download-modal.show {
|
#download-modal.show, #match-modal.show {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
@@ -752,7 +752,8 @@ body.stop-scrolling {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#download-modal.show .download-settings-content {
|
#download-modal.show .download-settings-content,
|
||||||
|
#match-modal.show .download-settings-content {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,3 +918,95 @@ body.stop-scrolling {
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glass-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-input:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: var(--brand-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reutilización para los resultados de búsqueda */
|
||||||
|
.match-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item img {
|
||||||
|
width: 40px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estado activo (seleccionado actualmente si quisieras marcarlo) */
|
||||||
|
.match-item.active {
|
||||||
|
border: 1px solid var(--brand-color);
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-source {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: auto; /* Empuja el botón a la derecha */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-source:hover {
|
||||||
|
background: var(--brand-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--brand-color-light);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajuste para que el texto no choque con el botón */
|
||||||
|
.match-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user