diff --git a/fe/src/App.vue b/fe/src/App.vue index 10a2ad840..927e7c30a 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,61 @@ 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). 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.08); + border-radius: 0 0 6px 6px; + background: #1f1f1f; /* solid so scrolled results don't show through */ + 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..84ac15af3 100644 --- a/fe/src/views/content/AddNewView.vue +++ b/fe/src/views/content/AddNewView.vue @@ -1175,8 +1175,34 @@ 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. + void runQueryParamSearch(route.query.q) }) +// 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 === 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