diff --git a/komga-webui/src/components/FileImportRow.vue b/komga-webui/src/components/FileImportRow.vue new file mode 100644 index 000000000..85db4d905 --- /dev/null +++ b/komga-webui/src/components/FileImportRow.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/komga-webui/src/components/PagesTable.vue b/komga-webui/src/components/PagesTable.vue new file mode 100644 index 000000000..34f8dcf27 --- /dev/null +++ b/komga-webui/src/components/PagesTable.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/komga-webui/src/components/RtlIcon.vue b/komga-webui/src/components/RtlIcon.vue new file mode 100644 index 000000000..5df7d5122 --- /dev/null +++ b/komga-webui/src/components/RtlIcon.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/FileNameChooserDialog.vue b/komga-webui/src/components/dialogs/FileNameChooserDialog.vue new file mode 100644 index 000000000..de9e4453e --- /dev/null +++ b/komga-webui/src/components/dialogs/FileNameChooserDialog.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/SeriesPickerDialog.vue b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue new file mode 100644 index 000000000..21ed89a37 --- /dev/null +++ b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/TransientBookDetailsDialog.vue b/komga-webui/src/components/dialogs/TransientBookDetailsDialog.vue new file mode 100644 index 000000000..bd76887b9 --- /dev/null +++ b/komga-webui/src/components/dialogs/TransientBookDetailsDialog.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/komga-webui/src/components/dialogs/TransientBookViewerDialog.vue b/komga-webui/src/components/dialogs/TransientBookViewerDialog.vue new file mode 100644 index 000000000..824d5d0d0 --- /dev/null +++ b/komga-webui/src/components/dialogs/TransientBookViewerDialog.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/komga-webui/src/functions/urls.ts b/komga-webui/src/functions/urls.ts index 849fc498f..1d4192ef0 100644 --- a/komga-webui/src/functions/urls.ts +++ b/komga-webui/src/functions/urls.ts @@ -47,3 +47,7 @@ export function collectionThumbnailUrl (collectionId: string): string { export function readListThumbnailUrl (readListId: string): string { return `${urls.originNoSlash}/api/v1/readlists/${readListId}/thumbnail` } + +export function transientBookPageUrl (transientBookId: string, page: number): string { + return `${urls.originNoSlash}/api/v1/transient-books/${transientBookId}/pages/${page}` +} diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index aafb92a01..770c27745 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -353,6 +353,44 @@ "shortcut_help": { "label_description": "Description", "label_key": "Key" + }, + "series_picker": { + "title": "Select Series", + "label_search_series": "Search Series" + }, + "filename_chooser": { + "title": "Destination File Name", + "label_source_filename": "Source File Name", + "field_destination_filename": "Destination file name", + "button_choose": "Choose", + "table": { + "order": "Order", + "existing_file": "Existing File" + } + }, + "transient_book_details": { + "title": "Book Details", + "title_comparison": "Book Comparison", + "label_candidate": "Candidate", + "label_existing": "Existing", + "label_name": "Name", + "label_size": "Size", + "label_format": "Format", + "label_pages": "Pages", + "pages_table": { + "index": "Index", + "filename": "File name", + "media_type": "Media type", + "width": "Width", + "height": "Height" + } + }, + "transient_book_viewer": { + "title": "Inspect Book", + "title_comparison": "Book Comparison", + "label_candidate": "Candidate", + "label_existing": "Existing", + "page_of_pages": "{page} / {pages}" } }, "enums": { @@ -374,6 +412,10 @@ "ENDED": "Ended", "HIATUS": "Hiatus", "ONGOING": "Ongoing" + }, + "copy_mode": { + "HARDLINK": "Hardlink/Copy Files", + "MOVE": "Move Files" } }, "error_codes": { @@ -519,5 +561,25 @@ "add_library": "Add library", "no_libraries_yet": "No libraries have been added yet!", "welcome_message": "Welcome to Komga" + }, + "book_import": { + "title": "Import", + "field_import_path": "Import from folder", + "button_browse": "Browse", + "button_scan": "Scan", + "table": { + "file_name": "File name", + "series": "Series", + "number": "Number", + "destination_name": "Destination name" + }, + "button_select_series": "Select Series", + "button_import": "Import", + "row": { + "warning_upgrade": "Existing book will be upgraded", + "error_analyze_first": "Book needs to be analyzed first", + "error_only_import_no_errors": "Can only import books without errors", + "error_choose_series": "Choose a series" + } } } diff --git a/komga-webui/src/main.ts b/komga-webui/src/main.ts index 967449d5d..98963f0f4 100644 --- a/komga-webui/src/main.ts +++ b/komga-webui/src/main.ts @@ -17,6 +17,7 @@ import komgaLibraries from './plugins/komga-libraries.plugin' import komgaReferential from './plugins/komga-referential.plugin' import komgaSeries from './plugins/komga-series.plugin' import komgaUsers from './plugins/komga-users.plugin' +import komgaTransientBooks from './plugins/komga-transientbooks.plugin' import vuetify from './plugins/vuetify' import './public-path' import router from './router' @@ -35,6 +36,7 @@ Vue.use(komgaReadLists, {http: Vue.prototype.$http}) Vue.use(komgaBooks, {http: Vue.prototype.$http}) Vue.use(komgaReferential, {http: Vue.prototype.$http}) Vue.use(komgaClaim, {http: Vue.prototype.$http}) +Vue.use(komgaTransientBooks, {http: Vue.prototype.$http}) Vue.use(komgaUsers, {store: store, http: Vue.prototype.$http}) Vue.use(komgaLibraries, {store: store, http: Vue.prototype.$http}) Vue.use(actuator, {http: Vue.prototype.$http}) diff --git a/komga-webui/src/plugins/komga-transientbooks.plugin.ts b/komga-webui/src/plugins/komga-transientbooks.plugin.ts new file mode 100644 index 000000000..231a2f411 --- /dev/null +++ b/komga-webui/src/plugins/komga-transientbooks.plugin.ts @@ -0,0 +1,20 @@ +import KomgaTransientBooksService from '@/services/komga-transientbooks.service' +import {AxiosInstance} from 'axios' +import _Vue from 'vue' + +let service: KomgaTransientBooksService + +export default { + install ( + Vue: typeof _Vue, + { http }: { http: AxiosInstance }) { + service = new KomgaTransientBooksService(http) + Vue.prototype.$komgaTransientBooks = service + }, +} + +declare module 'vue/types/vue' { + interface Vue { + $komgaTransientBooks: KomgaTransientBooksService; + } +} diff --git a/komga-webui/src/router.ts b/komga-webui/src/router.ts index 29373a691..03378c0d3 100644 --- a/komga-webui/src/router.ts +++ b/komga-webui/src/router.ts @@ -133,6 +133,12 @@ const router = new Router({ name: 'search', component: () => import(/* webpackChunkName: "search" */ './views/Search.vue'), }, + { + path: '/import', + name: 'import', + beforeEnter: adminGuard, + component: () => import(/* webpackChunkName: "book-import" */ './views/BookImport.vue'), + }, ], }, { diff --git a/komga-webui/src/services/komga-books.service.ts b/komga-webui/src/services/komga-books.service.ts index a47421754..e99042a72 100644 --- a/komga-webui/src/services/komga-books.service.ts +++ b/komga-webui/src/services/komga-books.service.ts @@ -1,5 +1,5 @@ -import { AxiosInstance } from 'axios' -import { BookDto, BookMetadataUpdateDto, PageDto, ReadProgressUpdateDto } from '@/types/komga-books' +import {AxiosInstance} from 'axios' +import {BookDto, BookImportBatchDto, BookMetadataUpdateDto, PageDto, ReadProgressUpdateDto} from '@/types/komga-books' const qs = require('qs') @@ -173,4 +173,16 @@ export default class KomgaBooksService { throw new Error(msg) } } + + async importBooks(batch: BookImportBatchDto) { + try { + await this.http.post(`${API_BOOKS}/import`, batch) + } catch (e) { + let msg = `An error occurred while trying to submit book import batch` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } } diff --git a/komga-webui/src/services/komga-transientbooks.service.ts b/komga-webui/src/services/komga-transientbooks.service.ts new file mode 100644 index 000000000..e9709048f --- /dev/null +++ b/komga-webui/src/services/komga-transientbooks.service.ts @@ -0,0 +1,39 @@ +import {AxiosInstance} from 'axios' +import {TransientBookDto} from "@/types/komga-transientbooks"; + +const API_TRANSIENT_BOOKS = '/api/v1/transient-books' + +export default class KomgaTransientBooksService { + private http: AxiosInstance + + constructor (http: AxiosInstance) { + this.http = http + } + + async scanForTransientBooks (path: string): Promise { + try { + return (await this.http.post(API_TRANSIENT_BOOKS, { + path: path, + })).data + } catch (e) { + let msg = `An error occurred while trying to scan for transient book` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async analyze (id: string): Promise { + try { + return (await this.http.post(`${API_TRANSIENT_BOOKS}/${id}/analyze`)).data + } catch (e) { + let msg = `An error occurred while trying to analyze transient book` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + +} diff --git a/komga-webui/src/types/enum-books.ts b/komga-webui/src/types/enum-books.ts index 74a7ddcfd..b509c53a1 100644 --- a/komga-webui/src/types/enum-books.ts +++ b/komga-webui/src/types/enum-books.ts @@ -18,3 +18,9 @@ export enum ReadStatus { IN_PROGRESS = 'IN_PROGRESS', READ = 'READ' } + +export enum CopyMode { + MOVE = 'MOVE', + COPY = 'COPY', + HARDLINK = 'HARDLINK', +} diff --git a/komga-webui/src/types/komga-books.ts b/komga-webui/src/types/komga-books.ts index d6aa0af3d..ea0398f4f 100644 --- a/komga-webui/src/types/komga-books.ts +++ b/komga-webui/src/types/komga-books.ts @@ -1,4 +1,5 @@ import {Context} from '@/types/context' +import {CopyMode} from "@/types/enum-books"; export interface BookDto { id: string, @@ -103,3 +104,15 @@ export interface BookFormat { type: string, color: string } + +export interface BookImportBatchDto{ + books: BookImportDto[], + copyMode: CopyMode, +} + +export interface BookImportDto { + sourceFile: string, + seriesId: string, + upgradeBookId?: string, + destinationName?: string, +} diff --git a/komga-webui/src/types/komga-transientbooks.ts b/komga-webui/src/types/komga-transientbooks.ts new file mode 100644 index 000000000..4780f8f5d --- /dev/null +++ b/komga-webui/src/types/komga-transientbooks.ts @@ -0,0 +1,19 @@ +import {PageDto} from "@/types/komga-books"; + +export interface ScanRequestDto { + path: string, +} + +export interface TransientBookDto { + id: string, + name: string, + url: string, + fileLastModified: string, + sizeBytes: number, + size: string, + status: string, + mediaType: string, + pages: PageDto[], + files: string[], + comment: string, +} diff --git a/komga-webui/src/views/BookImport.vue b/komga-webui/src/views/BookImport.vue new file mode 100644 index 000000000..22073f885 --- /dev/null +++ b/komga-webui/src/views/BookImport.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/komga-webui/src/views/Home.vue b/komga-webui/src/views/Home.vue index e2eaffef0..7018d5dde 100644 --- a/komga-webui/src/views/Home.vue +++ b/komga-webui/src/views/Home.vue @@ -69,6 +69,15 @@ + + + mdi-import + + + {{ $t('book_import.title') }} + + + mdi-cog