Move search controls and add result modal
This commit is contained in:
+113
-59
@@ -120,7 +120,7 @@ INDEX_HTML = r"""<!doctype html>
|
||||
}
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 380px) 1fr;
|
||||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
aside, main {
|
||||
@@ -173,16 +173,6 @@ INDEX_HTML = r"""<!doctype html>
|
||||
.controls {
|
||||
align-items: end;
|
||||
}
|
||||
.search-box {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, rgba(31, 24, 50, 0.86), rgba(17, 13, 29, 0.92));
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
.page-links {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -233,18 +223,26 @@ INDEX_HTML = r"""<!doctype html>
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
.settings, .episodes-panel, .results-panel, .deps {
|
||||
.settings, .search-panel, .episodes-panel, .results-panel, .deps {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, rgba(29, 22, 47, 0.88), rgba(17, 13, 29, 0.94));
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
.settings, .episodes-panel, .results-panel, .deps {
|
||||
.settings, .search-panel, .episodes-panel, .results-panel, .deps {
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
.search-panel {
|
||||
align-items: end;
|
||||
}
|
||||
.search-panel .controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(170px, 220px) auto;
|
||||
align-items: end;
|
||||
}
|
||||
.results-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -385,6 +383,31 @@ INDEX_HTML = r"""<!doctype html>
|
||||
color: #ddd0ff;
|
||||
font-size: 13px;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 22px;
|
||||
background: rgba(4, 2, 10, 0.68);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.modal-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.modal-window {
|
||||
width: min(980px, 100%);
|
||||
max-height: min(840px, calc(100vh - 44px));
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, rgba(29, 22, 47, 0.98), rgba(13, 9, 24, 0.98));
|
||||
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.52);
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
.results-panel .toolbar {
|
||||
align-items: end;
|
||||
}
|
||||
@@ -403,6 +426,7 @@ INDEX_HTML = r"""<!doctype html>
|
||||
.app { grid-template-columns: 1fr; }
|
||||
aside { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.controls, .row, .toolbar { align-items: stretch; flex-direction: column; }
|
||||
.search-panel .controls { grid-template-columns: 1fr; }
|
||||
.results-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
@@ -418,50 +442,71 @@ INDEX_HTML = r"""<!doctype html>
|
||||
"Search",
|
||||
badge_id="serverBadge",
|
||||
) + render_page_links("search") + r"""
|
||||
|
||||
<div class="search-box">
|
||||
<label for="search-query">Anime Title / Keyword</label>
|
||||
<input type="text" id="search-query" placeholder="e.g., Attack on Titan">
|
||||
|
||||
<label for="search-quality">Quality</label>
|
||||
<select id="search-quality">
|
||||
<option value="best">Best</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
|
||||
<button class="primary" id="searchBtn" type="button">Search</button>
|
||||
</div>
|
||||
<div class="notice" id="notice"></div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<section class="episodes-panel" id="episodesPanel">
|
||||
<section class="search-panel">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h2>Search anime</h2>
|
||||
<p class="muted">Choose a provider and search across subbed and dubbed results.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label for="search-query">Anime Title / Keyword
|
||||
<input type="text" id="search-query" placeholder="e.g., Attack on Titan">
|
||||
</label>
|
||||
<label for="searchProvider">Provider
|
||||
<select id="searchProvider">
|
||||
<option value="anikoto">Anikoto</option>
|
||||
<option value="anineko">AniNeko</option>
|
||||
<option value="pahe">AnimePahe</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary" id="searchBtn" type="button">Search</button>
|
||||
</div>
|
||||
<div class="notice" id="notice"></div>
|
||||
</section>
|
||||
<section class="results-panel">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h2>Select results</h2>
|
||||
<p class="muted" id="resultsMeta">Poster cards appear here after a search.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="results-grid" id="results"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="modal-overlay" id="selectionModal" hidden>
|
||||
<section class="modal-window" role="dialog" aria-modal="true" aria-labelledby="selectedTitle">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h2 id="selectedTitle">Select a result</h2>
|
||||
<p class="muted" id="selectedMeta">Episodes will appear here.</p>
|
||||
</div>
|
||||
<div class="row" style="flex:0 0 auto">
|
||||
<label style="min-width: 180px;">Watchlist category
|
||||
<select id="trackCategory">
|
||||
<option value="watching">Watching</option>
|
||||
<option value="planned">Planned</option>
|
||||
<option value="finished">Finished</option>
|
||||
<option value="dropped">Dropped</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="min-width: 160px;">Media type
|
||||
<select id="trackMediaType">
|
||||
<option value="tv">TV</option>
|
||||
<option value="movie">Movie</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="ghost" id="closeSelectionBtn" type="button">Close</button>
|
||||
<button class="ghost" id="trackBtn" type="button" disabled>Add to watchlist</button>
|
||||
<button class="primary" id="addBtn" type="button" disabled>Add to queue</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Watchlist category
|
||||
<select id="trackCategory">
|
||||
<option value="watching">Watching</option>
|
||||
<option value="planned">Planned</option>
|
||||
<option value="finished">Finished</option>
|
||||
<option value="dropped">Dropped</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Media type
|
||||
<select id="trackMediaType">
|
||||
<option value="tv">TV</option>
|
||||
<option value="movie">Movie</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Library name
|
||||
<input id="animeNameInput" placeholder="Anime name">
|
||||
@@ -487,21 +532,12 @@ INDEX_HTML = r"""<!doctype html>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="results-panel">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h2>Select results</h2>
|
||||
<p class="muted" id="resultsMeta">Poster cards appear here after a search.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="results-grid" id="results"></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const state = {
|
||||
config: { mode: "sub", quality: "best", download_dir: "" },
|
||||
config: { mode: "sub", quality: "best", download_dir: "", provider: "anikoto" },
|
||||
selected: null,
|
||||
episodes: [],
|
||||
searchRequestId: 0,
|
||||
@@ -510,6 +546,7 @@ INDEX_HTML = r"""<!doctype html>
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
""" + BROWSER_API_HELPER + r"""
|
||||
const selectionModal = $("selectionModal");
|
||||
|
||||
function setNotice(text) {
|
||||
const notice = $("notice");
|
||||
@@ -544,6 +581,14 @@ INDEX_HTML = r"""<!doctype html>
|
||||
return labels[String(provider || "").toLowerCase()] || "Provider";
|
||||
}
|
||||
|
||||
function openSelectionModal() {
|
||||
selectionModal.hidden = false;
|
||||
}
|
||||
|
||||
function closeSelectionModal() {
|
||||
selectionModal.hidden = true;
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
const el = $("results");
|
||||
el.innerHTML = "";
|
||||
@@ -630,6 +675,7 @@ INDEX_HTML = r"""<!doctype html>
|
||||
$("episodeChips").innerHTML = "";
|
||||
$("addBtn").disabled = true;
|
||||
$("trackBtn").disabled = false;
|
||||
openSelectionModal();
|
||||
try {
|
||||
const data = await api(`/api/anime/${encodeURIComponent(item.id)}/episodes?mode=${selectedMode}`);
|
||||
if (requestId !== state.episodeRequestId || state.selected !== item) return;
|
||||
@@ -663,6 +709,7 @@ INDEX_HTML = r"""<!doctype html>
|
||||
const requestId = ++state.searchRequestId;
|
||||
const query = $("search-query").value.trim();
|
||||
if (!query) return setNotice("Type a title first.");
|
||||
state.config.provider = $("searchProvider").value;
|
||||
setNotice("Searching...");
|
||||
$("resultsMeta").textContent = "Searching...";
|
||||
state.selected = null;
|
||||
@@ -671,7 +718,7 @@ INDEX_HTML = r"""<!doctype html>
|
||||
$("trackBtn").disabled = true;
|
||||
$("results").innerHTML = "";
|
||||
try {
|
||||
const data = await api(`/api/search?q=${encodeURIComponent(query)}&mode=${state.config.mode}`);
|
||||
const data = await api(`/api/search?q=${encodeURIComponent(query)}&mode=${state.config.mode}&provider=${encodeURIComponent(state.config.provider)}`);
|
||||
if (requestId !== state.searchRequestId) return;
|
||||
renderResults(data.results);
|
||||
setNotice(`${data.results.length} results`);
|
||||
@@ -717,7 +764,7 @@ INDEX_HTML = r"""<!doctype html>
|
||||
media_type: $("trackMediaType").value,
|
||||
season: $("seasonInput").value || "1",
|
||||
mode: state.config.mode,
|
||||
quality: $("search-quality").value,
|
||||
quality: state.config.quality,
|
||||
episodes: $("episodesInput").value,
|
||||
download_dir: $("jobFolder").value || state.config.download_dir
|
||||
};
|
||||
@@ -732,8 +779,9 @@ INDEX_HTML = r"""<!doctype html>
|
||||
async function loadConfig() {
|
||||
state.config = await api("/api/config");
|
||||
$("jobFolder").value = state.config.download_dir;
|
||||
$("search-quality").value = state.config.quality;
|
||||
$("searchProvider").value = state.config.provider || "anikoto";
|
||||
state.config.mode = state.config.mode === "dub" ? "dub" : "sub";
|
||||
state.config.quality = state.config.quality || "best";
|
||||
}
|
||||
|
||||
$("searchBtn").addEventListener("click", search);
|
||||
@@ -742,7 +790,13 @@ INDEX_HTML = r"""<!doctype html>
|
||||
});
|
||||
$("trackBtn").addEventListener("click", addSelectedToWatchlist);
|
||||
$("addBtn").addEventListener("click", addSelected);
|
||||
$("search-quality").addEventListener("change", () => { state.config.quality = $("search-quality").value; });
|
||||
$("closeSelectionBtn").addEventListener("click", closeSelectionModal);
|
||||
selectionModal.addEventListener("click", (event) => {
|
||||
if (event.target === selectionModal) closeSelectionModal();
|
||||
});
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && !selectionModal.hidden) closeSelectionModal();
|
||||
});
|
||||
$("firstEpBtn").addEventListener("click", () => { $("episodesInput").value = state.episodes[0] || ""; });
|
||||
$("latestEpBtn").addEventListener("click", () => {
|
||||
$("episodesInput").value = state.episodes[state.episodes.length - 1] || "";
|
||||
|
||||
Reference in New Issue
Block a user