Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions fe/src/components/domain/audiobook/LibraryImportRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,13 @@
<button
class="btn-search-toggle"
title="Search for a match"
@click="showSearchModal = true"
@click="emit('search', item)"
>
<PhMagnifyingGlass :size="14" />
</button>
</div>
</td>
</tr>

<LibraryImportSearchModal
v-if="showSearchModal"
:item="item"
@close="showSearchModal = false"
@select="applyMatch"
/>
</template>

<script setup lang="ts">
Expand All @@ -111,13 +104,13 @@ import {
} from '@phosphor-icons/vue'
import { useLibraryImportStore } from '@/stores/libraryImport'
import type { LibraryImportItem } from '@/stores/libraryImport'
import type { SearchResult } from '@/types'
import LibraryImportSearchModal from './LibraryImportSearchModal.vue'

const props = defineProps<{ item: LibraryImportItem }>()
const emit = defineEmits<{
search: [item: LibraryImportItem]
}>()

const store = useLibraryImportStore()
const showSearchModal = ref(false)

const bookDisplayTitle = computed(() => props.item.detectedTitle?.trim() || props.item.folderName)
const bookMetaLine = computed(() =>
Expand All @@ -134,10 +127,6 @@ function isAuthorMismatch(item: LibraryImportItem): boolean {
return !!matched && !matched.includes(detected) && !detected.includes(matched)
}

function applyMatch(result: SearchResult) {
store.selectMatch(props.item.id, result)
}

function formatGroupedFileLabel(sourceFile: string): string {
const normalizedSource = sourceFile.replace(/\\/g, '/')
const normalizedFolder = props.item.folderPath.replace(/\\/g, '/').replace(/\/+$/, '')
Expand Down
96 changes: 20 additions & 76 deletions fe/src/components/domain/audiobook/LibraryImportSearchModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,12 @@
</div>

<div v-if="searchResults.length > 0" class="results-list">
<div
<SearchResultComponent
v-for="result in searchResults"
:key="result.asin ?? result.title"
class="result-item"
:result="result"
:placeholder-url="placeholderUrl"
@click="select(result)"
>
<img
v-if="result.imageUrl"
:src="getProtectedImageSrc(result.imageUrl, `library-import-search-${result.asin ?? result.title}`, placeholderUrl)"
class="result-thumb"
alt=""
/>
<div class="result-info">
<span class="result-title">{{ result.title }}</span>
<span class="result-meta">
{{ result.authors?.[0]?.name }}
<span v-if="result.series"> · {{ Array.isArray(result.series) ? (result.series as any)[0]?.name : result.series }}</span>
<span v-if="result.asin" class="result-asin"> · {{ result.asin }}</span>
</span>
</div>
</div>
/>
</div>

<div v-else-if="hasSearched && !isSearching" class="no-results">
Expand All @@ -61,6 +46,17 @@
<div v-else-if="!hasSearched && !isSearching" class="hint-text">
Type a title or paste an ASIN to search
</div>

<div v-if="lastSelected != null">
<span>Quick select:</span>
<div class="results-list">
<SearchResultComponent
:result="lastSelected"
:placeholder-url="placeholderUrl"
@click="select(lastSelected)"
/>
</div>
</div>
</div>
</ModalBody>
</Modal>
Expand All @@ -71,19 +67,21 @@ import { ref, onMounted, nextTick } from 'vue'
import { PhSpinner } from '@phosphor-icons/vue'
import { Modal, ModalHeader, ModalBody } from '@/components/feedback'
import { apiService } from '@/services/api'
import { useProtectedImages } from '@/composables/useProtectedImages'
import type { LibraryImportItem } from '@/stores/libraryImport'
import { buildLibraryImportInitialAuthor, buildLibraryImportInitialQuery } from '@/utils/libraryImportSearch'
import { getPlaceholderUrl } from '@/utils/placeholder'
import type { SearchResult } from '@/types'
import SearchResultComponent from './SearchResultComponent.vue'

const props = defineProps<{ item: LibraryImportItem }>()
const props = defineProps<{
item: LibraryImportItem,
lastSelected: SearchResult | null
}>()
const emit = defineEmits<{
close: []
select: [result: SearchResult]
}>()

const { getProtectedImageSrc } = useProtectedImages()
const inputEl = ref<HTMLInputElement | null>(null)
const placeholderUrl = getPlaceholderUrl()
// Build the initial query: ASIN → filename stem (when more specific than folder) → folderName
Expand Down Expand Up @@ -169,60 +167,6 @@ function select(result: SearchResult) {
border-radius: 6px;
}

.result-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
transition: background 0.15s;
border-bottom: 1px solid #2a2a2a;
}

.result-item:last-child {
border-bottom: none;
}

.result-item:hover {
background: #2a2a2a;
}

.result-thumb {
width: 36px;
height: 36px;
object-fit: cover;
border-radius: 3px;
flex-shrink: 0;
}

.result-info {
min-width: 0;
flex: 1;
}

.result-title {
display: block;
font-size: 0.875rem;
color: #e0e0e0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.result-meta {
display: block;
font-size: 0.75rem;
color: #888;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.result-asin {
font-family: monospace;
font-size: 0.7rem;
}

.no-results,
.hint-text {
padding: 0.75rem 0;
Expand Down
88 changes: 88 additions & 0 deletions fe/src/components/domain/audiobook/SearchResultComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<div :key="result.asin ?? result.title"
class="result-item"
>
<img
v-if="result.imageUrl"
:src="getProtectedImageSrc(result.imageUrl, `library-import-search-${result.asin ?? result.title}`, placeholderUrl)"
class="result-thumb"
alt=""
/>
<div class="result-info">
<span class="result-title">{{ result.title }}</span>
<span class="result-meta">
{{ result.authors?.[0]?.name }}
<span v-if="result.series"> · {{ Array.isArray(result.series) ? (result.series as any)[0]?.name : result.series }}</span>
<span v-if="result.asin" class="result-asin"> · {{ result.asin }}</span>
</span>
</div>
</div>
</template>

<script setup lang="ts">
import { useProtectedImages } from '@/composables/useProtectedImages';
import type { SearchResult } from '@/types';

const { getProtectedImageSrc } = useProtectedImages()

defineProps<{
result: SearchResult;
placeholderUrl?: string;
}>();
</script>

<style scoped>
.result-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
transition: background 0.15s;
border-bottom: 1px solid #2a2a2a;
}

.result-item:last-child {
border-bottom: none;
}

.result-item:hover {
background: #2a2a2a;
}

.result-thumb {
width: 36px;
height: 36px;
object-fit: cover;
border-radius: 3px;
flex-shrink: 0;
}

.result-info {
min-width: 0;
flex: 1;
}

.result-title {
display: block;
font-size: 0.875rem;
color: #e0e0e0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.result-meta {
display: block;
font-size: 0.75rem;
color: #888;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.result-asin {
font-family: monospace;
font-size: 0.7rem;
}
</style>
2 changes: 1 addition & 1 deletion fe/src/views/library/AudiobookDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@
</div>
</div>
<div v-if="audiobook.files && audiobook.files.length" class="file-list">
<div v-for="f in audiobook.files" :key="f.id" class="file-item"
<div v-for="f in audiobook.files.sort((a, b) => getFileName(a.path).localeCompare(getFileName(b.path)))" :key="f.id" class="file-item"
:class="{ expanded: isFileAccordionExpanded(f.id) }">
<div class="file-header" @click="toggleFileAccordion(f.id)">
<div class="file-info">
Expand Down
35 changes: 33 additions & 2 deletions fe/src/views/library/LibraryImportView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,12 @@
</thead>

<tbody>
<LibraryImportRow v-for="item in sortedItems" :key="item.id" :item="item" />
<LibraryImportRow
v-for="item in sortedItems"
:key="item.id"
:item="item"
@search="searchMatch"
/>
</tbody>
</table>
</div>
Expand All @@ -175,6 +180,14 @@
:folders="rootFoldersStore.folders"
/>
</div>

<LibraryImportSearchModal
v-if="showSearchModal"
:item="selectedItem"
:lastSelected="lastSelectedResult"
@close="showSearchModal = false"
@select="applyMatch"
/>
</template>

<script setup lang="ts">
Expand All @@ -188,9 +201,10 @@ import {
PhArrowUp,
PhArrowsDownUp,
} from '@phosphor-icons/vue'
import { useLibraryImportStore } from '@/stores/libraryImport'
import { useLibraryImportStore, type LibraryImportItem } from '@/stores/libraryImport'
import { useRootFoldersStore } from '@/stores/rootFolders'
import { useConfigurationStore } from '@/stores/configuration'
import LibraryImportSearchModal from '@/components/domain/audiobook/LibraryImportSearchModal.vue'
import LibraryImportRow from '@/components/domain/audiobook/LibraryImportRow.vue'
import LibraryImportFooter from '@/components/domain/audiobook/LibraryImportFooter.vue'
import {
Expand All @@ -202,6 +216,7 @@ import {
type LibraryImportSortDirection,
type LibraryImportSortKey,
} from '@/utils/libraryImportTable'
import type { SearchResult } from '@/types'

const COLUMN_WIDTH_STORAGE_KEY = 'listenarr.libraryImport.columnWidths.v1'
const MAX_COLUMN_WIDTH = 960
Expand Down Expand Up @@ -252,6 +267,10 @@ const tableMinWidth = computed(
columnWidths.value.match,
)

const showSearchModal = ref(false)
const selectedItem = ref<LibraryImportItem>()
const lastSelectedResult = ref<SearchResult | null>(null)

let resizeState:
| {
key: LibraryImportResizableColumnKey
Expand Down Expand Up @@ -404,6 +423,18 @@ function persistColumnWidths() {
// Non-fatal: resizing still works for the current session.
}
}

function searchMatch(item: LibraryImportItem) {
selectedItem.value = item
showSearchModal.value = true
}

function applyMatch(result: SearchResult) {
if (selectedItem.value != null) {
lastSelectedResult.value = result
store.selectMatch(selectedItem.value.id, result)
}
}
</script>

<style scoped>
Expand Down
Loading