added manual match modal for local library
This commit is contained in:
@@ -91,8 +91,7 @@ export async function scanLibrary(request: FastifyRequest<{ Querystring: ScanQue
|
|||||||
export async function listEntries(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) {
|
export async function listEntries(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { type } = request.params;
|
const { type } = request.params;
|
||||||
const entries = await service.getEntriesByType(type);
|
return await service.getEntriesByType(type);
|
||||||
return entries;
|
|
||||||
} catch {
|
} catch {
|
||||||
return reply.status(500).send({ error: 'FAILED_TO_LIST_ENTRIES' });
|
return reply.status(500).send({ error: 'FAILED_TO_LIST_ENTRIES' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,8 +186,22 @@ export async function performLibraryScan(mode: 'full' | 'incremental' = 'increme
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntriesByType(type: string) {
|
export async function getEntriesByType(type: string) {
|
||||||
const entries = await queryAll(`SELECT * FROM local_entries WHERE type = ?`, [type], 'local_library');
|
const sql = `
|
||||||
return await Promise.all(entries.map((entry: any) => resolveEntryMetadata(entry, type)));
|
SELECT local_entries.*, COUNT(local_files.id) as file_count
|
||||||
|
FROM local_entries
|
||||||
|
LEFT JOIN local_files ON local_entries.id = local_files.entry_id
|
||||||
|
WHERE local_entries.type = ?
|
||||||
|
GROUP BY local_entries.id
|
||||||
|
`;
|
||||||
|
const entries = await queryAll(sql, [type], 'local_library');
|
||||||
|
return await Promise.all(entries.map(async (entry: any) => {
|
||||||
|
const metadata = await resolveEntryMetadata(entry, type);
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
path: entry.path,
|
||||||
|
files: entry.file_count
|
||||||
|
};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntryDetails(type: string, id: string) {
|
export async function getEntryDetails(type: string, id: string) {
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ const DashboardApp = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Library: {
|
Library: {
|
||||||
|
tempMatchContext: null,
|
||||||
loadStats: async function() {
|
loadStats: async function() {
|
||||||
const types = ['anime', 'manga', 'novels'];
|
const types = ['anime', 'manga', 'novels'];
|
||||||
const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' };
|
const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' };
|
||||||
@@ -446,7 +447,7 @@ const DashboardApp = {
|
|||||||
const meta = entry.metadata || {};
|
const meta = entry.metadata || {};
|
||||||
let poster = meta.coverImage?.large || '/public/assets/placeholder.svg';
|
let poster = meta.coverImage?.large || '/public/assets/placeholder.svg';
|
||||||
|
|
||||||
let title = isMatched ? (meta.title?.english || meta.title?.romaji) : entry.folder_name;
|
let title = isMatched ? (meta.title?.english || meta.title?.romaji) : entry.path;
|
||||||
if (!isMatched) title = title.replace(/\[.*?\]|\(.*?\)|\.mkv|\.mp4/g, '').trim();
|
if (!isMatched) title = title.replace(/\[.*?\]|\(.*?\)|\.mkv|\.mp4/g, '').trim();
|
||||||
|
|
||||||
const url = isMatched ? (type === 'anime' ? `/anime/${meta.id}` : `/book/${meta.id}`) : '#';
|
const url = isMatched ? (type === 'anime' ? `/anime/${meta.id}` : `/book/${meta.id}`) : '#';
|
||||||
@@ -459,12 +460,12 @@ const DashboardApp = {
|
|||||||
${!isMatched ? `<div class="unmatched-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg> UNMATCHED</div>` : ''}
|
${!isMatched ? `<div class="unmatched-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg> UNMATCHED</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
<h3 class="item-title" title="${entry.folder_name}">${title}</h3>
|
<h3 class="item-title" title="${entry.path}">${title}</h3>
|
||||||
<div class="item-meta">
|
<div class="item-meta">
|
||||||
<span class="meta-pill type-pill">${entry.files ? entry.files.length : 0} FILES</span>
|
<span class="meta-pill type-pill">${entry.files} FILES</span>
|
||||||
${isMatched ? '<span class="meta-pill status-pill">MATCHED</span>' : ''}
|
${isMatched ? '<span class="meta-pill status-pill">MATCHED</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="folder-path-tooltip">${entry.folder_name}</div>
|
<div class="folder-path-tooltip">${entry.path}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="edit-icon-btn" onclick="DashboardApp.Library.openManualMatch('${entry.id}', '${type}')" title="${isMatched ? 'Rematch' : 'Fix Match'}">
|
<button class="edit-icon-btn" onclick="DashboardApp.Library.openManualMatch('${entry.id}', '${type}')" title="${isMatched ? 'Rematch' : 'Fix Match'}">
|
||||||
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||||
@@ -505,16 +506,67 @@ const DashboardApp = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openManualMatch: function(id, type) {
|
openManualMatch: function(id, type) {
|
||||||
const newId = prompt("Enter AniList ID to force match:");
|
const item = DashboardApp.State.localLibraryData.find(x => x.id === id);
|
||||||
if (newId) {
|
const pathName = item ? item.path : 'Unknown path';
|
||||||
fetch(`${API_BASE}/library/${type}/${id}/match`, {
|
|
||||||
|
this.tempMatchContext = { id, type };
|
||||||
|
|
||||||
|
document.getElementById('manual-match-path').textContent = pathName;
|
||||||
|
document.getElementById('manual-match-id').value = '';
|
||||||
|
|
||||||
|
const modal = document.getElementById('manual-match-modal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
|
||||||
|
setTimeout(() => document.getElementById('manual-match-id').focus(), 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
closeManualMatch: function() {
|
||||||
|
document.getElementById('manual-match-modal').classList.add('hidden');
|
||||||
|
this.tempMatchContext = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
submitManualMatch: async function() {
|
||||||
|
if (!this.tempMatchContext) return;
|
||||||
|
|
||||||
|
const newId = document.getElementById('manual-match-id').value;
|
||||||
|
if (!newId) {
|
||||||
|
alert("Please enter a valid ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, type } = this.tempMatchContext;
|
||||||
|
|
||||||
|
const confirmBtn = document.querySelector('#manual-match-modal .btn-primary');
|
||||||
|
const originalText = confirmBtn.textContent;
|
||||||
|
confirmBtn.textContent = "Matching...";
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/library/${type}/${id}/match`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' },
|
headers: {
|
||||||
body: JSON.stringify({ source: 'anilist', matched_id: parseInt(newId) })
|
...window.AuthUtils.getSimpleAuthHeaders(),
|
||||||
}).then(res => {
|
'Content-Type': 'application/json'
|
||||||
if(res.ok) { alert("Matched! Refreshing..."); this.loadContent(type); }
|
},
|
||||||
else { alert("Failed to match."); }
|
body: JSON.stringify({
|
||||||
|
source: 'anilist',
|
||||||
|
matched_id: parseInt(newId)
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(res.ok) {
|
||||||
|
this.closeManualMatch();
|
||||||
|
this.loadContent(type);
|
||||||
|
} else {
|
||||||
|
const errData = await res.json();
|
||||||
|
alert("Failed to match: " + (errData.error || "Unknown error"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Connection error");
|
||||||
|
} finally {
|
||||||
|
confirmBtn.textContent = originalText;
|
||||||
|
confirmBtn.disabled = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -705,3 +705,94 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
8. CUSTOM MODAL (Manual Match)
|
||||||
|
========================================= */
|
||||||
|
.custom-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-overlay.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-content {
|
||||||
|
background: var(--color-bg-elevated, #18181b);
|
||||||
|
border: 1px solid var(--border-medium, rgba(255,255,255,0.1));
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transform: scale(1);
|
||||||
|
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-overlay.hidden .custom-modal-content {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 { margin: 0; font-size: 1.25rem; }
|
||||||
|
|
||||||
|
.close-modal-btn {
|
||||||
|
background: transparent; border: none;
|
||||||
|
color: var(--color-text-muted); font-size: 1.5rem;
|
||||||
|
cursor: pointer; line-height: 1;
|
||||||
|
}
|
||||||
|
.close-modal-btn:hover { color: white; }
|
||||||
|
|
||||||
|
.modal-body { padding: 1.5rem; }
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.9rem; margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-display {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #e4e4e7;
|
||||||
|
word-break: break-all;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group { margin-bottom: 1.5rem; }
|
||||||
|
.input-group label {
|
||||||
|
display: block; font-size: 0.85rem;
|
||||||
|
font-weight: 600; color: var(--color-text-secondary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
@@ -194,7 +194,33 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="manual-match-modal" class="custom-modal-overlay hidden">
|
||||||
|
<div class="custom-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Fix Match</h3>
|
||||||
|
<button class="close-modal-btn" onclick="DashboardApp.Library.closeManualMatch()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-description">Introduce el ID de AniList correcto para asociar este archivo local.</p>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>File Path / Folder Name</label>
|
||||||
|
<div id="manual-match-path" class="path-display"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>AniList ID</label>
|
||||||
|
<input type="number" id="manual-match-id" class="stream-input" placeholder="Ej: 21 (One Piece)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-secondary" onclick="DashboardApp.Library.closeManualMatch()">Cancel</button>
|
||||||
|
<button class="btn-primary" onclick="DashboardApp.Library.submitManualMatch()">Confirm Match</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="updateToast" class="hidden">
|
<div id="updateToast" class="hidden">
|
||||||
<p>Update available: <span id="latestVersionDisplay">v1.x</span></p>
|
<p>Update available: <span id="latestVersionDisplay">v1.x</span></p>
|
||||||
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ export async function scanLibrary(request: FastifyRequest<{ Querystring: ScanQue
|
|||||||
export async function listEntries(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) {
|
export async function listEntries(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { type } = request.params;
|
const { type } = request.params;
|
||||||
const entries = await service.getEntriesByType(type);
|
return await service.getEntriesByType(type);
|
||||||
return entries;
|
|
||||||
} catch {
|
} catch {
|
||||||
return reply.status(500).send({ error: 'FAILED_TO_LIST_ENTRIES' });
|
return reply.status(500).send({ error: 'FAILED_TO_LIST_ENTRIES' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,8 +186,22 @@ export async function performLibraryScan(mode: 'full' | 'incremental' = 'increme
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntriesByType(type: string) {
|
export async function getEntriesByType(type: string) {
|
||||||
const entries = await queryAll(`SELECT * FROM local_entries WHERE type = ?`, [type], 'local_library');
|
const sql = `
|
||||||
return await Promise.all(entries.map((entry: any) => resolveEntryMetadata(entry, type)));
|
SELECT local_entries.*, COUNT(local_files.id) as file_count
|
||||||
|
FROM local_entries
|
||||||
|
LEFT JOIN local_files ON local_entries.id = local_files.entry_id
|
||||||
|
WHERE local_entries.type = ?
|
||||||
|
GROUP BY local_entries.id
|
||||||
|
`;
|
||||||
|
const entries = await queryAll(sql, [type], 'local_library');
|
||||||
|
return await Promise.all(entries.map(async (entry: any) => {
|
||||||
|
const metadata = await resolveEntryMetadata(entry, type);
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
path: entry.path,
|
||||||
|
files: entry.file_count
|
||||||
|
};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntryDetails(type: string, id: string) {
|
export async function getEntryDetails(type: string, id: string) {
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ const DashboardApp = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Library: {
|
Library: {
|
||||||
|
tempMatchContext: null,
|
||||||
loadStats: async function() {
|
loadStats: async function() {
|
||||||
const types = ['anime', 'manga', 'novels'];
|
const types = ['anime', 'manga', 'novels'];
|
||||||
const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' };
|
const elements = { 'anime': 'local-anime-count', 'manga': 'local-manga-count', 'novels': 'local-novel-count' };
|
||||||
@@ -446,7 +447,7 @@ const DashboardApp = {
|
|||||||
const meta = entry.metadata || {};
|
const meta = entry.metadata || {};
|
||||||
let poster = meta.coverImage?.large || '/public/assets/placeholder.svg';
|
let poster = meta.coverImage?.large || '/public/assets/placeholder.svg';
|
||||||
|
|
||||||
let title = isMatched ? (meta.title?.english || meta.title?.romaji) : entry.folder_name;
|
let title = isMatched ? (meta.title?.english || meta.title?.romaji) : entry.path;
|
||||||
if (!isMatched) title = title.replace(/\[.*?\]|\(.*?\)|\.mkv|\.mp4/g, '').trim();
|
if (!isMatched) title = title.replace(/\[.*?\]|\(.*?\)|\.mkv|\.mp4/g, '').trim();
|
||||||
|
|
||||||
const url = isMatched ? (type === 'anime' ? `/anime/${meta.id}` : `/book/${meta.id}`) : '#';
|
const url = isMatched ? (type === 'anime' ? `/anime/${meta.id}` : `/book/${meta.id}`) : '#';
|
||||||
@@ -459,12 +460,12 @@ const DashboardApp = {
|
|||||||
${!isMatched ? `<div class="unmatched-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg> UNMATCHED</div>` : ''}
|
${!isMatched ? `<div class="unmatched-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg> UNMATCHED</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
<h3 class="item-title" title="${entry.folder_name}">${title}</h3>
|
<h3 class="item-title" title="${entry.path}">${title}</h3>
|
||||||
<div class="item-meta">
|
<div class="item-meta">
|
||||||
<span class="meta-pill type-pill">${entry.files ? entry.files.length : 0} FILES</span>
|
<span class="meta-pill type-pill">${entry.files} FILES</span>
|
||||||
${isMatched ? '<span class="meta-pill status-pill">MATCHED</span>' : ''}
|
${isMatched ? '<span class="meta-pill status-pill">MATCHED</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="folder-path-tooltip">${entry.folder_name}</div>
|
<div class="folder-path-tooltip">${entry.path}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="edit-icon-btn" onclick="DashboardApp.Library.openManualMatch('${entry.id}', '${type}')" title="${isMatched ? 'Rematch' : 'Fix Match'}">
|
<button class="edit-icon-btn" onclick="DashboardApp.Library.openManualMatch('${entry.id}', '${type}')" title="${isMatched ? 'Rematch' : 'Fix Match'}">
|
||||||
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||||
@@ -505,16 +506,67 @@ const DashboardApp = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openManualMatch: function(id, type) {
|
openManualMatch: function(id, type) {
|
||||||
const newId = prompt("Enter AniList ID to force match:");
|
const item = DashboardApp.State.localLibraryData.find(x => x.id === id);
|
||||||
if (newId) {
|
const pathName = item ? item.path : 'Unknown path';
|
||||||
fetch(`${API_BASE}/library/${type}/${id}/match`, {
|
|
||||||
|
this.tempMatchContext = { id, type };
|
||||||
|
|
||||||
|
document.getElementById('manual-match-path').textContent = pathName;
|
||||||
|
document.getElementById('manual-match-id').value = '';
|
||||||
|
|
||||||
|
const modal = document.getElementById('manual-match-modal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
|
||||||
|
setTimeout(() => document.getElementById('manual-match-id').focus(), 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
closeManualMatch: function() {
|
||||||
|
document.getElementById('manual-match-modal').classList.add('hidden');
|
||||||
|
this.tempMatchContext = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
submitManualMatch: async function() {
|
||||||
|
if (!this.tempMatchContext) return;
|
||||||
|
|
||||||
|
const newId = document.getElementById('manual-match-id').value;
|
||||||
|
if (!newId) {
|
||||||
|
alert("Please enter a valid ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, type } = this.tempMatchContext;
|
||||||
|
|
||||||
|
const confirmBtn = document.querySelector('#manual-match-modal .btn-primary');
|
||||||
|
const originalText = confirmBtn.textContent;
|
||||||
|
confirmBtn.textContent = "Matching...";
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/library/${type}/${id}/match`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...window.AuthUtils.getSimpleAuthHeaders(), 'Content-Type': 'application/json' },
|
headers: {
|
||||||
body: JSON.stringify({ source: 'anilist', matched_id: parseInt(newId) })
|
...window.AuthUtils.getSimpleAuthHeaders(),
|
||||||
}).then(res => {
|
'Content-Type': 'application/json'
|
||||||
if(res.ok) { alert("Matched! Refreshing..."); this.loadContent(type); }
|
},
|
||||||
else { alert("Failed to match."); }
|
body: JSON.stringify({
|
||||||
|
source: 'anilist',
|
||||||
|
matched_id: parseInt(newId)
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(res.ok) {
|
||||||
|
this.closeManualMatch();
|
||||||
|
this.loadContent(type);
|
||||||
|
} else {
|
||||||
|
const errData = await res.json();
|
||||||
|
alert("Failed to match: " + (errData.error || "Unknown error"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Connection error");
|
||||||
|
} finally {
|
||||||
|
confirmBtn.textContent = originalText;
|
||||||
|
confirmBtn.disabled = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -705,3 +705,94 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
8. CUSTOM MODAL (Manual Match)
|
||||||
|
========================================= */
|
||||||
|
.custom-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-overlay.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-content {
|
||||||
|
background: var(--color-bg-elevated, #18181b);
|
||||||
|
border: 1px solid var(--border-medium, rgba(255,255,255,0.1));
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transform: scale(1);
|
||||||
|
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal-overlay.hidden .custom-modal-content {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 { margin: 0; font-size: 1.25rem; }
|
||||||
|
|
||||||
|
.close-modal-btn {
|
||||||
|
background: transparent; border: none;
|
||||||
|
color: var(--color-text-muted); font-size: 1.5rem;
|
||||||
|
cursor: pointer; line-height: 1;
|
||||||
|
}
|
||||||
|
.close-modal-btn:hover { color: white; }
|
||||||
|
|
||||||
|
.modal-body { padding: 1.5rem; }
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.9rem; margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-display {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #e4e4e7;
|
||||||
|
word-break: break-all;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group { margin-bottom: 1.5rem; }
|
||||||
|
.input-group label {
|
||||||
|
display: block; font-size: 0.85rem;
|
||||||
|
font-weight: 600; color: var(--color-text-secondary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
@@ -181,7 +181,33 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="manual-match-modal" class="custom-modal-overlay hidden">
|
||||||
|
<div class="custom-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Fix Match</h3>
|
||||||
|
<button class="close-modal-btn" onclick="DashboardApp.Library.closeManualMatch()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-description">Introduce el ID de AniList correcto para asociar este archivo local.</p>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>File Path / Folder Name</label>
|
||||||
|
<div id="manual-match-path" class="path-display"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>AniList ID</label>
|
||||||
|
<input type="number" id="manual-match-id" class="stream-input" placeholder="Ej: 21 (One Piece)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-secondary" onclick="DashboardApp.Library.closeManualMatch()">Cancel</button>
|
||||||
|
<button class="btn-primary" onclick="DashboardApp.Library.submitManualMatch()">Confirm Match</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="updateToast" class="hidden">
|
<div id="updateToast" class="hidden">
|
||||||
<p>Update available: <span id="latestVersionDisplay">v1.x</span></p>
|
<p>Update available: <span id="latestVersionDisplay">v1.x</span></p>
|
||||||
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user