added manual matching on books
This commit is contained in:
@@ -88,9 +88,10 @@ export async function getChapters(req: any, reply: FastifyReply) {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const source = req.query.source || 'anilist';
|
const source = req.query.source || 'anilist';
|
||||||
const provider = req.query.provider;
|
const provider = req.query.provider;
|
||||||
|
const extensionBookId = req.query.extensionBookId;
|
||||||
|
|
||||||
const isExternal = source !== 'anilist';
|
const isExternal = source !== 'anilist';
|
||||||
return await booksService.getChaptersForBook(id, isExternal, provider);
|
return await booksService.getChaptersForBook(id, isExternal, provider, extensionBookId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return { chapters: [] };
|
return { chapters: [] };
|
||||||
|
|||||||
@@ -326,7 +326,8 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
|
|||||||
averageScore: m.rating || m.score || null,
|
averageScore: m.rating || m.score || null,
|
||||||
format: m.format,
|
format: m.format,
|
||||||
seasonYear: null,
|
seasonYear: null,
|
||||||
isExtensionResult: true
|
isExtensionResult: true,
|
||||||
|
url: m.url,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,47 +362,60 @@ async function fetchBookMetadata(id: string): Promise<Book | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, search: boolean, origin: string): Promise<ChapterWithProvider[]> {
|
async function searchChaptersInExtension(ext: Extension, name: string, lookupId: string, cacheId: string, search: boolean, origin: string, disableCache = false): Promise<ChapterWithProvider[]> {
|
||||||
const cacheKey = `chapters:${name}:${origin}:${search ? "search" : "id"}:${searchTitle}`;
|
const cacheKey = `chapters:${name}:${origin}:${search ? "search" : "id"}:${cacheId}`;
|
||||||
const cached = await getCache(cacheKey);
|
if (!disableCache) {
|
||||||
|
const cached = await getCache(cacheKey);
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
|
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
|
||||||
|
|
||||||
if (!isExpired) {
|
if (!isExpired) {
|
||||||
console.log(`[${name}] Chapters cache hit for: ${searchTitle}`);
|
console.log(`[${name}] Chapters cache hit for: ${lookupId}`);
|
||||||
try {
|
try {
|
||||||
return JSON.parse(cached.result) as ChapterWithProvider[];
|
return JSON.parse(cached.result) as ChapterWithProvider[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[${name}] Error parsing cached chapters:`, e);
|
console.error(`[${name}] Error parsing cached chapters:`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log(`[${name}] Chapters cache expired for: ${searchTitle}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[${name}] Searching chapters for: ${searchTitle}`);
|
console.log(`[${name}] Searching chapters for: ${lookupId}`);
|
||||||
|
|
||||||
let mediaId: string;
|
let mediaId: string;
|
||||||
if (search) {
|
if (search) {
|
||||||
const matches = await ext.search!({
|
const matches = await ext.search!({
|
||||||
query: searchTitle,
|
query: lookupId,
|
||||||
media: {
|
media: {
|
||||||
romajiTitle: searchTitle,
|
romajiTitle: lookupId,
|
||||||
englishTitle: searchTitle,
|
englishTitle: lookupId,
|
||||||
startDate: { year: 0, month: 0, day: 0 }
|
startDate: { year: 0, month: 0, day: 0 }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const best = matches?.[0];
|
if (!matches?.length) return [];
|
||||||
|
|
||||||
if (!best) { return [] }
|
const nq = normalize(lookupId);
|
||||||
|
|
||||||
mediaId = best.id;
|
const scored = matches.map(m => {
|
||||||
|
const nt = normalize(m.title);
|
||||||
|
let score = similarity(nq, nt);
|
||||||
|
|
||||||
|
if (nt === nq || nt.includes(nq)) score += 0.5;
|
||||||
|
|
||||||
|
return { m, score };
|
||||||
|
});
|
||||||
|
|
||||||
|
scored.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
if (scored[0].score < 0.4) return [];
|
||||||
|
|
||||||
|
mediaId = scored[0].m.id;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const match = await ext.getMetadata(searchTitle);
|
const match = await ext.getMetadata(lookupId);
|
||||||
mediaId = match.id;
|
mediaId = match.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +446,7 @@ async function searchChaptersInExtension(ext: Extension, name: string, searchTit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getChaptersForBook(id: string, ext: Boolean, onlyProvider?: string): Promise<{ chapters: ChapterWithProvider[] }> {
|
export async function getChaptersForBook(id: string, ext: Boolean, onlyProvider?: string, extensionBookId?: string): Promise<{ chapters: ChapterWithProvider[] }> {
|
||||||
let bookData: Book | null = null;
|
let bookData: Book | null = null;
|
||||||
let searchTitle: string = "";
|
let searchTitle: string = "";
|
||||||
|
|
||||||
@@ -462,11 +476,30 @@ export async function getChaptersForBook(id: string, ext: Boolean, onlyProvider?
|
|||||||
|
|
||||||
for (const [name, ext] of bookExtensions) {
|
for (const [name, ext] of bookExtensions) {
|
||||||
if (onlyProvider && name !== onlyProvider) continue;
|
if (onlyProvider && name !== onlyProvider) continue;
|
||||||
if (name == extension) {
|
if (extensionBookId && name === onlyProvider) {
|
||||||
const chapters = await searchChaptersInExtension(ext, name, id, false, exts);
|
const targetId = extensionBookId ?? id;
|
||||||
|
|
||||||
|
const chapters = await searchChaptersInExtension(
|
||||||
|
ext,
|
||||||
|
name,
|
||||||
|
targetId, // lookup
|
||||||
|
id, // cache siempre con el id normal
|
||||||
|
false,
|
||||||
|
exts,
|
||||||
|
Boolean(extensionBookId)
|
||||||
|
);
|
||||||
|
|
||||||
allChapters.push(...chapters);
|
allChapters.push(...chapters);
|
||||||
} else {
|
} else {
|
||||||
const chapters = await searchChaptersInExtension(ext, name, searchTitle, true, exts);
|
const chapters = await searchChaptersInExtension(
|
||||||
|
ext,
|
||||||
|
name,
|
||||||
|
searchTitle,
|
||||||
|
id, // cache con id normal
|
||||||
|
true,
|
||||||
|
exts
|
||||||
|
);
|
||||||
|
|
||||||
allChapters.push(...chapters);
|
allChapters.push(...chapters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,3 +582,46 @@ export async function getChapterContent(bookId: string, chapterId: string, provi
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ const AnimePlayer = (function() {
|
|||||||
let hlsInstance = null;
|
let hlsInstance = null;
|
||||||
|
|
||||||
let _manualExtensionId = null;
|
let _manualExtensionId = null;
|
||||||
let _searchTimeout = null;
|
|
||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
wrapper: null,
|
wrapper: null,
|
||||||
@@ -46,10 +45,6 @@ const AnimePlayer = (function() {
|
|||||||
dlConfirmBtn: null,
|
dlConfirmBtn: null,
|
||||||
dlCancelBtn: null,
|
dlCancelBtn: null,
|
||||||
manualMatchBtn: null,
|
manualMatchBtn: null,
|
||||||
matchModal: null,
|
|
||||||
matchInput: null,
|
|
||||||
matchList: null,
|
|
||||||
closeMatchModalBtn: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(animeId, initialSource, isLocal, animeData) {
|
function init(animeId, initialSource, isLocal, animeData) {
|
||||||
@@ -83,31 +78,6 @@ const AnimePlayer = (function() {
|
|||||||
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.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');
|
||||||
|
|
||||||
@@ -168,112 +138,33 @@ const AnimePlayer = (function() {
|
|||||||
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
||||||
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
||||||
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
||||||
|
if (els.manualMatchBtn) {
|
||||||
|
els.manualMatchBtn.addEventListener('click', openMatchModal);
|
||||||
|
}
|
||||||
|
|
||||||
loadExtensionsList();
|
loadExtensionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMatchModal() {
|
function openMatchModal() {
|
||||||
if (!els.matchModal) return;
|
const currentExt = els.extSelect.value;
|
||||||
|
if (!currentExt || currentExt === 'local') return;
|
||||||
|
|
||||||
// Limpiar contenido previo
|
MatchModal.open({
|
||||||
els.matchInput.value = '';
|
provider: currentExt,
|
||||||
els.matchList.innerHTML = `<div style="padding:20px; text-align:center; color:#777;">Type to search in ${els.extSelect.value}...</div>`;
|
initialQuery: _animeTitle, // Variable existente en player.js
|
||||||
|
onSearch: async (query, prov) => {
|
||||||
// 1. Mostrar el contenedor (para que el navegador calcule el layout)
|
const res = await fetch(`/api/search/${prov}?q=${encodeURIComponent(query)}`);
|
||||||
els.matchModal.style.display = 'flex';
|
const data = await res.json();
|
||||||
|
return data.results || [];
|
||||||
// 2. Pequeño delay o forzar reflow para que la transición de opacidad funcione
|
},
|
||||||
requestAnimationFrame(() => {
|
onSelect: (item) => {
|
||||||
els.matchModal.classList.add('show');
|
console.log("Selected Anime ID:", item.id);
|
||||||
});
|
_manualExtensionId = item.id;
|
||||||
|
loadStream();
|
||||||
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.");
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ let isLocal = false;
|
|||||||
let currentLanguage = null;
|
let currentLanguage = null;
|
||||||
let uniqueLanguages = [];
|
let uniqueLanguages = [];
|
||||||
let isSortAscending = true;
|
let isSortAscending = true;
|
||||||
|
let manualExtensionBookId = null;
|
||||||
|
|
||||||
const chapterPagination = Object.create(PaginationManager);
|
const chapterPagination = Object.create(PaginationManager);
|
||||||
chapterPagination.init(6, () => renderChapterList());
|
chapterPagination.init(6, () => renderChapterList());
|
||||||
@@ -36,6 +37,37 @@ async function init() {
|
|||||||
await loadChapters();
|
await loadChapters();
|
||||||
await setupAddToListButton();
|
await setupAddToListButton();
|
||||||
|
|
||||||
|
document.getElementById('manual-match-btn')?.addEventListener('click', () => {
|
||||||
|
const select = document.getElementById('provider-filter');
|
||||||
|
const provider = select.value;
|
||||||
|
|
||||||
|
// Obtener título para prellenar
|
||||||
|
const currentTitle = bookData?.title?.romaji || bookData?.title?.english || '';
|
||||||
|
|
||||||
|
MatchModal.open({
|
||||||
|
provider: provider,
|
||||||
|
initialQuery: currentTitle,
|
||||||
|
// Define CÓMO buscar
|
||||||
|
onSearch: async (query, prov) => {
|
||||||
|
const res = await fetch(`/api/search/books/${prov}?q=${encodeURIComponent(query)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return data.results || [];
|
||||||
|
},
|
||||||
|
// Define QUÉ hacer al seleccionar
|
||||||
|
onSelect: (item) => {
|
||||||
|
console.log("Selected Book ID:", item.id);
|
||||||
|
manualExtensionBookId = item.id;
|
||||||
|
|
||||||
|
// Lógica existente de tu book.js para recargar caps
|
||||||
|
loadChapters(provider);
|
||||||
|
|
||||||
|
// Feedback visual en el botón
|
||||||
|
const btn = document.getElementById('manual-match-btn');
|
||||||
|
if(btn) btn.style.color = '#22c55e';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Init Error:", err);
|
console.error("Init Error:", err);
|
||||||
showError("Error loading book");
|
showError("Error loading book");
|
||||||
@@ -311,6 +343,9 @@ async function loadChapters(targetProvider = null) {
|
|||||||
const source = extensionName || 'anilist';
|
const source = extensionName || 'anilist';
|
||||||
fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
|
fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
|
||||||
if (targetProvider !== 'all') fetchUrl += `&provider=${targetProvider}`;
|
if (targetProvider !== 'all') fetchUrl += `&provider=${targetProvider}`;
|
||||||
|
if (manualExtensionBookId && targetProvider !== 'all') {
|
||||||
|
fetchUrl += `&extensionBookId=${manualExtensionBookId}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(fetchUrl);
|
const res = await fetch(fetchUrl);
|
||||||
@@ -515,6 +550,8 @@ async function loadAvailableExtensions() {
|
|||||||
|
|
||||||
function setupProviderFilter() {
|
function setupProviderFilter() {
|
||||||
const select = document.getElementById('provider-filter');
|
const select = document.getElementById('provider-filter');
|
||||||
|
const manualBtn = document.getElementById('manual-match-btn'); // NUEVO
|
||||||
|
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
select.style.display = 'inline-block';
|
select.style.display = 'inline-block';
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
@@ -538,11 +575,32 @@ function setupProviderFilter() {
|
|||||||
select.appendChild(opt);
|
select.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lógica de selección inicial
|
||||||
if (isLocal) select.value = 'local';
|
if (isLocal) select.value = 'local';
|
||||||
else if (extensionName && availableExtensions.includes(extensionName)) select.value = extensionName;
|
else if (extensionName && availableExtensions.includes(extensionName)) select.value = extensionName;
|
||||||
else if (availableExtensions.length > 0) select.value = availableExtensions[0];
|
else if (availableExtensions.length > 0) select.value = availableExtensions[0];
|
||||||
|
|
||||||
select.onchange = () => loadChapters(select.value);
|
// Visibilidad inicial del botón manual
|
||||||
|
updateManualButtonVisibility(select.value);
|
||||||
|
|
||||||
|
select.onchange = () => {
|
||||||
|
// Al cambiar de proveedor, reseteamos la selección manual para evitar conflictos
|
||||||
|
manualExtensionBookId = null;
|
||||||
|
updateManualButtonVisibility(select.value);
|
||||||
|
loadChapters(select.value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateManualButtonVisibility(provider) {
|
||||||
|
const btn = document.getElementById('manual-match-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
// Solo mostrar si es un proveedor específico (no 'all' ni 'local')
|
||||||
|
if (provider !== 'all' && provider !== 'local') {
|
||||||
|
btn.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
btn.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateExtensionPill() {
|
function updateExtensionPill() {
|
||||||
@@ -569,9 +627,9 @@ function updateCustomAddButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupModalClickOutside() {
|
function setupModalClickOutside() {
|
||||||
const modal = document.getElementById('add-list-modal');
|
const addListModal = document.getElementById('add-list-modal');
|
||||||
if (modal) {
|
if (addListModal) {
|
||||||
modal.addEventListener('click', (e) => {
|
addListModal.addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'add-list-modal') ListModalManager.close();
|
if (e.target.id === 'add-list-modal') ListModalManager.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
171
desktop/src/scripts/utils/match-modal.js
Normal file
171
desktop/src/scripts/utils/match-modal.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
const MatchModal = (function() {
|
||||||
|
let _config = {
|
||||||
|
onSearch: async (query, provider) => [], // Debe devolver Array de objetos
|
||||||
|
onSelect: (item, provider) => {},
|
||||||
|
provider: 'generic'
|
||||||
|
};
|
||||||
|
|
||||||
|
let elements = {};
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (document.getElementById('waifu-match-modal')) return;
|
||||||
|
|
||||||
|
// Inyectar HTML
|
||||||
|
const modalHTML = `
|
||||||
|
<div class="match-modal-overlay" id="waifu-match-modal">
|
||||||
|
<div class="match-modal-content">
|
||||||
|
<div class="match-header">
|
||||||
|
<h3 class="match-title">Manual Match <span id="match-provider-badge" style="opacity:0.6; font-size:0.8em; margin-left:8px;"></span></h3>
|
||||||
|
<button class="match-close-btn" id="match-close-btn">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="match-search-container">
|
||||||
|
<input type="text" id="match-input" class="match-input" placeholder="Search title..." autocomplete="off">
|
||||||
|
<button id="match-btn-action" class="match-search-btn">Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="match-results-body" id="match-results-container">
|
||||||
|
<div class="match-msg">Type to start searching...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
|
||||||
|
// Cachear elementos
|
||||||
|
elements = {
|
||||||
|
overlay: document.getElementById('waifu-match-modal'),
|
||||||
|
input: document.getElementById('match-input'),
|
||||||
|
results: document.getElementById('match-results-container'),
|
||||||
|
badge: document.getElementById('match-provider-badge'),
|
||||||
|
closeBtn: document.getElementById('match-close-btn'),
|
||||||
|
searchBtn: document.getElementById('match-btn-action')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
elements.closeBtn.onclick = close;
|
||||||
|
elements.overlay.onclick = (e) => { if(e.target === elements.overlay) close(); };
|
||||||
|
|
||||||
|
// Búsqueda al hacer clic
|
||||||
|
elements.searchBtn.onclick = () => performSearch(elements.input.value);
|
||||||
|
|
||||||
|
// Búsqueda al escribir (Debounce)
|
||||||
|
elements.input.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
if(e.target.value.trim().length === 0) return;
|
||||||
|
searchTimeout = setTimeout(() => performSearch(e.target.value), 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter key
|
||||||
|
elements.input.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') performSearch(elements.input.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(options) {
|
||||||
|
init(); // Asegurar que el DOM existe
|
||||||
|
|
||||||
|
_config = { ..._config, ...options };
|
||||||
|
|
||||||
|
// Resetear UI
|
||||||
|
elements.input.value = options.initialQuery || '';
|
||||||
|
elements.results.innerHTML = '<div class="match-msg">Search above to find matches...</div>';
|
||||||
|
elements.badge.innerText = options.provider ? `(${options.provider})` : '';
|
||||||
|
|
||||||
|
// Mostrar Modal
|
||||||
|
elements.overlay.classList.add('active');
|
||||||
|
|
||||||
|
// Auto-search si hay query inicial
|
||||||
|
if (options.initialQuery) {
|
||||||
|
performSearch(options.initialQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => elements.input.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if(elements.overlay) elements.overlay.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performSearch(query) {
|
||||||
|
if (!query || query.trim().length < 2) return;
|
||||||
|
|
||||||
|
elements.results.innerHTML = '<div class="match-spinner"></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ejecutar la función de búsqueda pasada en la config
|
||||||
|
const results = await _config.onSearch(query, _config.provider);
|
||||||
|
renderResults(results);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
elements.results.innerHTML = '<div class="match-msg error">Error searching provider.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(results) {
|
||||||
|
elements.results.innerHTML = '';
|
||||||
|
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
elements.results.innerHTML = '<div class="match-msg">No matches found.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = document.createElement('div');
|
||||||
|
grid.className = 'match-list-grid';
|
||||||
|
|
||||||
|
results.forEach(item => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'match-item';
|
||||||
|
|
||||||
|
// Normalización de datos para asegurar compatibilidad con Anime/Libros
|
||||||
|
const img = item.coverImage?.large || item.coverImage || item.image || '/public/assets/no-image.png';
|
||||||
|
const title = item.title?.english || item.title?.romaji || item.title || 'Unknown Title';
|
||||||
|
const meta = item.releaseDate || item.year || item.startDate?.year || '';
|
||||||
|
const url = item.url || item.externalUrl || null;
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<img src="${img}" class="match-poster" loading="lazy">
|
||||||
|
<div class="match-info">
|
||||||
|
<div class="match-item-title">${title}</div>
|
||||||
|
<div class="match-item-meta">${meta}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Botón de enlace externo (si existe URL)
|
||||||
|
if (url) {
|
||||||
|
const linkBtn = document.createElement('a');
|
||||||
|
linkBtn.href = url;
|
||||||
|
linkBtn.target = "_blank";
|
||||||
|
linkBtn.className = "match-link-btn";
|
||||||
|
linkBtn.title = "View Source";
|
||||||
|
linkBtn.innerHTML = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
// Evitar que el click en el enlace dispare el select
|
||||||
|
linkBtn.onclick = (e) => e.stopPropagation();
|
||||||
|
el.appendChild(linkBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click en la tarjeta selecciona
|
||||||
|
el.onclick = () => {
|
||||||
|
_config.onSelect(item);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.appendChild(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.results.appendChild(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
close
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<link rel="stylesheet" href="/views/css/anime/player.css" />
|
<link rel="stylesheet" href="/views/css/anime/player.css" />
|
||||||
<link rel="stylesheet" href="/views/css/components/titlebar.css">
|
<link rel="stylesheet" href="/views/css/components/titlebar.css">
|
||||||
<script src="/src/scripts/titlebar.js"></script>
|
<script src="/src/scripts/titlebar.js"></script>
|
||||||
|
<link rel="stylesheet" href="/views/css/components/match-modal.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="titlebar">
|
<div id="titlebar">
|
||||||
@@ -103,20 +104,6 @@
|
|||||||
<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">
|
||||||
@@ -234,9 +221,9 @@
|
|||||||
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
|
<script src="/src/scripts/utils/match-modal.js"></script>
|
||||||
|
|
||||||
<script src="/src/scripts/anime/player.js"></script>
|
<script src="/src/scripts/anime/player.js"></script>
|
||||||
<script src="/src/scripts/anime/entry.js"></script>
|
<script src="/src/scripts/anime/entry.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<link rel="stylesheet" href="/views/css/books/book.css">
|
<link rel="stylesheet" href="/views/css/books/book.css">
|
||||||
<link rel="stylesheet" href="/views/css/components/updateNotifier.css">
|
<link rel="stylesheet" href="/views/css/components/updateNotifier.css">
|
||||||
<link rel="stylesheet" href="/views/css/components/titlebar.css">
|
<link rel="stylesheet" href="/views/css/components/titlebar.css">
|
||||||
|
<link rel="stylesheet" href="/views/css/components/match-modal.css">
|
||||||
<script src="/src/scripts/titlebar.js"></script>
|
<script src="/src/scripts/titlebar.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -91,6 +92,12 @@
|
|||||||
<h2>Chapters</h2>
|
<h2>Chapters</h2>
|
||||||
|
|
||||||
<div class="chapter-controls">
|
<div class="chapter-controls">
|
||||||
|
<button id="manual-match-btn" class="glass-btn-icon" style="display: none;" title="Manual Match">
|
||||||
|
<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>
|
||||||
<select id="provider-filter" class="glass-select" style="display: none;">
|
<select id="provider-filter" class="glass-select" style="display: none;">
|
||||||
<option value="all">All Providers</option>
|
<option value="all">All Providers</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -150,6 +157,7 @@
|
|||||||
<script src="/src/scripts/utils/pagination-manager.js"></script>
|
<script src="/src/scripts/utils/pagination-manager.js"></script>
|
||||||
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
|
<script src="/src/scripts/utils/match-modal.js"></script>
|
||||||
<script src="/src/scripts/books/book.js"></script>
|
<script src="/src/scripts/books/book.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -936,77 +936,3 @@ body.stop-scrolling {
|
|||||||
border-color: var(--brand-color);
|
border-color: var(--brand-color);
|
||||||
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
|
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;
|
|
||||||
}
|
|
||||||
282
desktop/views/css/components/match-modal.css
Normal file
282
desktop/views/css/components/match-modal.css
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/* match-modal.css */
|
||||||
|
:root {
|
||||||
|
--match-primary: #8b5cf6;
|
||||||
|
--match-bg: rgba(15, 15, 15, 0.95);
|
||||||
|
--match-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--match-input-bg: rgba(0, 0, 0, 0.4);
|
||||||
|
--match-text-muted: #a1a1aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay Global con efecto Glass */
|
||||||
|
.match-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-modal-overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contenido del Modal */
|
||||||
|
.match-modal-content {
|
||||||
|
background: var(--match-bg);
|
||||||
|
border: 1px solid var(--match-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px; /* Un poco más ancho para mejor lectura */
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow:
|
||||||
|
0 25px 80px rgba(0, 0, 0, 0.9),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
transform: scale(0.95) translateY(10px);
|
||||||
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-modal-overlay.active .match-modal-content {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Estilizado */
|
||||||
|
.match-header {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--match-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(to bottom, rgba(255,255,255,0.02), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
/* Efecto gradiente en el texto como en anilist-modal */
|
||||||
|
background: linear-gradient(to right, #fff, #aaa);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#match-provider-badge {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
-webkit-text-fill-color: #888; /* Resetear el gradiente para el badge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-close-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid var(--match-border);
|
||||||
|
color: #ccc;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-close-btn:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
border-color: #ef4444;
|
||||||
|
color: white;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buscador y Controles */
|
||||||
|
.match-search-container {
|
||||||
|
padding: 1.5rem 2rem 1rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--match-input-bg);
|
||||||
|
border: 1px solid var(--match-border);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-input:focus {
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
border-color: var(--match-primary);
|
||||||
|
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-search-btn {
|
||||||
|
padding: 0 1.8rem;
|
||||||
|
background: var(--match-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-search-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lista de Resultados */
|
||||||
|
.match-results-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem 2rem 2rem 2rem;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-results-body::-webkit-scrollbar { width: 6px; }
|
||||||
|
.match-results-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 10px; }
|
||||||
|
|
||||||
|
.match-list-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tarjeta de Resultado Mejorada */
|
||||||
|
.match-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-poster {
|
||||||
|
width: 50px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #222;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item-meta {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--match-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botón de enlace externo refinado */
|
||||||
|
.match-link-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-link-btn:hover {
|
||||||
|
background: var(--match-primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--match-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 0 15px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mensajes y Spinner */
|
||||||
|
.match-spinner {
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
border: 3px solid rgba(255,255,255,0.1);
|
||||||
|
border-top-color: var(--match-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin: 3rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
.match-msg {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--match-text-muted);
|
||||||
|
margin-top: 3rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.match-msg.error { color: #ef4444; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.match-modal-content {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.match-search-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
.match-search-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,9 +88,10 @@ export async function getChapters(req: any, reply: FastifyReply) {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const source = req.query.source || 'anilist';
|
const source = req.query.source || 'anilist';
|
||||||
const provider = req.query.provider;
|
const provider = req.query.provider;
|
||||||
|
const extensionBookId = req.query.extensionBookId;
|
||||||
|
|
||||||
const isExternal = source !== 'anilist';
|
const isExternal = source !== 'anilist';
|
||||||
return await booksService.getChaptersForBook(id, isExternal, provider);
|
return await booksService.getChaptersForBook(id, isExternal, provider, extensionBookId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return { chapters: [] };
|
return { chapters: [] };
|
||||||
|
|||||||
@@ -326,7 +326,8 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
|
|||||||
averageScore: m.rating || m.score || null,
|
averageScore: m.rating || m.score || null,
|
||||||
format: m.format,
|
format: m.format,
|
||||||
seasonYear: null,
|
seasonYear: null,
|
||||||
isExtensionResult: true
|
isExtensionResult: true,
|
||||||
|
url: m.url,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,42 +362,42 @@ async function fetchBookMetadata(id: string): Promise<Book | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, search: boolean, origin: string): Promise<ChapterWithProvider[]> {
|
async function searchChaptersInExtension(ext: Extension, name: string, lookupId: string, cacheId: string, search: boolean, origin: string, disableCache = false): Promise<ChapterWithProvider[]> {
|
||||||
const cacheKey = `chapters:${name}:${origin}:${search ? "search" : "id"}:${searchTitle}`;
|
const cacheKey = `chapters:${name}:${origin}:${search ? "search" : "id"}:${cacheId}`;
|
||||||
const cached = await getCache(cacheKey);
|
if (!disableCache) {
|
||||||
|
const cached = await getCache(cacheKey);
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
|
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
|
||||||
|
|
||||||
if (!isExpired) {
|
if (!isExpired) {
|
||||||
console.log(`[${name}] Chapters cache hit for: ${searchTitle}`);
|
console.log(`[${name}] Chapters cache hit for: ${lookupId}`);
|
||||||
try {
|
try {
|
||||||
return JSON.parse(cached.result) as ChapterWithProvider[];
|
return JSON.parse(cached.result) as ChapterWithProvider[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[${name}] Error parsing cached chapters:`, e);
|
console.error(`[${name}] Error parsing cached chapters:`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log(`[${name}] Chapters cache expired for: ${searchTitle}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[${name}] Searching chapters for: ${searchTitle}`);
|
console.log(`[${name}] Searching chapters for: ${lookupId}`);
|
||||||
|
|
||||||
let mediaId: string;
|
let mediaId: string;
|
||||||
if (search) {
|
if (search) {
|
||||||
const matches = await ext.search!({
|
const matches = await ext.search!({
|
||||||
query: searchTitle,
|
query: lookupId,
|
||||||
media: {
|
media: {
|
||||||
romajiTitle: searchTitle,
|
romajiTitle: lookupId,
|
||||||
englishTitle: searchTitle,
|
englishTitle: lookupId,
|
||||||
startDate: { year: 0, month: 0, day: 0 }
|
startDate: { year: 0, month: 0, day: 0 }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!matches?.length) return [];
|
if (!matches?.length) return [];
|
||||||
|
|
||||||
const nq = normalize(searchTitle);
|
const nq = normalize(lookupId);
|
||||||
|
|
||||||
const scored = matches.map(m => {
|
const scored = matches.map(m => {
|
||||||
const nt = normalize(m.title);
|
const nt = normalize(m.title);
|
||||||
@@ -414,7 +415,7 @@ async function searchChaptersInExtension(ext: Extension, name: string, searchTit
|
|||||||
mediaId = scored[0].m.id;
|
mediaId = scored[0].m.id;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const match = await ext.getMetadata(searchTitle);
|
const match = await ext.getMetadata(lookupId);
|
||||||
mediaId = match.id;
|
mediaId = match.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +446,7 @@ async function searchChaptersInExtension(ext: Extension, name: string, searchTit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getChaptersForBook(id: string, ext: Boolean, onlyProvider?: string): Promise<{ chapters: ChapterWithProvider[] }> {
|
export async function getChaptersForBook(id: string, ext: Boolean, onlyProvider?: string, extensionBookId?: string): Promise<{ chapters: ChapterWithProvider[] }> {
|
||||||
let bookData: Book | null = null;
|
let bookData: Book | null = null;
|
||||||
let searchTitle: string = "";
|
let searchTitle: string = "";
|
||||||
|
|
||||||
@@ -475,11 +476,30 @@ export async function getChaptersForBook(id: string, ext: Boolean, onlyProvider?
|
|||||||
|
|
||||||
for (const [name, ext] of bookExtensions) {
|
for (const [name, ext] of bookExtensions) {
|
||||||
if (onlyProvider && name !== onlyProvider) continue;
|
if (onlyProvider && name !== onlyProvider) continue;
|
||||||
if (name == extension) {
|
if (extensionBookId && name === onlyProvider) {
|
||||||
const chapters = await searchChaptersInExtension(ext, name, id, false, exts);
|
const targetId = extensionBookId ?? id;
|
||||||
|
|
||||||
|
const chapters = await searchChaptersInExtension(
|
||||||
|
ext,
|
||||||
|
name,
|
||||||
|
targetId, // lookup
|
||||||
|
id, // cache siempre con el id normal
|
||||||
|
false,
|
||||||
|
exts,
|
||||||
|
Boolean(extensionBookId)
|
||||||
|
);
|
||||||
|
|
||||||
allChapters.push(...chapters);
|
allChapters.push(...chapters);
|
||||||
} else {
|
} else {
|
||||||
const chapters = await searchChaptersInExtension(ext, name, searchTitle, true, exts);
|
const chapters = await searchChaptersInExtension(
|
||||||
|
ext,
|
||||||
|
name,
|
||||||
|
searchTitle,
|
||||||
|
id, // cache con id normal
|
||||||
|
true,
|
||||||
|
exts
|
||||||
|
);
|
||||||
|
|
||||||
allChapters.push(...chapters);
|
allChapters.push(...chapters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const AnimePlayer = (function() {
|
|||||||
let hlsInstance = null;
|
let hlsInstance = null;
|
||||||
|
|
||||||
let _manualExtensionId = null;
|
let _manualExtensionId = null;
|
||||||
let _searchTimeout = null;
|
|
||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
wrapper: null,
|
wrapper: null,
|
||||||
@@ -46,10 +45,6 @@ const AnimePlayer = (function() {
|
|||||||
dlConfirmBtn: null,
|
dlConfirmBtn: null,
|
||||||
dlCancelBtn: null,
|
dlCancelBtn: null,
|
||||||
manualMatchBtn: null,
|
manualMatchBtn: null,
|
||||||
matchModal: null,
|
|
||||||
matchInput: null,
|
|
||||||
matchList: null,
|
|
||||||
closeMatchModalBtn: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(animeId, initialSource, isLocal, animeData) {
|
function init(animeId, initialSource, isLocal, animeData) {
|
||||||
@@ -83,31 +78,6 @@ const AnimePlayer = (function() {
|
|||||||
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.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');
|
||||||
|
|
||||||
@@ -168,112 +138,33 @@ const AnimePlayer = (function() {
|
|||||||
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
||||||
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
||||||
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
||||||
|
if (els.manualMatchBtn) {
|
||||||
|
els.manualMatchBtn.addEventListener('click', openMatchModal);
|
||||||
|
}
|
||||||
|
|
||||||
loadExtensionsList();
|
loadExtensionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMatchModal() {
|
function openMatchModal() {
|
||||||
if (!els.matchModal) return;
|
const currentExt = els.extSelect.value;
|
||||||
|
if (!currentExt || currentExt === 'local') return;
|
||||||
|
|
||||||
// Limpiar contenido previo
|
MatchModal.open({
|
||||||
els.matchInput.value = '';
|
provider: currentExt,
|
||||||
els.matchList.innerHTML = `<div style="padding:20px; text-align:center; color:#777;">Type to search in ${els.extSelect.value}...</div>`;
|
initialQuery: _animeTitle, // Variable existente en player.js
|
||||||
|
onSearch: async (query, prov) => {
|
||||||
// 1. Mostrar el contenedor (para que el navegador calcule el layout)
|
const res = await fetch(`/api/search/${prov}?q=${encodeURIComponent(query)}`);
|
||||||
els.matchModal.style.display = 'flex';
|
const data = await res.json();
|
||||||
|
return data.results || [];
|
||||||
// 2. Pequeño delay o forzar reflow para que la transición de opacidad funcione
|
},
|
||||||
requestAnimationFrame(() => {
|
onSelect: (item) => {
|
||||||
els.matchModal.classList.add('show');
|
console.log("Selected Anime ID:", item.id);
|
||||||
});
|
_manualExtensionId = item.id;
|
||||||
|
loadStream();
|
||||||
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.");
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ let isLocal = false;
|
|||||||
let currentLanguage = null;
|
let currentLanguage = null;
|
||||||
let uniqueLanguages = [];
|
let uniqueLanguages = [];
|
||||||
let isSortAscending = true;
|
let isSortAscending = true;
|
||||||
|
let manualExtensionBookId = null;
|
||||||
|
|
||||||
const chapterPagination = Object.create(PaginationManager);
|
const chapterPagination = Object.create(PaginationManager);
|
||||||
chapterPagination.init(6, () => renderChapterList());
|
chapterPagination.init(6, () => renderChapterList());
|
||||||
@@ -36,6 +37,37 @@ async function init() {
|
|||||||
await loadChapters();
|
await loadChapters();
|
||||||
await setupAddToListButton();
|
await setupAddToListButton();
|
||||||
|
|
||||||
|
document.getElementById('manual-match-btn')?.addEventListener('click', () => {
|
||||||
|
const select = document.getElementById('provider-filter');
|
||||||
|
const provider = select.value;
|
||||||
|
|
||||||
|
// Obtener título para prellenar
|
||||||
|
const currentTitle = bookData?.title?.romaji || bookData?.title?.english || '';
|
||||||
|
|
||||||
|
MatchModal.open({
|
||||||
|
provider: provider,
|
||||||
|
initialQuery: currentTitle,
|
||||||
|
// Define CÓMO buscar
|
||||||
|
onSearch: async (query, prov) => {
|
||||||
|
const res = await fetch(`/api/search/books/${prov}?q=${encodeURIComponent(query)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return data.results || [];
|
||||||
|
},
|
||||||
|
// Define QUÉ hacer al seleccionar
|
||||||
|
onSelect: (item) => {
|
||||||
|
console.log("Selected Book ID:", item.id);
|
||||||
|
manualExtensionBookId = item.id;
|
||||||
|
|
||||||
|
// Lógica existente de tu book.js para recargar caps
|
||||||
|
loadChapters(provider);
|
||||||
|
|
||||||
|
// Feedback visual en el botón
|
||||||
|
const btn = document.getElementById('manual-match-btn');
|
||||||
|
if(btn) btn.style.color = '#22c55e';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Init Error:", err);
|
console.error("Init Error:", err);
|
||||||
showError("Error loading book");
|
showError("Error loading book");
|
||||||
@@ -311,6 +343,9 @@ async function loadChapters(targetProvider = null) {
|
|||||||
const source = extensionName || 'anilist';
|
const source = extensionName || 'anilist';
|
||||||
fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
|
fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
|
||||||
if (targetProvider !== 'all') fetchUrl += `&provider=${targetProvider}`;
|
if (targetProvider !== 'all') fetchUrl += `&provider=${targetProvider}`;
|
||||||
|
if (manualExtensionBookId && targetProvider !== 'all') {
|
||||||
|
fetchUrl += `&extensionBookId=${manualExtensionBookId}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(fetchUrl);
|
const res = await fetch(fetchUrl);
|
||||||
@@ -515,6 +550,8 @@ async function loadAvailableExtensions() {
|
|||||||
|
|
||||||
function setupProviderFilter() {
|
function setupProviderFilter() {
|
||||||
const select = document.getElementById('provider-filter');
|
const select = document.getElementById('provider-filter');
|
||||||
|
const manualBtn = document.getElementById('manual-match-btn'); // NUEVO
|
||||||
|
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
select.style.display = 'inline-block';
|
select.style.display = 'inline-block';
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
@@ -538,11 +575,32 @@ function setupProviderFilter() {
|
|||||||
select.appendChild(opt);
|
select.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lógica de selección inicial
|
||||||
if (isLocal) select.value = 'local';
|
if (isLocal) select.value = 'local';
|
||||||
else if (extensionName && availableExtensions.includes(extensionName)) select.value = extensionName;
|
else if (extensionName && availableExtensions.includes(extensionName)) select.value = extensionName;
|
||||||
else if (availableExtensions.length > 0) select.value = availableExtensions[0];
|
else if (availableExtensions.length > 0) select.value = availableExtensions[0];
|
||||||
|
|
||||||
select.onchange = () => loadChapters(select.value);
|
// Visibilidad inicial del botón manual
|
||||||
|
updateManualButtonVisibility(select.value);
|
||||||
|
|
||||||
|
select.onchange = () => {
|
||||||
|
// Al cambiar de proveedor, reseteamos la selección manual para evitar conflictos
|
||||||
|
manualExtensionBookId = null;
|
||||||
|
updateManualButtonVisibility(select.value);
|
||||||
|
loadChapters(select.value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateManualButtonVisibility(provider) {
|
||||||
|
const btn = document.getElementById('manual-match-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
// Solo mostrar si es un proveedor específico (no 'all' ni 'local')
|
||||||
|
if (provider !== 'all' && provider !== 'local') {
|
||||||
|
btn.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
btn.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateExtensionPill() {
|
function updateExtensionPill() {
|
||||||
@@ -569,9 +627,9 @@ function updateCustomAddButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupModalClickOutside() {
|
function setupModalClickOutside() {
|
||||||
const modal = document.getElementById('add-list-modal');
|
const addListModal = document.getElementById('add-list-modal');
|
||||||
if (modal) {
|
if (addListModal) {
|
||||||
modal.addEventListener('click', (e) => {
|
addListModal.addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'add-list-modal') ListModalManager.close();
|
if (e.target.id === 'add-list-modal') ListModalManager.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
171
docker/src/scripts/utils/match-modal.js
Normal file
171
docker/src/scripts/utils/match-modal.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
const MatchModal = (function() {
|
||||||
|
let _config = {
|
||||||
|
onSearch: async (query, provider) => [], // Debe devolver Array de objetos
|
||||||
|
onSelect: (item, provider) => {},
|
||||||
|
provider: 'generic'
|
||||||
|
};
|
||||||
|
|
||||||
|
let elements = {};
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (document.getElementById('waifu-match-modal')) return;
|
||||||
|
|
||||||
|
// Inyectar HTML
|
||||||
|
const modalHTML = `
|
||||||
|
<div class="match-modal-overlay" id="waifu-match-modal">
|
||||||
|
<div class="match-modal-content">
|
||||||
|
<div class="match-header">
|
||||||
|
<h3 class="match-title">Manual Match <span id="match-provider-badge" style="opacity:0.6; font-size:0.8em; margin-left:8px;"></span></h3>
|
||||||
|
<button class="match-close-btn" id="match-close-btn">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="match-search-container">
|
||||||
|
<input type="text" id="match-input" class="match-input" placeholder="Search title..." autocomplete="off">
|
||||||
|
<button id="match-btn-action" class="match-search-btn">Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="match-results-body" id="match-results-container">
|
||||||
|
<div class="match-msg">Type to start searching...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
|
||||||
|
// Cachear elementos
|
||||||
|
elements = {
|
||||||
|
overlay: document.getElementById('waifu-match-modal'),
|
||||||
|
input: document.getElementById('match-input'),
|
||||||
|
results: document.getElementById('match-results-container'),
|
||||||
|
badge: document.getElementById('match-provider-badge'),
|
||||||
|
closeBtn: document.getElementById('match-close-btn'),
|
||||||
|
searchBtn: document.getElementById('match-btn-action')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
elements.closeBtn.onclick = close;
|
||||||
|
elements.overlay.onclick = (e) => { if(e.target === elements.overlay) close(); };
|
||||||
|
|
||||||
|
// Búsqueda al hacer clic
|
||||||
|
elements.searchBtn.onclick = () => performSearch(elements.input.value);
|
||||||
|
|
||||||
|
// Búsqueda al escribir (Debounce)
|
||||||
|
elements.input.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
if(e.target.value.trim().length === 0) return;
|
||||||
|
searchTimeout = setTimeout(() => performSearch(e.target.value), 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter key
|
||||||
|
elements.input.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') performSearch(elements.input.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(options) {
|
||||||
|
init(); // Asegurar que el DOM existe
|
||||||
|
|
||||||
|
_config = { ..._config, ...options };
|
||||||
|
|
||||||
|
// Resetear UI
|
||||||
|
elements.input.value = options.initialQuery || '';
|
||||||
|
elements.results.innerHTML = '<div class="match-msg">Search above to find matches...</div>';
|
||||||
|
elements.badge.innerText = options.provider ? `(${options.provider})` : '';
|
||||||
|
|
||||||
|
// Mostrar Modal
|
||||||
|
elements.overlay.classList.add('active');
|
||||||
|
|
||||||
|
// Auto-search si hay query inicial
|
||||||
|
if (options.initialQuery) {
|
||||||
|
performSearch(options.initialQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => elements.input.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if(elements.overlay) elements.overlay.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performSearch(query) {
|
||||||
|
if (!query || query.trim().length < 2) return;
|
||||||
|
|
||||||
|
elements.results.innerHTML = '<div class="match-spinner"></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ejecutar la función de búsqueda pasada en la config
|
||||||
|
const results = await _config.onSearch(query, _config.provider);
|
||||||
|
renderResults(results);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
elements.results.innerHTML = '<div class="match-msg error">Error searching provider.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(results) {
|
||||||
|
elements.results.innerHTML = '';
|
||||||
|
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
elements.results.innerHTML = '<div class="match-msg">No matches found.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = document.createElement('div');
|
||||||
|
grid.className = 'match-list-grid';
|
||||||
|
|
||||||
|
results.forEach(item => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'match-item';
|
||||||
|
|
||||||
|
// Normalización de datos para asegurar compatibilidad con Anime/Libros
|
||||||
|
const img = item.coverImage?.large || item.coverImage || item.image || '/public/assets/no-image.png';
|
||||||
|
const title = item.title?.english || item.title?.romaji || item.title || 'Unknown Title';
|
||||||
|
const meta = item.releaseDate || item.year || item.startDate?.year || '';
|
||||||
|
const url = item.url || item.externalUrl || null;
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<img src="${img}" class="match-poster" loading="lazy">
|
||||||
|
<div class="match-info">
|
||||||
|
<div class="match-item-title">${title}</div>
|
||||||
|
<div class="match-item-meta">${meta}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Botón de enlace externo (si existe URL)
|
||||||
|
if (url) {
|
||||||
|
const linkBtn = document.createElement('a');
|
||||||
|
linkBtn.href = url;
|
||||||
|
linkBtn.target = "_blank";
|
||||||
|
linkBtn.className = "match-link-btn";
|
||||||
|
linkBtn.title = "View Source";
|
||||||
|
linkBtn.innerHTML = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
// Evitar que el click en el enlace dispare el select
|
||||||
|
linkBtn.onclick = (e) => e.stopPropagation();
|
||||||
|
el.appendChild(linkBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click en la tarjeta selecciona
|
||||||
|
el.onclick = () => {
|
||||||
|
_config.onSelect(item);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
grid.appendChild(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.results.appendChild(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
close
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<link rel="stylesheet" href="/views/css/components/anilist-modal.css" />
|
<link rel="stylesheet" href="/views/css/components/anilist-modal.css" />
|
||||||
<link rel="stylesheet" href="/views/css/anime/anime.css" />
|
<link rel="stylesheet" href="/views/css/anime/anime.css" />
|
||||||
<link rel="stylesheet" href="/views/css/anime/player.css" />
|
<link rel="stylesheet" href="/views/css/anime/player.css" />
|
||||||
|
<link rel="stylesheet" href="/views/css/components/match-modal.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="/anime" class="back-btn">
|
<a href="/anime" class="back-btn">
|
||||||
@@ -80,21 +81,6 @@
|
|||||||
<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">
|
||||||
@@ -212,6 +198,7 @@
|
|||||||
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
|
<script src="/src/scripts/utils/match-modal.js"></script>
|
||||||
|
|
||||||
<script src="/src/scripts/anime/player.js"></script>
|
<script src="/src/scripts/anime/player.js"></script>
|
||||||
<script src="/src/scripts/anime/entry.js"></script>
|
<script src="/src/scripts/anime/entry.js"></script>
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
<title>WaifuBoard Book</title>
|
<title>WaifuBoard Book</title>
|
||||||
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon">
|
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="/views/css/globals.css">
|
<link rel="stylesheet" href="/views/css/globals.css">
|
||||||
<link rel="stylesheet" href="/views/css/components/anilist-modal.css">
|
|
||||||
<link rel="stylesheet" href="/views/css/books/book.css">
|
<link rel="stylesheet" href="/views/css/books/book.css">
|
||||||
|
<link rel="stylesheet" href="/views/css/components/anilist-modal.css">
|
||||||
<link rel="stylesheet" href="/views/css/components/updateNotifier.css">
|
<link rel="stylesheet" href="/views/css/components/updateNotifier.css">
|
||||||
|
<link rel="stylesheet" href="/views/css/components/match-modal.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -80,6 +81,12 @@
|
|||||||
<h2>Chapters</h2>
|
<h2>Chapters</h2>
|
||||||
|
|
||||||
<div class="chapter-controls">
|
<div class="chapter-controls">
|
||||||
|
<button id="manual-match-btn" class="glass-btn-icon" style="display: none;" title="Manual Match">
|
||||||
|
<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>
|
||||||
<select id="provider-filter" class="glass-select" style="display: none;">
|
<select id="provider-filter" class="glass-select" style="display: none;">
|
||||||
<option value="all">All Providers</option>
|
<option value="all">All Providers</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -109,9 +116,6 @@
|
|||||||
<button class="page-btn" id="next-page">Next</button>
|
<button class="page-btn" id="next-page">Next</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -139,6 +143,7 @@
|
|||||||
<script src="/src/scripts/utils/pagination-manager.js"></script>
|
<script src="/src/scripts/utils/pagination-manager.js"></script>
|
||||||
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
<script src="/src/scripts/utils/media-metadata-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
|
<script src="/src/scripts/utils/match-modal.js"></script>
|
||||||
<script src="/src/scripts/books/book.js"></script>
|
<script src="/src/scripts/books/book.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -936,77 +936,3 @@ body.stop-scrolling {
|
|||||||
border-color: var(--brand-color);
|
border-color: var(--brand-color);
|
||||||
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
|
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;
|
|
||||||
}
|
|
||||||
282
docker/views/css/components/match-modal.css
Normal file
282
docker/views/css/components/match-modal.css
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/* match-modal.css */
|
||||||
|
:root {
|
||||||
|
--match-primary: #8b5cf6;
|
||||||
|
--match-bg: rgba(15, 15, 15, 0.95);
|
||||||
|
--match-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--match-input-bg: rgba(0, 0, 0, 0.4);
|
||||||
|
--match-text-muted: #a1a1aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay Global con efecto Glass */
|
||||||
|
.match-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-modal-overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contenido del Modal */
|
||||||
|
.match-modal-content {
|
||||||
|
background: var(--match-bg);
|
||||||
|
border: 1px solid var(--match-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px; /* Un poco más ancho para mejor lectura */
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow:
|
||||||
|
0 25px 80px rgba(0, 0, 0, 0.9),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
transform: scale(0.95) translateY(10px);
|
||||||
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-modal-overlay.active .match-modal-content {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Estilizado */
|
||||||
|
.match-header {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--match-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(to bottom, rgba(255,255,255,0.02), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
/* Efecto gradiente en el texto como en anilist-modal */
|
||||||
|
background: linear-gradient(to right, #fff, #aaa);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#match-provider-badge {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
-webkit-text-fill-color: #888; /* Resetear el gradiente para el badge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-close-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid var(--match-border);
|
||||||
|
color: #ccc;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-close-btn:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
border-color: #ef4444;
|
||||||
|
color: white;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buscador y Controles */
|
||||||
|
.match-search-container {
|
||||||
|
padding: 1.5rem 2rem 1rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--match-input-bg);
|
||||||
|
border: 1px solid var(--match-border);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-input:focus {
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
border-color: var(--match-primary);
|
||||||
|
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-search-btn {
|
||||||
|
padding: 0 1.8rem;
|
||||||
|
background: var(--match-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-search-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lista de Resultados */
|
||||||
|
.match-results-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem 2rem 2rem 2rem;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-results-body::-webkit-scrollbar { width: 6px; }
|
||||||
|
.match-results-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 10px; }
|
||||||
|
|
||||||
|
.match-list-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tarjeta de Resultado Mejorada */
|
||||||
|
.match-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-poster {
|
||||||
|
width: 50px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #222;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item-meta {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--match-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botón de enlace externo refinado */
|
||||||
|
.match-link-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-link-btn:hover {
|
||||||
|
background: var(--match-primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--match-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 0 15px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mensajes y Spinner */
|
||||||
|
.match-spinner {
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
border: 3px solid rgba(255,255,255,0.1);
|
||||||
|
border-top-color: var(--match-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin: 3rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
.match-msg {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--match-text-muted);
|
||||||
|
margin-top: 3rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.match-msg.error { color: #ef4444; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.match-modal-content {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.match-search-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
.match-search-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user