mirror of
https://github.com/gotson/komga.git
synced 2025-12-21 16:03:03 +01:00
feat(webui): import ComicRack lists as read lists
This commit is contained in:
parent
c1e435762c
commit
8b0dac3125
17 changed files with 247 additions and 16 deletions
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "الصفوف لكل صفحة:",
|
||||
"sortBy": "مفروزة حسب"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} ملفات",
|
||||
"counterSize": "{0} ملفات ({1} في المجموع)"
|
||||
},
|
||||
"noDataText": "لا توجد بيانات متاحة"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Zeilen pro Seite:",
|
||||
"sortBy": "Sortiere nach"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} Dateien",
|
||||
"counterSize": "{0} Dateien ({1} gesamt)"
|
||||
},
|
||||
"noDataText": "Keine Daten vorhanden"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Rows per page:",
|
||||
"sortBy": "Sort by"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} files",
|
||||
"counterSize": "{0} files ({1} in total)"
|
||||
},
|
||||
"noDataText": "No data available"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
@ -153,6 +157,18 @@
|
|||
"recently_added_series": "Recently Added Series",
|
||||
"recently_updated_series": "Recently Updated Series"
|
||||
},
|
||||
"data_import": {
|
||||
"book_number": "Book number: {name}",
|
||||
"book_series": "Series: {name}",
|
||||
"button_import": "Import",
|
||||
"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_files_label": "ComicRack Reading Lists (.cbl)",
|
||||
"import_read_lists": "Import Read Lists",
|
||||
"imported_as": "Imported as {name}",
|
||||
"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"
|
||||
},
|
||||
"dialog": {
|
||||
"add_to_collection": {
|
||||
"button_create": "Create",
|
||||
|
|
@ -363,7 +379,14 @@
|
|||
"ERR_1005": "Unknown error while analyzing book",
|
||||
"ERR_1006": "Book does not contain any page",
|
||||
"ERR_1007": "Some entries could not be analyzed",
|
||||
"ERR_1008": "Unknown error while getting book's entries"
|
||||
"ERR_1008": "Unknown error while getting book's entries",
|
||||
"ERR_1009": "A read list with that name already exists",
|
||||
"ERR_1010": "No books were matched within the read list request",
|
||||
"ERR_1011": "No unique match for series",
|
||||
"ERR_1012": "No match for series",
|
||||
"ERR_1013": "No unique match for book number within series",
|
||||
"ERR_1014": "No match for book number within series",
|
||||
"ERR_1015": "Error while deserializing ComicRack ReadingList"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "age rating",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Filas por página:",
|
||||
"sortBy": "Ordenado por"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} archivos",
|
||||
"counterSize": "{0} archivos ({1} en total)"
|
||||
},
|
||||
"noDataText": "No hay datos disponibles"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Élements par page :",
|
||||
"sortBy": "Trier par"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} fichiers",
|
||||
"counterSize": "{0} fichiers ({1} au total)"
|
||||
},
|
||||
"noDataText": "Aucune donnée disponible"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@
|
|||
"dataTable": {
|
||||
"itemsPerPageText": "Righe per pagina:",
|
||||
"sortBy": "Ordina per"
|
||||
}
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} files",
|
||||
"counterSize": "{0} files ({1} in totale)"
|
||||
},
|
||||
"noDataText": "Nessun elemento disponibile"
|
||||
},
|
||||
"account_settings": {
|
||||
"account_settings": "Impostazioni Account",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "ページ毎の行数:",
|
||||
"sortBy": "ソート方式"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} ファイル",
|
||||
"counterSize": "{0} ファイル (合計 {1})"
|
||||
},
|
||||
"noDataText": "データがありません"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Rader per side:",
|
||||
"sortBy": "Sorter etter"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} filer",
|
||||
"counterSize": "{0} filer ({1} totalt)"
|
||||
},
|
||||
"noDataText": "Ingen data er tilgjengelig"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Linhas por página:",
|
||||
"sortBy": "Ordenar por"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} arquivo(s)",
|
||||
"counterSize": "{0} arquivo(s) ({1} no total)"
|
||||
},
|
||||
"noDataText": "Não há dados disponíveis"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Строк на странице:",
|
||||
"sortBy": "Сортировать по"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "Файлов: {0}",
|
||||
"counterSize": "Файлов: {0} (всего {1})"
|
||||
},
|
||||
"noDataText": "Отсутствуют данные"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "Rader per sida:",
|
||||
"sortBy": "Sortera efter"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} filer",
|
||||
"counterSize": "{0} filer (av {1} totalt)"
|
||||
},
|
||||
"noDataText": "Ingen data tillgänglig"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"itemsPerPageText": "每页行数:",
|
||||
"sortBy": "排序方式"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} 个文件",
|
||||
"counterSize": "{0} 个文件(共 {1})"
|
||||
},
|
||||
"noDataText": "无数据可用"
|
||||
},
|
||||
"account_settings": {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ const router = new Router({
|
|||
beforeEnter: adminGuard,
|
||||
component: () => import(/* webpackChunkName: "settings-server" */ './views/SettingsServer.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/data-import',
|
||||
name: 'settings-data-import',
|
||||
beforeEnter: adminGuard,
|
||||
component: () => import(/* webpackChunkName: "settings-data-import" */ './views/SettingsDataImport.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AxiosInstance } from 'axios'
|
||||
import { BookDto } from '@/types/komga-books'
|
||||
import {AxiosInstance} from 'axios'
|
||||
import {BookDto} from '@/types/komga-books'
|
||||
|
||||
const qs = require('qs')
|
||||
|
||||
|
|
@ -8,19 +8,19 @@ const API_READLISTS = '/api/v1/readlists'
|
|||
export default class KomgaReadListsService {
|
||||
private http: AxiosInstance
|
||||
|
||||
constructor (http: AxiosInstance) {
|
||||
constructor(http: AxiosInstance) {
|
||||
this.http = http
|
||||
}
|
||||
|
||||
async getReadLists (libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise<Page<ReadListDto>> {
|
||||
async getReadLists(libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise<Page<ReadListDto>> {
|
||||
try {
|
||||
const params = { ...pageRequest } as any
|
||||
const params = {...pageRequest} as any
|
||||
if (libraryIds) params.library_id = libraryIds
|
||||
if (search) params.search = search
|
||||
|
||||
return (await this.http.get(API_READLISTS, {
|
||||
params: params,
|
||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve readLists'
|
||||
|
|
@ -31,7 +31,7 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async getOneReadList (readListId: string): Promise<ReadListDto> {
|
||||
async getOneReadList(readListId: string): Promise<ReadListDto> {
|
||||
try {
|
||||
return (await this.http.get(`${API_READLISTS}/${readListId}`)).data
|
||||
} catch (e) {
|
||||
|
|
@ -43,7 +43,7 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async postReadList (readList: ReadListCreationDto): Promise<ReadListDto> {
|
||||
async postReadList(readList: ReadListCreationDto): Promise<ReadListDto> {
|
||||
try {
|
||||
return (await this.http.post(API_READLISTS, readList)).data
|
||||
} catch (e) {
|
||||
|
|
@ -55,7 +55,25 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async patchReadList (readListId: string, readList: ReadListUpdateDto) {
|
||||
async postReadListImport(files: any): Promise<ReadListRequestResultDto[]> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
files.forEach((f: any) => formData.append("files", f))
|
||||
return (await this.http.post(`${API_READLISTS}/import`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to import readlists'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async patchReadList(readListId: string, readList: ReadListUpdateDto) {
|
||||
try {
|
||||
await this.http.patch(`${API_READLISTS}/${readListId}`, readList)
|
||||
} catch (e) {
|
||||
|
|
@ -67,7 +85,7 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async deleteReadList (readListId: string) {
|
||||
async deleteReadList(readListId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_READLISTS}/${readListId}`)
|
||||
} catch (e) {
|
||||
|
|
@ -79,9 +97,9 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async getBooks (readListId: string, pageRequest?: PageRequest): Promise<Page<BookDto>> {
|
||||
async getBooks(readListId: string, pageRequest?: PageRequest): Promise<Page<BookDto>> {
|
||||
try {
|
||||
const params = { ...pageRequest }
|
||||
const params = {...pageRequest}
|
||||
return (await this.http.get(`${API_READLISTS}/${readListId}/books`, {
|
||||
params: params,
|
||||
})).data
|
||||
|
|
@ -94,7 +112,7 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async getBookSiblingNext (readListId: string, bookId: string): Promise<BookDto> {
|
||||
async getBookSiblingNext(readListId: string, bookId: string): Promise<BookDto> {
|
||||
try {
|
||||
return (await this.http.get(`${API_READLISTS}/${readListId}/books/${bookId}/next`)).data
|
||||
} catch (e) {
|
||||
|
|
@ -106,7 +124,7 @@ export default class KomgaReadListsService {
|
|||
}
|
||||
}
|
||||
|
||||
async getBookSiblingPrevious (readListId: string, bookId: string): Promise<BookDto> {
|
||||
async getBookSiblingPrevious(readListId: string, bookId: string): Promise<BookDto> {
|
||||
try {
|
||||
return (await this.http.get(`${API_READLISTS}/${readListId}/books/${bookId}/previous`)).data
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -16,3 +16,20 @@ interface ReadListUpdateDto {
|
|||
name?: string,
|
||||
bookIds?: string[]
|
||||
}
|
||||
|
||||
interface ReadListRequestResultDto {
|
||||
readList?: ReadListDto,
|
||||
unmatchedBooks: ReadListRequestResultBookDto[],
|
||||
errorCode: string,
|
||||
requestName: string,
|
||||
}
|
||||
|
||||
interface ReadListRequestResultBookDto {
|
||||
book: ReadListRequestBookDto,
|
||||
errorCode: string,
|
||||
}
|
||||
|
||||
interface ReadListRequestBookDto {
|
||||
series: string,
|
||||
number: number,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<v-tab :to="{name: 'settings-analysis'}">{{ $t('media_analysis.media_analysis') }}</v-tab>
|
||||
<v-tab :to="{name: 'settings-users'}">{{ $t('users.users') }}</v-tab>
|
||||
<v-tab :to="{name: 'settings-server'}">{{ $t('server.tab_title') }}</v-tab>
|
||||
<v-tab :to="{name: 'settings-data-import'}">{{ $t('data_import.tab_title') }}</v-tab>
|
||||
</v-tabs>
|
||||
<router-view/>
|
||||
</div>
|
||||
|
|
|
|||
121
komga-webui/src/views/SettingsDataImport.vue
Normal file
121
komga-webui/src/views/SettingsDataImport.vue
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<v-row>
|
||||
<v-col class="text-h5">{{ $t('data_import.import_read_lists') }}</v-col>
|
||||
</v-row>
|
||||
<v-form v-model="valid" ref="form">
|
||||
<v-row>
|
||||
<v-col class="body-2" v-html="$t('data_import.comicrack_preambule_html')"></v-col>
|
||||
</v-row>
|
||||
<v-row align="center">
|
||||
<v-col cols>
|
||||
<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
|
||||
: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: 'SettingsDataImport',
|
||||
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