komga/komga-webui/src/components/FileImportRow.vue
2025-01-17 17:59:22 +08:00

295 lines
9 KiB
Vue

<template>
<tr v-if="book">
<td>
<slot></slot>
</td>
<td>{{ book.name }}</td>
<!-- Status icon -->
<td>
<template v-if="bookAnalyzed">
<v-tooltip bottom :disabled="!status.message">
<template v-slot:activator="{ on }">
<v-icon :color="status.color" v-on="on">{{ status.icon }}</v-icon>
</template>
{{ convertErrorCodes(status.message) }}
</v-tooltip>
</template>
<v-progress-circular
v-else
indeterminate
color="primary"
:size="20"
:width="2"
/>
</td>
<!-- Series picker -->
<td @click="modalSeriesPicker = true" style="cursor: pointer">
<template v-if="selectedSeries">{{ selectedSeries.title }}</template>
<template v-else>
<div style="height: 2em" class="missing"></div>
</template>
<series-picker-dialog v-model="modalSeriesPicker" @update:series="pickedSeries" :include-oneshots="true"/>
</td>
<!-- Book number chooser -->
<td>
<v-text-field v-if="!selectedSeries?.oneshot"
v-model.number="bookNumber"
type="number"
step="0.1"
dense
:disabled="!selectedSeries"
/>
<span v-else>{{ $t('common.oneshot') }}</span>
</td>
<!-- Book details -->
<td class="px-1">
<v-btn icon elevation="1" :disabled="!bookAnalyzed" @click="modalBookDetails = true">
<v-icon v-if="bookToUpgrade">mdi-file-compare</v-icon>
<v-icon v-else>mdi-book-information-variant</v-icon>
</v-btn>
<transient-book-details-dialog
v-model="modalBookDetails"
:left-book="bookAnalyzed"
:right-book="bookToUpgrade"
:right-pages="bookToUpgradePages"
/>
</td>
<!-- Book viewer -->
<td class="px-1">
<v-btn icon
elevation="1"
@click="modalViewer = true"
:disabled="!bookAnalyzed || bookAnalyzed.status !== MediaStatus.READY"
>
<v-icon v-if="bookToUpgrade">mdi-compare</v-icon>
<v-icon v-else>mdi-image</v-icon>
</v-btn>
<transient-book-viewer-dialog
v-model="modalViewer"
:left-pages="leftPagesWithUrl"
:right-pages="rightPagesWithUrl"
/>
</td>
<!-- Destination name chooser -->
<td class="px-1">
<v-btn icon
elevation="1"
@click="modalNameChooser = true"
:disabled="!bookAnalyzed || bookAnalyzed.status !== MediaStatus.READY"
>
<v-icon>mdi-format-color-text</v-icon>
</v-btn>
<file-name-chooser-dialog
v-model="modalNameChooser"
:books="seriesBooks"
:name.sync="destinationName"
:existing="book.name"
/>
</td>
<td>
{{ destinationName }}
</td>
<td>
<template v-if="error">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon color="error" v-on="on">mdi-alert-circle</v-icon>
</template>
{{ error }}
</v-tooltip>
</template>
<template v-if="!error && bookToUpgrade">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon color="warning" v-on="on">mdi-comment-alert</v-icon>
</template>
{{ $t('book_import.row.warning_upgrade') }}
</v-tooltip>
</template>
</td>
</tr>
</template>
<script lang="ts">
import Vue, {PropType} from 'vue'
import {MediaStatus} from '@/types/enum-books'
import {SeriesDto} from '@/types/komga-series'
import {TransientBookDto} from '@/types/komga-transientbooks'
import {BookDto, BookImportDto, PageDto, PageDtoWithUrl} from '@/types/komga-books'
import SeriesPickerDialog from '@/components/dialogs/SeriesPickerDialog.vue'
import TransientBookDetailsDialog from '@/components/dialogs/TransientBookDetailsDialog.vue'
import TransientBookViewerDialog from '@/components/dialogs/TransientBookViewerDialog.vue'
import {bookPageUrl, transientBookPageUrl} from '@/functions/urls'
import {convertErrorCodes} from '@/functions/error-codes'
import FileNameChooserDialog from '@/components/dialogs/FileNameChooserDialog.vue'
import {SeriesSelected} from '@/types/series-slim'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'FileImportRow',
components: {SeriesPickerDialog, TransientBookDetailsDialog, TransientBookViewerDialog, FileNameChooserDialog},
props: {
book: {
type: Object as PropType<TransientBookDto>,
required: true,
},
series: {
type: Object as PropType<SeriesDto>,
required: false,
},
payload: {
type: Object,
required: false,
},
},
watch: {
book: {
handler(val) {
this.analyze(val)
this.destinationName = val.name
},
immediate: true,
},
selectedSeries: {
handler(val) {
this.getSeriesBooks(val)
},
immediate: true,
},
series: {
handler(val) {
if (val) this.selectedSeries = {
seriesId: val.id,
title: val.metadata.title,
oneshot: val.oneshot,
}
},
immediate: true,
},
bookNumber: {
handler(val) {
this.checkForUpgrade(val)
},
immediate: true,
},
importPayload: {
handler(val) {
this.$emit('update:payload', val)
},
immediate: true,
},
},
data: () => ({
MediaStatus,
convertErrorCodes,
innerSelect: false,
bookAnalyzed: undefined as unknown as TransientBookDto,
selectedSeries: undefined as SeriesSelected | undefined,
seriesBooks: [] as BookDto[],
bookToUpgrade: undefined as BookDto | undefined,
bookToUpgradePages: [] as PageDto[],
modalSeriesPicker: false,
modalBookDetails: false,
modalViewer: false,
modalNameChooser: false,
bookNumber: undefined as number | undefined,
destinationName: '',
}),
computed: {
leftPagesWithUrl(): PageDtoWithUrl[] {
return this.bookAnalyzed ? this.bookAnalyzed.pages.map(p => ({
...p,
url: transientBookPageUrl(this.bookAnalyzed.id, p.number),
})) : []
},
rightPagesWithUrl(): PageDtoWithUrl[] {
return this.bookToUpgrade ? this.bookToUpgradePages.map(p => ({
...p,
url: bookPageUrl(this.bookToUpgrade!!.id, p.number),
})) : []
},
status(): object {
if (!this.bookAnalyzed) return {}
switch (this.bookAnalyzed.status) {
case MediaStatus.READY:
return {icon: 'mdi-check-circle', color: 'success', message: ''}
default:
return {icon: 'mdi-alert-circle', color: 'error', message: this.bookAnalyzed.comment}
}
},
existingFileNames(): string[] {
return this.seriesBooks.map(x => x.name)
},
error(): string {
if (!this.bookAnalyzed) return this.$t('book_import.row.error_analyze_first').toString()
if (this.bookAnalyzed.status != MediaStatus.READY) return this.$t('book_import.row.error_only_import_no_errors').toString()
if (!this.selectedSeries) return this.$t('book_import.row.error_choose_series').toString()
return ''
},
importPayload(): BookImportDto | undefined {
if (this.error || !this.selectedSeries) return undefined
return {
seriesId: this.selectedSeries?.seriesId,
sourceFile: this.book.url,
upgradeBookId: this.bookToUpgrade?.id,
destinationName: this.destinationName,
}
},
},
methods: {
async analyze(book: TransientBookDto) {
this.bookAnalyzed = await this.$komgaTransientBooks.analyze(book.id)
this.getSeries(this.bookAnalyzed.seriesId)
this.bookNumber = this.bookAnalyzed.number
},
async getSeries(seriesId?: string) {
if (seriesId) {
const seriesDto = await this.$komgaSeries.getOneSeries(seriesId)
this.selectedSeries = {
seriesId: seriesDto.id,
title: seriesDto.metadata.title,
oneshot: seriesDto.oneshot,
}
}
},
async getSeriesBooks(series: SeriesSelected) {
if (series) {
this.seriesBooks = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(series.seriesId)),
} as BookSearch, {unpaged: true, sort: 'metadata.numberSort'})).content
if (series.oneshot) {
this.bookNumber = this.seriesBooks[0].metadata.numberSort
}
this.checkForUpgrade(this.bookNumber)
}
},
async checkForUpgrade(number: number | undefined) {
this.bookToUpgrade = this.seriesBooks.find(b => b.metadata.numberSort === number)
if (this.bookToUpgrade) this.bookToUpgradePages = await this.$komgaBooks.getBookPages(this.bookToUpgrade.id)
else this.bookToUpgradePages = []
},
pickedSeries(series: SeriesDto) {
this.selectedSeries = {
seriesId: series.id,
title: series.metadata.title,
oneshot: series.oneshot,
}
},
},
})
</script>
<style scoped>
.missing {
border: 2px dashed red;
}
</style>