From c9c8de6fe3aacb773878ed9b32705195464de1db Mon Sep 17 00:00:00 2001 From: dny238 Date: Wed, 16 Sep 2026 08:19:50 -0600 Subject: [PATCH 1/2] feat(search): offer "Search for ''" jump from library search into Add New MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header search bar only ever searches the library you already have, which is confusing when the book you want isn't added yet — it just shows "No matches". Mirror Sonarr: whenever a term is present, the dropdown now also offers a "Search for ''" affordance that jumps into the Add New metadata search with the term pre-filled and auto-run. - App.vue: add the affordance row in the header search dropdown (shown alongside local matches and in place of the bare "No matches" state), a searchExternally() handler that pushes to the `add-new` route with ?q=, and Enter now falls through to it when there is no local match. - AddNewView.vue: read ?q= on mount and auto-run the search; also watch the query so using the affordance again while already on the page re-runs it. No backend changes; performSearch() already auto-detects the search type. Co-Authored-By: Claude Opus 4.8 --- fe/src/App.vue | 92 +++++++++++++++++++++++++++-- fe/src/views/content/AddNewView.vue | 20 +++++++ 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/fe/src/App.vue b/fe/src/App.vue index 10a2ad840..bc8c4bcca 100644 --- a/fe/src/App.vue +++ b/fe/src/App.vue @@ -128,11 +128,30 @@ -
- -
Searching...
-
No matches
+
+ +
Searching...
+ +
+
No matches in your library
+
+ + +
@@ -1284,8 +1303,22 @@ const selectSuggestion = (s: { id: number; title: string; author?: string }) => } } +// Jump from the "what I already have" library search into the Add New metadata +// search, pre-filling the current term (Sonarr-style affordance). This is the +// escape hatch when the book you want isn't in your library yet. +const searchExternally = () => { + const q = searchQuery.value.trim() + if (!q) return + searchQuery.value = '' + suggestions.value = [] + closeSearch() + void router.push({ name: 'add-new', query: { q } }) +} + const applyFirstResult = () => { if (suggestions.value.length > 0) selectSuggestion(suggestions.value[0]!) + // No local match for a non-empty query: Enter takes you straight to Add New. + else if (searchQuery.value.trim().length > 0) searchExternally() } watch( @@ -2664,6 +2697,57 @@ these are not present, the Google Fonts import in `fe/index.html` will be used a font-size: 0.9rem; } +/* "Search for ''" jump into Add New (Sonarr-style). */ +.search-addnew { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px; + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 0 0 6px 6px; + background: transparent; + color: #e6eef6; + cursor: pointer; + text-align: left; + font: inherit; +} + +.search-addnew:hover, +.search-addnew:focus-visible { + background: rgba(33, 150, 243, 0.12); + outline: none; +} + +.search-addnew-icon { + width: 18px; + height: 18px; + flex-shrink: 0; + color: #2196f3; +} + +.search-addnew-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.search-addnew-title { + font-weight: 500; + color: #fff; + font-size: 0.95rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.search-addnew-sub { + font-size: 0.82rem; + color: #bfc8cf; +} + /* Mobile search overlay */ @media (max-width: 768px) { .nav-search-inline { diff --git a/fe/src/views/content/AddNewView.vue b/fe/src/views/content/AddNewView.vue index 3a1d2f87c..1ce43bcbf 100644 --- a/fe/src/views/content/AddNewView.vue +++ b/fe/src/views/content/AddNewView.vue @@ -1175,8 +1175,28 @@ onMounted(() => { const np = Number(p) if (!isNaN(np) && np > 0) audiblePage.value = np } + + // A pre-filled term arrives via ?q= from the global header's "Search for + // ''" affordance (see App.vue). Auto-run it so the user lands on + // results without retyping. performSearch() auto-detects the search type. + const initialQuery = route.query.q + if (initialQuery) { + searchQuery.value = String(initialQuery) + void performSearch() + } }) +// If the header affordance is used while already on /add-new, the route query +// changes without remounting, so onMounted won't fire again — re-run here. +watch( + () => route.query.q, + (q, previous) => { + if (!q || q === previous) return + searchQuery.value = String(q) + void performSearch() + }, +) + // Library checking functions - now handled by useLibraryCheck composable // Unified Search - now handled by useSearch composable From fe7f1ed57d3421b8bce603468516eecbfc67c691 Mon Sep 17 00:00:00 2001 From: dny238 Date: Thu, 17 Sep 2026 14:27:56 -0600 Subject: [PATCH 2/2] fix(search): scroll to results and keep the "Search for" affordance in view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-up fixes to the library-search Add New affordance: - When the affordance runs a search while already on /add-new, the results updated further down the page with no viewport change, so it read as a no-op. The query-driven search now scrolls the results into view after running. - The affordance sat after the results inside the max-height:300px scrollable dropdown, so a full list of local matches pushed it out of view entirely — which hid it on the narrow mobile overlay. It's now a sticky footer pinned to the bottom of the dropdown with a solid background, always reachable. Co-Authored-By: Claude Opus 4.8 --- fe/src/App.vue | 10 +++++++--- fe/src/views/content/AddNewView.vue | 22 ++++++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/fe/src/App.vue b/fe/src/App.vue index bc8c4bcca..927e7c30a 100644 --- a/fe/src/App.vue +++ b/fe/src/App.vue @@ -2697,17 +2697,21 @@ these are not present, the Google Fonts import in `fe/index.html` will be used a font-size: 0.9rem; } -/* "Search for ''" jump into Add New (Sonarr-style). */ +/* "Search for ''" jump into Add New (Sonarr-style). Pinned to the bottom of + the scrollable results so a long list of local matches can't push it out of view + (which hid it entirely on the narrow mobile dropdown). */ .search-addnew { + position: sticky; + bottom: -6px; /* cancel the dropdown's 6px bottom padding so it hugs the edge */ display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px; border: none; - border-top: 1px solid rgba(255, 255, 255, 0.06); + border-top: 1px solid rgba(255, 255, 255, 0.08); border-radius: 0 0 6px 6px; - background: transparent; + background: #1f1f1f; /* solid so scrolled results don't show through */ color: #e6eef6; cursor: pointer; text-align: left; diff --git a/fe/src/views/content/AddNewView.vue b/fe/src/views/content/AddNewView.vue index 1ce43bcbf..84ac15af3 100644 --- a/fe/src/views/content/AddNewView.vue +++ b/fe/src/views/content/AddNewView.vue @@ -1179,11 +1179,7 @@ onMounted(() => { // A pre-filled term arrives via ?q= from the global header's "Search for // ''" affordance (see App.vue). Auto-run it so the user lands on // results without retyping. performSearch() auto-detects the search type. - const initialQuery = route.query.q - if (initialQuery) { - searchQuery.value = String(initialQuery) - void performSearch() - } + void runQueryParamSearch(route.query.q) }) // If the header affordance is used while already on /add-new, the route query @@ -1191,12 +1187,22 @@ onMounted(() => { watch( () => route.query.q, (q, previous) => { - if (!q || q === previous) return - searchQuery.value = String(q) - void performSearch() + if (q === previous) return + void runQueryParamSearch(q) }, ) +// Run a search driven by the ?q= route param and bring the results into view. +// Without the scroll, running the search while already on this page updates the +// results lower down with no visible change, so the affordance reads as a no-op. +const runQueryParamSearch = async (raw: unknown) => { + const term = raw == null ? '' : String(raw).trim() + if (!term) return + searchQuery.value = term + await performSearch() + await scrollUnifiedSearchInputIntoView() +} + // Library checking functions - now handled by useLibraryCheck composable // Unified Search - now handled by useSearch composable