mirror of
https://github.com/gotson/komga.git
synced 2026-05-02 11:54:31 +02:00
feat(webui): interactive readlist import
This commit is contained in:
parent
400f7baa53
commit
648ebb4b0d
11 changed files with 565 additions and 128 deletions
159
komga-webui/src/components/ReadListMatchRow.vue
Normal file
159
komga-webui/src/components/ReadListMatchRow.vue
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<template>
|
||||
<tr v-if="match">
|
||||
<slot/>
|
||||
<td>{{ match.request.series }}</td>
|
||||
<td>{{ match.request.number }}</td>
|
||||
|
||||
<!-- Series picker -->
|
||||
<td @click="modalSeriesPicker = true" style="cursor: pointer">
|
||||
<template v-if="selectedSeries">{{ selectedSeries.metadata.title }}</template>
|
||||
<template v-else>
|
||||
<div style="height: 2em" class="missing"></div>
|
||||
</template>
|
||||
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries"></series-picker-dialog>
|
||||
</td>
|
||||
|
||||
<!-- Book picker -->
|
||||
<td @click="selectedSeries ? modalBookPicker = true : undefined" :style="selectedSeries ? 'cursor: pointer': ''">
|
||||
<template v-if="selectedBook">
|
||||
{{ selectedBook.metadata.number }} - {{ selectedBook.metadata.title }}
|
||||
</template>
|
||||
<template v-else-if="selectedSeries">
|
||||
<div style="height: 2em" class="missing"></div>
|
||||
</template>
|
||||
<book-picker-dialog
|
||||
v-model="modalBookPicker"
|
||||
:books="seriesBooks"
|
||||
:book.sync="selectedBook"
|
||||
></book-picker-dialog>
|
||||
</td>
|
||||
|
||||
<!-- Error -->
|
||||
<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-else-if="duplicate">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon color="warning" v-on="on">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
{{ $t('readlist_import.row.duplicate_book') }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropType} from 'vue'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
import {BookDto} 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 FileNameChooserDialog from '@/components/dialogs/FileNameChooserDialog.vue'
|
||||
import {ReadListRequestBookMatchesDto} from '@/types/komga-readlists'
|
||||
import BookPickerDialog from '@/components/dialogs/BookPickerDialog.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ReadListMatchRow',
|
||||
components: {
|
||||
BookPickerDialog,
|
||||
SeriesPickerDialog,
|
||||
TransientBookDetailsDialog,
|
||||
TransientBookViewerDialog,
|
||||
FileNameChooserDialog,
|
||||
},
|
||||
props: {
|
||||
match: {
|
||||
type: Object as PropType<ReadListRequestBookMatchesDto>,
|
||||
required: true,
|
||||
},
|
||||
duplicate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bookId: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
match: {
|
||||
handler(val) {
|
||||
this.processMatch(val)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
selectedSeries: {
|
||||
handler(val, old) {
|
||||
if (!old) {
|
||||
this.getSeriesBooks(val)
|
||||
} else if (val?.id !== old?.id) {
|
||||
this.selectedBook = undefined
|
||||
this.getSeriesBooks(val)
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
selectedBook: {
|
||||
handler(val) {
|
||||
this.$emit('update:bookId', val?.id)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
innerSelect: false,
|
||||
selectedSeries: undefined as SeriesDto | undefined,
|
||||
selectedBook: undefined as BookDto | undefined,
|
||||
seriesBooks: [] as BookDto[],
|
||||
modalSeriesPicker: false,
|
||||
modalBookPicker: false,
|
||||
}),
|
||||
computed: {
|
||||
existingFileNames(): string[] {
|
||||
return this.seriesBooks.map(x => x.name)
|
||||
},
|
||||
error(): string {
|
||||
if (!this.selectedSeries) return this.$t('book_import.row.error_choose_series').toString()
|
||||
if (!this.selectedBook) return this.$t('readlist_import.row.error_choose_book').toString()
|
||||
return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async processMatch(match: ReadListRequestBookMatchesDto) {
|
||||
let seriesId: string | undefined
|
||||
if (match.matches.length === 1) {
|
||||
seriesId = match.matches[0].seriesId
|
||||
} else if (match.matches.length > 1) {
|
||||
seriesId = match.matches.find((m) => m.bookIds.length > 1)?.seriesId
|
||||
}
|
||||
if (seriesId) {
|
||||
this.selectedSeries = await this.$komgaSeries.getOneSeries(seriesId)
|
||||
const bookId = match.matches.find((m) => m.seriesId === seriesId)?.bookIds.find(Boolean)
|
||||
if (bookId) {
|
||||
this.selectedBook = await this.$komgaBooks.getBook(bookId)
|
||||
}
|
||||
}
|
||||
},
|
||||
async getSeriesBooks(series: SeriesDto) {
|
||||
if (series) {
|
||||
this.seriesBooks = (await this.$komgaSeries.getBooks(series.id, {unpaged: true})).content
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.missing {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -105,6 +105,8 @@ export default Vue.extend({
|
|||
onNotification(event: NotificationEvent) {
|
||||
this.queue.push({
|
||||
text: event.message,
|
||||
text2: event.text2,
|
||||
goTo: event.goTo,
|
||||
})
|
||||
},
|
||||
async onBookImported(event: BookImportSseDto) {
|
||||
|
|
|
|||
136
komga-webui/src/components/dialogs/BookPickerDialog.vue
Normal file
136
komga-webui/src/components/dialogs/BookPickerDialog.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="600"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('dialog.book_picker.title') }}</v-card-title>
|
||||
<v-btn icon absolute top right @click="dialogClose">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text style="height: 50%">
|
||||
<v-container fluid>
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="filter"
|
||||
autofocus
|
||||
:label="$t('dialog.book_picker.filter')"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-list v-if="filteredBooks.length > 0">
|
||||
<v-list-item-group color="primary" v-model="selectedItem">
|
||||
<v-list-item
|
||||
v-for="(book, index) in filteredBooks"
|
||||
:key="index"
|
||||
@click="choose(book)"
|
||||
>
|
||||
<v-img :src="bookThumbnailUrl(book.id)"
|
||||
height="50"
|
||||
max-width="35"
|
||||
class="my-1 mx-3"
|
||||
contain
|
||||
/>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ book.metadata.title }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ book.metadata.number }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-if="book.metadata.releaseDate">{{
|
||||
new Intl.DateTimeFormat($i18n.locale, {
|
||||
dateStyle: 'long',
|
||||
timeZone: 'UTC'
|
||||
}).format(new Date(book.metadata.releaseDate))
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropType} from 'vue'
|
||||
import {BookDto} from '@/types/komga-books'
|
||||
import {bookThumbnailUrl} from '@/functions/urls'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BookPickerDialog',
|
||||
data: () => {
|
||||
return {
|
||||
modal: false,
|
||||
selectedItem: -1,
|
||||
filter: '',
|
||||
bookThumbnailUrl,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
books: {
|
||||
type: Array as PropType<BookDto[]>,
|
||||
required: true,
|
||||
},
|
||||
book: {
|
||||
type: Object as PropType<BookDto>,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.modal = val
|
||||
if (val) {
|
||||
this.clear()
|
||||
}
|
||||
},
|
||||
modal(val) {
|
||||
!val && this.dialogClose()
|
||||
},
|
||||
books(val) {
|
||||
this.selectBook(val, this.book)
|
||||
},
|
||||
book(val) {
|
||||
this.selectBook(this.books, val)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredBooks(): BookDto[] {
|
||||
if (this.filter) {
|
||||
return this.books.filter((b) => b.metadata.number.toLowerCase().includes(this.filter.toLowerCase()) || b.metadata.title.toLowerCase().includes(this.filter.toLowerCase()) || b.metadata.releaseDate?.includes(this.filter))
|
||||
}
|
||||
return this.books
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.filter = ''
|
||||
},
|
||||
choose(book: BookDto) {
|
||||
this.$emit('update:book', book)
|
||||
this.dialogClose()
|
||||
},
|
||||
dialogClose() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
selectBook(books: BookDto[], book?: BookDto) {
|
||||
this.selectedItem = this.$_.findIndex(books, (b) => b.id === book?.id)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -189,6 +189,7 @@
|
|||
"common": {
|
||||
"age": "Age",
|
||||
"all_libraries": "All Libraries",
|
||||
"book": "Book",
|
||||
"books": "Books",
|
||||
"books_n": "No book | 1 book | {count} books",
|
||||
"books_total": "{count} / {total} books",
|
||||
|
|
@ -227,6 +228,7 @@
|
|||
"publisher": "Publisher",
|
||||
"read": "Read",
|
||||
"read_on": "Read on {date}",
|
||||
"readlist": "Read List",
|
||||
"readlists": "Read Lists",
|
||||
"required": "Required",
|
||||
"reset_filters": "Reset filters",
|
||||
|
|
@ -254,10 +256,15 @@
|
|||
"book_number": "Book number: {name}",
|
||||
"book_series": "Series: {name}",
|
||||
"button_import": "Import",
|
||||
"button_match": "Match",
|
||||
"comicrack_preambule_html": "You can import existing ComicRack Reading Lists in <code>.cbl</code> format.<br/>Komga will try to match the provided series and book number with series and books in your libraries.",
|
||||
"field_file_label": "ComicRack Reading List (.cbl)",
|
||||
"field_files_label": "ComicRack Reading Lists (.cbl)",
|
||||
"import_read_lists": "Import Read Lists",
|
||||
"imported_as": "Imported as {name}",
|
||||
"readlist_created": "Read List created: {name}",
|
||||
"requested_number": "Requested Number",
|
||||
"requested_series": "Requested Series",
|
||||
"results_preambule": "Result of the import is shown below. You can also check the unmatched books for each provided file.",
|
||||
"size_limit": "Size should be less than {size} MB",
|
||||
"tab_title": "Data Import"
|
||||
|
|
@ -296,6 +303,10 @@
|
|||
"button_confirm": "Analyze",
|
||||
"title": "Analyze library"
|
||||
},
|
||||
"book_picker": {
|
||||
"filter": "Filter by book number, title, or release date",
|
||||
"title": "Select Book"
|
||||
},
|
||||
"delete_book": {
|
||||
"button_confirm": "Delete",
|
||||
"confirm_delete": "Yes, delete book \"{name}\" and its files",
|
||||
|
|
@ -434,8 +445,8 @@
|
|||
"button_cancel": "Cancel",
|
||||
"button_confirm": "Save changes",
|
||||
"dialog_title": "Edit read list",
|
||||
"field_name": "Name",
|
||||
"field_manual_ordering": "Manual ordering",
|
||||
"field_name": "Name",
|
||||
"field_summary": "Summary",
|
||||
"label_ordering": "By default, books in a read list are ordered manually. You can disable manual ordering to sort books by release date.",
|
||||
"tab_general": "General",
|
||||
|
|
@ -525,6 +536,7 @@
|
|||
},
|
||||
"series_picker": {
|
||||
"label_search_series": "Search Series",
|
||||
"no_results": "No Series found",
|
||||
"title": "Select Series"
|
||||
},
|
||||
"server_stop": {
|
||||
|
|
@ -759,6 +771,12 @@
|
|||
"less": "Read less",
|
||||
"more": "Read more"
|
||||
},
|
||||
"readlist_import": {
|
||||
"row": {
|
||||
"duplicate_book": "Duplicate book",
|
||||
"error_choose_book": "Choose a book"
|
||||
}
|
||||
},
|
||||
"readlists_expansion_panel": {
|
||||
"manage_readlist": "Manage read list",
|
||||
"title": "{name} read list"
|
||||
|
|
|
|||
|
|
@ -226,10 +226,10 @@ const router = new Router({
|
|||
component: () => import(/* webpackChunkName: "import-books" */ './views/ImportBooks.vue'),
|
||||
},
|
||||
{
|
||||
path: '/import/readlists',
|
||||
name: 'import-readlists',
|
||||
path: '/import/readlist',
|
||||
name: 'import-readlist',
|
||||
beforeEnter: adminGuard,
|
||||
component: () => import(/* webpackChunkName: "import-readlists" */ './views/ImportReadLists.vue'),
|
||||
component: () => import(/* webpackChunkName: "import-readlist" */ './views/ImportReadList.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import {AxiosInstance} from 'axios'
|
||||
import {AuthorDto, BookDto} from '@/types/komga-books'
|
||||
import {
|
||||
ReadListCreationDto,
|
||||
ReadListDto,
|
||||
ReadListRequestMatchDto,
|
||||
ReadListThumbnailDto,
|
||||
ReadListUpdateDto,
|
||||
} from '@/types/komga-readlists'
|
||||
|
||||
const qs = require('qs')
|
||||
|
||||
|
|
@ -55,17 +62,17 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async postReadListImport(files: any): Promise<ReadListRequestResultDto[]> {
|
||||
async postReadListMatch(file: any): Promise<ReadListRequestMatchDto> {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
files.forEach((f: any) => formData.append('files', f))
|
||||
return (await this.http.post(`${API_READLISTS}/import`, formData, {
|
||||
formData.append('file', file)
|
||||
return (await this.http.post(`${API_READLISTS}/match/comicrack`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to import readlists\''
|
||||
let msg = 'An error occurred while trying to match readlist'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import {Route} from 'vue-router'
|
||||
|
||||
export const LIBRARY_ADDED = 'library-added'
|
||||
export const LIBRARY_CHANGED = 'library-changed'
|
||||
export const LIBRARY_DELETED = 'library-deleted'
|
||||
|
|
@ -47,4 +49,9 @@ export interface ErrorEvent {
|
|||
|
||||
export interface NotificationEvent {
|
||||
message: string,
|
||||
text2?: string,
|
||||
goTo?: {
|
||||
text: string,
|
||||
click: () => Promise<Route>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,3 +46,24 @@ export interface ReadListThumbnailDto {
|
|||
type: string,
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export interface ReadListRequestMatchDto {
|
||||
readListMatch: ReadListMatchDto,
|
||||
matches: ReadListRequestBookMatchesDto[],
|
||||
errorCode: string,
|
||||
}
|
||||
|
||||
export interface ReadListMatchDto {
|
||||
name: string,
|
||||
errorCode: string,
|
||||
}
|
||||
|
||||
export interface ReadListRequestBookMatchesDto {
|
||||
request: ReadListRequestBookDto,
|
||||
matches: ReadListRequestBookMatchDto[],
|
||||
}
|
||||
|
||||
export interface ReadListRequestBookMatchDto {
|
||||
seriesId: string,
|
||||
bookIds: string[],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div>
|
||||
<v-tabs grow>
|
||||
<v-tab :to="{name: 'import-books'}">{{ $t('common.books') }}</v-tab>
|
||||
<v-tab :to="{name: 'import-readlists'}">{{ $t('common.readlists') }}</v-tab>
|
||||
<v-tab :to="{name: 'import-readlist'}">{{ $t('common.readlist') }}</v-tab>
|
||||
</v-tabs>
|
||||
<router-view/>
|
||||
</div>
|
||||
|
|
|
|||
206
komga-webui/src/views/ImportReadList.vue
Normal file
206
komga-webui/src/views/ImportReadList.vue
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<v-alert type="info" text class="body-2" dismissible>
|
||||
<div v-html="$t('data_import.comicrack_preambule_html')"/>
|
||||
</v-alert>
|
||||
<v-form v-model="validMatch" ref="formMatch">
|
||||
<v-row align="center">
|
||||
<v-col cols="12" sm="">
|
||||
<v-file-input
|
||||
v-model="file"
|
||||
:label="$t('data_import.field_file_label')"
|
||||
prepend-icon="mdi-file-document-multiple"
|
||||
accept=".cbl"
|
||||
show-size
|
||||
:rules="importRules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!validMatch"
|
||||
@click="matchFile"
|
||||
>{{ $t('data_import.button_match') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
<template v-if="result">
|
||||
<v-divider/>
|
||||
|
||||
<v-simple-table>
|
||||
<thead class="font-weight-medium">
|
||||
<tr>
|
||||
<td>#</td>
|
||||
<td>{{ $t('data_import.requested_series') }}</td>
|
||||
<td>{{ $t('data_import.requested_number') }}</td>
|
||||
<td>{{ $t('common.series') }}</td>
|
||||
<td>{{ $t('common.book') }}</td>
|
||||
<td>
|
||||
<v-icon>mdi-alert-circle-outline</v-icon>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
v-for="(match, i) in result.matches"
|
||||
:key="i"
|
||||
>
|
||||
<read-list-match-row :match="match" :book-id.sync="form.bookIds[i]"
|
||||
:duplicate="isDuplicateBook(form.bookIds[i])">
|
||||
<td>{{ i + 1 }}</td>
|
||||
</read-list-match-row>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
|
||||
<form novalidate>
|
||||
<v-row align="center" justify="end">
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
:label="$t('dialog.edit_readlist.field_name')"
|
||||
v-model="form.name"
|
||||
clearable
|
||||
:error-messages="nameErrors"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-checkbox
|
||||
:label="$t('dialog.edit_readlist.field_manual_ordering')"
|
||||
v-model="form.ordered"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="5">
|
||||
<v-textarea v-model="form.summary"
|
||||
:label="$t('dialog.edit_readlist.field_summary')"
|
||||
rows="1"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn :color="creationFinished ? 'success': 'primary'"
|
||||
@click="create"
|
||||
:disabled="$v.$invalid"
|
||||
>
|
||||
<v-icon left v-if="creationFinished">mdi-check</v-icon>
|
||||
{{ $t('common.create') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</form>
|
||||
</template>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {convertErrorCodes} from '@/functions/error-codes'
|
||||
import {ReadListCreationDto, ReadListDto, ReadListRequestMatchDto} from '@/types/komga-readlists'
|
||||
import ReadListMatchRow from '@/components/ReadListMatchRow.vue'
|
||||
import {ERROR, NOTIFICATION, NotificationEvent} from '@/types/events'
|
||||
import {helpers, required} from 'vuelidate/lib/validators'
|
||||
|
||||
function validBookIds(this: any, value: string[]) {
|
||||
return value.filter(Boolean).length === this.result.matches.length && value.filter(Boolean).length === [...new Set(value)].length
|
||||
}
|
||||
|
||||
function validName(this: any, value: string) {
|
||||
return !helpers.req(value) || !this.readLists.some((e: ReadListDto) => e.name.toLowerCase() === value.toLowerCase())
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ImportReadLists',
|
||||
components: {ReadListMatchRow},
|
||||
data: () => ({
|
||||
convertErrorCodes,
|
||||
file: undefined,
|
||||
result: undefined as unknown as ReadListRequestMatchDto,
|
||||
validMatch: true,
|
||||
validCreate: true,
|
||||
form: {
|
||||
name: '',
|
||||
ordered: true,
|
||||
summary: '',
|
||||
bookIds: [] as string[],
|
||||
},
|
||||
readLists: [] as ReadListDto[],
|
||||
creationFinished: false,
|
||||
}),
|
||||
validations: {
|
||||
form: {
|
||||
name: {required, validName},
|
||||
ordered: {},
|
||||
summary: {},
|
||||
bookIds: {validBookIds},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
importRules(): any {
|
||||
return [
|
||||
(value: any) => {
|
||||
if (value == null) return false
|
||||
return value['size'] < 10_000_000 || this.$t('data_import.size_limit', {size: '10'}).toString()
|
||||
},
|
||||
]
|
||||
},
|
||||
nameErrors(): string[] {
|
||||
const errors = [] as string[]
|
||||
!this.$v?.form?.name?.required && errors.push(this.$t('common.required').toString())
|
||||
!this.$v?.form?.name?.validName && errors.push(this.$t('dialog.add_to_readlist.field_search_create_error').toString())
|
||||
return errors
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.readLists = (await this.$komgaReadLists.getReadLists(undefined, {unpaged: true} as PageRequest)).content
|
||||
},
|
||||
methods: {
|
||||
isDuplicateBook(bookId: string): boolean {
|
||||
return this.form.bookIds.filter((b) => b === bookId).length > 1
|
||||
},
|
||||
async matchFile() {
|
||||
this.result = await this.$komgaReadLists.postReadListMatch(this.file)
|
||||
this.form.name = this.result.readListMatch.name
|
||||
this.form.summary = ''
|
||||
this.form.ordered = true
|
||||
this.form.bookIds = []
|
||||
this.creationFinished = false
|
||||
},
|
||||
validateForm(): ReadListCreationDto | undefined {
|
||||
if (this.$v.$invalid) return undefined
|
||||
return {
|
||||
name: this.form.name,
|
||||
bookIds: this.form.bookIds,
|
||||
ordered: this.form.ordered,
|
||||
summary: this.form.summary,
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
if (!this.creationFinished) {
|
||||
const toCreate = this.validateForm()
|
||||
if (!toCreate) return
|
||||
|
||||
try {
|
||||
const created = await this.$komgaReadLists.postReadList(toCreate)
|
||||
this.$eventHub.$emit(NOTIFICATION, {
|
||||
message: this.$t('data_import.readlist_created', {name: created.name}).toString(),
|
||||
goTo: {
|
||||
text: this.$t('common.go_to_readlist'),
|
||||
click: () => this.$router.push({name: 'browse-readlist', params: {readListId: created.id}}),
|
||||
},
|
||||
} as NotificationEvent)
|
||||
this.creationFinished = true
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<v-alert type="info" text class="body-2">
|
||||
<div v-html="$t('data_import.comicrack_preambule_html')"/>
|
||||
</v-alert>
|
||||
<v-form v-model="valid" ref="form">
|
||||
<v-row align="center">
|
||||
<v-col cols="12" sm="">
|
||||
<v-file-input
|
||||
v-model="files"
|
||||
:label="$t('data_import.field_files_label')"
|
||||
multiple
|
||||
prepend-icon="mdi-file-document-multiple"
|
||||
accept=".cbl"
|
||||
show-size
|
||||
:rules="rules"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!valid"
|
||||
@click="importFiles"
|
||||
>{{ $t('data_import.button_import') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
||||
<template v-if="results.length > 0">
|
||||
<div class="mb-4 body-2">{{ $t('data_import.results_preambule') }}</div>
|
||||
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel
|
||||
v-for="result in results"
|
||||
:key="result.requestName"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="1">
|
||||
<v-icon v-if="result.readList === null" color="error">mdi-alert</v-icon>
|
||||
<v-icon v-if="result.readList && result.unmatchedBooks.length === 0" color="success">mdi-check</v-icon>
|
||||
<v-icon v-if="result.readList && result.unmatchedBooks.length > 0" color="warning">mdi-alert-circle
|
||||
</v-icon>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
{{ result.requestName }}
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<template v-if="result.readList">{{
|
||||
$t('data_import.imported_as', {name: result.readList.name})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>{{ convertErrorCodes(result.errorCode) }}</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content v-if="result.unmatchedBooks.length > 0">
|
||||
<v-list elevation="1" dense>
|
||||
<v-list-item
|
||||
v-for="(book, i) in result.unmatchedBooks"
|
||||
:key="i"
|
||||
three-line
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ convertErrorCodes(book.errorCode) }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('data_import.book_series', {name: book.book.series})
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>{{
|
||||
$t('data_import.book_number', {name: book.book.number})
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</template>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {convertErrorCodes} from '@/functions/error-codes'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ImportReadLists',
|
||||
data: () => ({
|
||||
convertErrorCodes,
|
||||
files: [],
|
||||
results: [] as ReadListRequestResultDto[],
|
||||
valid: true,
|
||||
}),
|
||||
computed: {
|
||||
rules(): any {
|
||||
return [
|
||||
(value: any) => !value || value.reduce((a: any, b: any) => a + (b['size'] || 0), 0) < 10_000_000 || this.$t('data_import.size_limit', {size: '10'}).toString(),
|
||||
]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async importFiles() {
|
||||
this.results.splice(0, this.results.length, ...(await this.$komgaReadLists.postReadListImport(this.files)));
|
||||
(this.$refs.form as any).reset()
|
||||
},
|
||||
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Loading…
Reference in a new issue