Move search controls and add result modal
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.51.4 - 2026-08-09
|
||||||
|
|
||||||
|
- Moved Search controls from the sidebar to a top-of-page search panel.
|
||||||
|
- Removed the Search page quality selector; queued downloads now use the saved Config quality default.
|
||||||
|
- Added a Search page provider dropdown for choosing Anikoto, AniNeko, or AnimePahe per search.
|
||||||
|
- Moved result actions into a floating window that opens after selecting a search result.
|
||||||
|
|
||||||
## 0.51.3 - 2026-08-09
|
## 0.51.3 - 2026-08-09
|
||||||
|
|
||||||
- Removed the Episode mode selector from the Search page and now choose the selected result's episode language automatically from the saved Config default and available `SUB`/`DUB` flags.
|
- Removed the Episode mode selector from the Search page and now choose the selected result's episode language automatically from the saved Config default and available `SUB`/`DUB` flags.
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ Kaizoku is a local web app for searching, tracking, and downloading anime from A
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Search anime through the configured provider and tag results with available `SUB` and `DUB` episode languages.
|
- Search anime through the configured provider and tag results with available `SUB` and `DUB` episode languages.
|
||||||
|
- Choose the active search provider directly from the Search page.
|
||||||
- Prefer provider-supplied search artwork, with a local title-based thumbnail fallback when provider artwork is missing or broken.
|
- Prefer provider-supplied search artwork, with a local title-based thumbnail fallback when provider artwork is missing or broken.
|
||||||
- Queue single episodes or batches in subbed or dubbed mode.
|
- Queue single episodes or batches in subbed or dubbed mode.
|
||||||
|
- Open result actions in a floating window for watchlist, media type, library name, season, episode range, and download folder choices.
|
||||||
- Fall back across the other configured providers when an episode or stream cannot be resolved on the primary provider.
|
- Fall back across the other configured providers when an episode or stream cannot be resolved on the primary provider.
|
||||||
- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`.
|
- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`.
|
||||||
- Save English external subtitles when the provider exposes usable subtitle tracks.
|
- Save English external subtitles when the provider exposes usable subtitle tracks.
|
||||||
|
|||||||
@@ -916,9 +916,12 @@ def episode_language_summary(show_id, provider):
|
|||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def search_anime(query, mode):
|
def search_anime(query, mode, provider=None):
|
||||||
runtime = ensure_runtime()
|
runtime = ensure_runtime()
|
||||||
provider = str((runtime.get("config") or {}).get("provider") or "anikoto").strip().lower()
|
configured_provider = str((runtime.get("config") or {}).get("provider") or "anikoto").strip().lower()
|
||||||
|
provider = str(provider or configured_provider).strip().lower()
|
||||||
|
if provider not in provider_bridge.PROVIDERS:
|
||||||
|
provider = configured_provider if configured_provider in provider_bridge.PROVIDERS else "anikoto"
|
||||||
data = provider_bridge.search(query, provider=provider, page=1)
|
data = provider_bridge.search(query, provider=provider, page=1)
|
||||||
shows = data.get("results") or []
|
shows = data.get("results") or []
|
||||||
results = []
|
results = []
|
||||||
|
|||||||
+2
-2
@@ -454,7 +454,7 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
<h2>Saved defaults</h2>
|
<h2>Saved defaults</h2>
|
||||||
<div class="stack muted">
|
<div class="stack muted">
|
||||||
<p>These values preload the Search page and act as the fallback folder for new queue jobs.</p>
|
<p>These values preload the Search page and act as the fallback folder for new queue jobs.</p>
|
||||||
<p>Mode and quality can still be changed on the Search page before queueing a download.</p>
|
<p>Search uses these saved mode and quality defaults when opening result actions and queueing downloads.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="settings">
|
<section class="settings">
|
||||||
@@ -476,7 +476,7 @@ CONFIG_HTML = r"""<!doctype html>
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div>
|
<div>
|
||||||
<h2>Defaults</h2>
|
<h2>Defaults</h2>
|
||||||
<p class="muted">Manage the folder, mode, and quality that the Search page starts with.</p>
|
<p class="muted">Manage the folder, mode, quality, and provider defaults used by Search and new queue jobs.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="primary" id="saveConfigBtn" type="button">Save</button>
|
<button class="primary" id="saveConfigBtn" type="button">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-1
@@ -640,11 +640,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
params = parse_qs(parsed.query)
|
params = parse_qs(parsed.query)
|
||||||
query = (params.get("q") or [""])[0].strip()
|
query = (params.get("q") or [""])[0].strip()
|
||||||
mode = (params.get("mode") or [config["mode"]])[0].lower()
|
mode = (params.get("mode") or [config["mode"]])[0].lower()
|
||||||
|
provider = (params.get("provider") or [config.get("provider") or ""])[0].strip().lower()
|
||||||
if not query:
|
if not query:
|
||||||
raise ValueError("Search query is required")
|
raise ValueError("Search query is required")
|
||||||
if mode not in MODE_CHOICES:
|
if mode not in MODE_CHOICES:
|
||||||
raise ValueError("Mode must be sub or dub")
|
raise ValueError("Mode must be sub or dub")
|
||||||
self.json({"results": Handler._context(self).search_anime(query, mode)})
|
self.json({"results": Handler._context(self).search_anime(query, mode, provider=provider)})
|
||||||
elif parsed.path == "/api/search/thumb":
|
elif parsed.path == "/api/search/thumb":
|
||||||
Handler._runtime(self)
|
Handler._runtime(self)
|
||||||
params = parse_qs(parsed.query)
|
params = parse_qs(parsed.query)
|
||||||
|
|||||||
+113
-59
@@ -120,7 +120,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
.app {
|
.app {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(320px, 380px) 1fr;
|
grid-template-columns: minmax(240px, 300px) 1fr;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
aside, main {
|
aside, main {
|
||||||
@@ -173,16 +173,6 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
.controls {
|
.controls {
|
||||||
align-items: end;
|
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 {
|
.page-links {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -233,18 +223,26 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.settings, .episodes-panel, .results-panel, .deps {
|
.settings, .search-panel, .episodes-panel, .results-panel, .deps {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background: linear-gradient(180deg, rgba(29, 22, 47, 0.88), rgba(17, 13, 29, 0.94));
|
background: linear-gradient(180deg, rgba(29, 22, 47, 0.88), rgba(17, 13, 29, 0.94));
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
.settings, .episodes-panel, .results-panel, .deps {
|
.settings, .search-panel, .episodes-panel, .results-panel, .deps {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
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 {
|
.results-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -385,6 +383,31 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
color: #ddd0ff;
|
color: #ddd0ff;
|
||||||
font-size: 13px;
|
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 {
|
.results-panel .toolbar {
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
@@ -403,6 +426,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
.app { grid-template-columns: 1fr; }
|
.app { grid-template-columns: 1fr; }
|
||||||
aside { border-right: 0; border-bottom: 1px solid var(--line); }
|
aside { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||||
.controls, .row, .toolbar { align-items: stretch; flex-direction: column; }
|
.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)); }
|
.results-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -418,50 +442,71 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
"Search",
|
"Search",
|
||||||
badge_id="serverBadge",
|
badge_id="serverBadge",
|
||||||
) + render_page_links("search") + r"""
|
) + 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>
|
</aside>
|
||||||
|
|
||||||
<main>
|
<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 class="toolbar">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="selectedTitle">Select a result</h2>
|
<h2 id="selectedTitle">Select a result</h2>
|
||||||
<p class="muted" id="selectedMeta">Episodes will appear here.</p>
|
<p class="muted" id="selectedMeta">Episodes will appear here.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="flex:0 0 auto">
|
<div class="row" style="flex:0 0 auto">
|
||||||
<label style="min-width: 180px;">Watchlist category
|
<button class="ghost" id="closeSelectionBtn" type="button">Close</button>
|
||||||
<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="trackBtn" type="button" disabled>Add to watchlist</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>
|
<button class="primary" id="addBtn" type="button" disabled>Add to queue</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="row">
|
||||||
<label>Library name
|
<label>Library name
|
||||||
<input id="animeNameInput" placeholder="Anime name">
|
<input id="animeNameInput" placeholder="Anime name">
|
||||||
@@ -487,21 +532,12 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="results-panel">
|
</div>
|
||||||
<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>
|
<script>
|
||||||
const state = {
|
const state = {
|
||||||
config: { mode: "sub", quality: "best", download_dir: "" },
|
config: { mode: "sub", quality: "best", download_dir: "", provider: "anikoto" },
|
||||||
selected: null,
|
selected: null,
|
||||||
episodes: [],
|
episodes: [],
|
||||||
searchRequestId: 0,
|
searchRequestId: 0,
|
||||||
@@ -510,6 +546,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
|
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
""" + BROWSER_API_HELPER + r"""
|
""" + BROWSER_API_HELPER + r"""
|
||||||
|
const selectionModal = $("selectionModal");
|
||||||
|
|
||||||
function setNotice(text) {
|
function setNotice(text) {
|
||||||
const notice = $("notice");
|
const notice = $("notice");
|
||||||
@@ -544,6 +581,14 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
return labels[String(provider || "").toLowerCase()] || "Provider";
|
return labels[String(provider || "").toLowerCase()] || "Provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSelectionModal() {
|
||||||
|
selectionModal.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSelectionModal() {
|
||||||
|
selectionModal.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderResults(results) {
|
function renderResults(results) {
|
||||||
const el = $("results");
|
const el = $("results");
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
@@ -630,6 +675,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
$("episodeChips").innerHTML = "";
|
$("episodeChips").innerHTML = "";
|
||||||
$("addBtn").disabled = true;
|
$("addBtn").disabled = true;
|
||||||
$("trackBtn").disabled = false;
|
$("trackBtn").disabled = false;
|
||||||
|
openSelectionModal();
|
||||||
try {
|
try {
|
||||||
const data = await api(`/api/anime/${encodeURIComponent(item.id)}/episodes?mode=${selectedMode}`);
|
const data = await api(`/api/anime/${encodeURIComponent(item.id)}/episodes?mode=${selectedMode}`);
|
||||||
if (requestId !== state.episodeRequestId || state.selected !== item) return;
|
if (requestId !== state.episodeRequestId || state.selected !== item) return;
|
||||||
@@ -663,6 +709,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
const requestId = ++state.searchRequestId;
|
const requestId = ++state.searchRequestId;
|
||||||
const query = $("search-query").value.trim();
|
const query = $("search-query").value.trim();
|
||||||
if (!query) return setNotice("Type a title first.");
|
if (!query) return setNotice("Type a title first.");
|
||||||
|
state.config.provider = $("searchProvider").value;
|
||||||
setNotice("Searching...");
|
setNotice("Searching...");
|
||||||
$("resultsMeta").textContent = "Searching...";
|
$("resultsMeta").textContent = "Searching...";
|
||||||
state.selected = null;
|
state.selected = null;
|
||||||
@@ -671,7 +718,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
$("trackBtn").disabled = true;
|
$("trackBtn").disabled = true;
|
||||||
$("results").innerHTML = "";
|
$("results").innerHTML = "";
|
||||||
try {
|
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;
|
if (requestId !== state.searchRequestId) return;
|
||||||
renderResults(data.results);
|
renderResults(data.results);
|
||||||
setNotice(`${data.results.length} results`);
|
setNotice(`${data.results.length} results`);
|
||||||
@@ -717,7 +764,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
media_type: $("trackMediaType").value,
|
media_type: $("trackMediaType").value,
|
||||||
season: $("seasonInput").value || "1",
|
season: $("seasonInput").value || "1",
|
||||||
mode: state.config.mode,
|
mode: state.config.mode,
|
||||||
quality: $("search-quality").value,
|
quality: state.config.quality,
|
||||||
episodes: $("episodesInput").value,
|
episodes: $("episodesInput").value,
|
||||||
download_dir: $("jobFolder").value || state.config.download_dir
|
download_dir: $("jobFolder").value || state.config.download_dir
|
||||||
};
|
};
|
||||||
@@ -732,8 +779,9 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
state.config = await api("/api/config");
|
state.config = await api("/api/config");
|
||||||
$("jobFolder").value = state.config.download_dir;
|
$("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.mode = state.config.mode === "dub" ? "dub" : "sub";
|
||||||
|
state.config.quality = state.config.quality || "best";
|
||||||
}
|
}
|
||||||
|
|
||||||
$("searchBtn").addEventListener("click", search);
|
$("searchBtn").addEventListener("click", search);
|
||||||
@@ -742,7 +790,13 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
});
|
});
|
||||||
$("trackBtn").addEventListener("click", addSelectedToWatchlist);
|
$("trackBtn").addEventListener("click", addSelectedToWatchlist);
|
||||||
$("addBtn").addEventListener("click", addSelected);
|
$("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] || ""; });
|
$("firstEpBtn").addEventListener("click", () => { $("episodesInput").value = state.episodes[0] || ""; });
|
||||||
$("latestEpBtn").addEventListener("click", () => {
|
$("latestEpBtn").addEventListener("click", () => {
|
||||||
$("episodesInput").value = state.episodes[state.episodes.length - 1] || "";
|
$("episodesInput").value = state.episodes[state.episodes.length - 1] || "";
|
||||||
|
|||||||
Reference in New Issue
Block a user