@@ -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