mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
create library
This commit is contained in:
parent
b9ea59186b
commit
16e9f1a950
23 changed files with 1392 additions and 11 deletions
|
|
@ -1,7 +1,13 @@
|
|||
import { defineQuery, useQuery } from '@pinia/colada'
|
||||
import { defineMutation, defineQuery, useMutation, useQuery, useQueryCache } from '@pinia/colada'
|
||||
import { komgaClient } from '@/api/komga-client'
|
||||
import { useClientSettingsUser } from '@/colada/client-settings'
|
||||
import { combinePromises } from '@/colada/utils'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { QUERY_KEYS_USERS } from '@/colada/users'
|
||||
|
||||
export const QUERY_KEYS_LIBRARIES = {
|
||||
root: ['libraries'] as const,
|
||||
}
|
||||
|
||||
export const useLibraries = defineQuery(() => {
|
||||
const {
|
||||
|
|
@ -10,7 +16,7 @@ export const useLibraries = defineQuery(() => {
|
|||
refetch: refetchLibraries,
|
||||
...rest
|
||||
} = useQuery({
|
||||
key: () => ['libraries'],
|
||||
key: () => QUERY_KEYS_LIBRARIES.root,
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v1/libraries')
|
||||
|
|
@ -54,3 +60,34 @@ export const useLibraries = defineQuery(() => {
|
|||
...rest,
|
||||
}
|
||||
})
|
||||
|
||||
export const useCreateLibrary = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: (library: components['schemas']['LibraryCreationDto']) =>
|
||||
komgaClient.POST('/api/v1/libraries', {
|
||||
body: library,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryCache.invalidateQueries({ key: QUERY_KEYS_LIBRARIES.root })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const useUpdateLibrary = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: (library: components['schemas']['LibraryDto']) =>
|
||||
komgaClient.PATCH('/api/v1/libraries/{libraryId}', {
|
||||
params: {
|
||||
path: {
|
||||
libraryId: library.id,
|
||||
},
|
||||
},
|
||||
body: library,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryCache.invalidateQueries({ key: QUERY_KEYS_LIBRARIES.root })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
7
next-ui/src/components.d.ts
vendored
7
next-ui/src/components.d.ts
vendored
|
|
@ -54,6 +54,13 @@ declare module 'vue' {
|
|||
LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default']
|
||||
LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default']
|
||||
LayoutAppDrawerReorderLibraries: typeof import('./components/layout/app/drawer/ReorderLibraries.vue')['default']
|
||||
LibraryFormCreateEdit: typeof import('./components/library/form/CreateEdit.vue')['default']
|
||||
LibraryFormGeneral: typeof import('./components/library/form/General.vue')['default']
|
||||
LibraryFormStepGeneral: typeof import('./components/library/form/StepGeneral.vue')['default']
|
||||
LibraryFormStepMetadata: typeof import('./components/library/form/StepMetadata.vue')['default']
|
||||
LibraryFormStepOptions: typeof import('./components/library/form/StepOptions.vue')['default']
|
||||
LibraryFormStepScanner: typeof import('./components/library/form/StepScanner.vue')['default']
|
||||
LibraryMenuLibraries: typeof import('./components/library/MenuLibraries.vue')['default']
|
||||
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
|
||||
MenuLibraries: typeof import('./components/menu/MenuLibraries.vue')['default']
|
||||
PageHashKnownTable: typeof import('./components/pageHash/KnownTable.vue')['default']
|
||||
|
|
|
|||
|
|
@ -64,7 +64,14 @@
|
|||
<v-btn
|
||||
:loading="loading"
|
||||
:disabled="!formValid"
|
||||
:text="okText"
|
||||
:text="
|
||||
okText ||
|
||||
$formatMessage({
|
||||
description: 'Confirmation dialog: Confirm button',
|
||||
defaultMessage: 'Confirm',
|
||||
id: '33t+CB',
|
||||
})
|
||||
"
|
||||
type="submit"
|
||||
variant="elevated"
|
||||
rounded="xs"
|
||||
|
|
@ -111,7 +118,7 @@ export type DialogConfirmProps = {
|
|||
const {
|
||||
title = undefined,
|
||||
subtitle = undefined,
|
||||
okText = 'Confirm',
|
||||
okText = undefined,
|
||||
validateText = 'confirm',
|
||||
maxWidth = undefined,
|
||||
activator = undefined,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
:subtitle="subtitle"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #text>
|
||||
<v-card-text :class="cardTextClass">
|
||||
<slot
|
||||
name="text"
|
||||
:proxy-model="proxyModel"
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
:save="save"
|
||||
:is-pristine="isPristine"
|
||||
/>
|
||||
</template>
|
||||
</v-card-text>
|
||||
|
||||
<template #actions>
|
||||
<v-spacer />
|
||||
|
|
@ -50,6 +50,7 @@
|
|||
/>
|
||||
<v-btn
|
||||
:text="
|
||||
okText ||
|
||||
$formatMessage({
|
||||
description: 'ConfirmEdit dialog: Save button',
|
||||
defaultMessage: 'Save',
|
||||
|
|
@ -86,6 +87,8 @@ export type DialogConfirmEditProps = {
|
|||
*/
|
||||
title?: string
|
||||
subtitle?: string
|
||||
okText?: string
|
||||
cardTextClass?: string
|
||||
maxWidth?: string | number
|
||||
activator?: Element | string
|
||||
loading?: boolean
|
||||
|
|
@ -97,6 +100,8 @@ export type DialogConfirmEditProps = {
|
|||
const {
|
||||
title = undefined,
|
||||
subtitle = undefined,
|
||||
okText = undefined,
|
||||
cardTextClass = undefined,
|
||||
maxWidth = undefined,
|
||||
activator = undefined,
|
||||
loading = false,
|
||||
|
|
|
|||
|
|
@ -7,15 +7,19 @@ import { expect, waitFor } from 'storybook/test'
|
|||
import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/ClientSettingsUser'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { VList } from 'vuetify/components'
|
||||
import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
|
||||
import SnackQueue from '@/components/SnackQueue.vue'
|
||||
import { delay, http } from 'msw'
|
||||
import { response401Unauthorized } from '@/mocks/api/handlers'
|
||||
|
||||
const meta = {
|
||||
component: Libraries,
|
||||
render: (args: object) => ({
|
||||
components: { Libraries, VList },
|
||||
components: { Libraries, VList, DialogConfirmEditInstance, SnackQueue },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<v-list nav><Libraries /></v-list>',
|
||||
template: '<v-list nav><Libraries /></v-list><DialogConfirmEditInstance/><SnackQueue/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
|
|
@ -100,3 +104,23 @@ export const Ordered: Story = {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [http.all('*/api/*', async () => await delay(5_000))],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const CreationError: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.post('/api/v1/libraries', ({ response }) =>
|
||||
response.untyped(response401Unauthorized()),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
id: '90yqRq',
|
||||
})
|
||||
"
|
||||
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
|
||||
@click="createLibrary"
|
||||
/>
|
||||
<v-icon-btn
|
||||
id="menu-libraries-drawer"
|
||||
|
|
@ -32,7 +34,7 @@
|
|||
})
|
||||
"
|
||||
/>
|
||||
<MenuLibraries activator-id="#menu-libraries-drawer" />
|
||||
<LibraryMenuLibraries activator-id="#menu-libraries-drawer" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
|
|
@ -99,12 +101,83 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLibraries } from '@/colada/libraries'
|
||||
import { useCreateLibrary, useLibraries } from '@/colada/libraries'
|
||||
import { useCurrentUser } from '@/colada/users'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import CreateEdit from '@/components/library/form/CreateEdit.vue'
|
||||
import { getLibraryDefaults } from '@/modules/libraries'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { useMessagesStore } from '@/stores/messages'
|
||||
import type { ErrorCause } from '@/api/komga-client'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
|
||||
const intl = useIntl()
|
||||
const display = useDisplay()
|
||||
const { unpinned, pinned, refresh } = useLibraries()
|
||||
const { isAdmin } = useCurrentUser()
|
||||
|
||||
// ensure freshness, especially if libraries have been reordered
|
||||
void refresh()
|
||||
|
||||
const { confirmEdit: dialogConfirmEdit } = storeToRefs(useDialogsStore())
|
||||
const { mutateAsync: mutateCreateLibrary } = useCreateLibrary()
|
||||
const messagesStore = useMessagesStore()
|
||||
|
||||
function createLibrary() {
|
||||
dialogConfirmEdit.value.dialogProps = {
|
||||
title: intl.formatMessage({
|
||||
description: 'Create library dialog title',
|
||||
defaultMessage: 'Create library',
|
||||
id: 'nuoJ1n',
|
||||
}),
|
||||
maxWidth: 600,
|
||||
okText: 'Create',
|
||||
cardTextClass: 'px-0',
|
||||
closeOnSave: false,
|
||||
scrollable: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirmEdit.value.slot = {
|
||||
component: markRaw(CreateEdit),
|
||||
props: { createMode: true },
|
||||
}
|
||||
dialogConfirmEdit.value.record = getLibraryDefaults()
|
||||
dialogConfirmEdit.value.callback = handleDialogConfirmation
|
||||
}
|
||||
|
||||
function handleDialogConfirmation(
|
||||
hideDialog: () => void,
|
||||
setLoading: (isLoading: boolean) => void,
|
||||
) {
|
||||
setLoading(true)
|
||||
|
||||
const newLib = dialogConfirmEdit.value.record as components['schemas']['LibraryCreationDto']
|
||||
|
||||
mutateCreateLibrary(newLib)
|
||||
.then(() => {
|
||||
hideDialog()
|
||||
messagesStore.messages.push({
|
||||
text: intl.formatMessage(
|
||||
{
|
||||
description: 'Snackbar notification shown upon successful library creation',
|
||||
defaultMessage: 'Library created: {library}',
|
||||
id: '+8++PW',
|
||||
},
|
||||
{
|
||||
library: newLib.name,
|
||||
},
|
||||
),
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message || intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
67
next-ui/src/components/library/form/CreateEdit.stories.ts
Normal file
67
next-ui/src/components/library/form/CreateEdit.stories.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import CreateEdit from './CreateEdit.vue'
|
||||
import { ScanInterval } from '@/types/ScanInterval'
|
||||
import { SeriesCover } from '@/types/SeriesCover'
|
||||
import { getLibraryDefaults } from '@/modules/libraries'
|
||||
|
||||
const meta = {
|
||||
component: CreateEdit,
|
||||
render: (args: object) => ({
|
||||
components: { CreateEdit },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<CreateEdit :model-value="args.modelValue" v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof CreateEdit>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Create: Story = {
|
||||
args: {
|
||||
createMode: true,
|
||||
modelValue: getLibraryDefaults(),
|
||||
},
|
||||
}
|
||||
|
||||
export const Edit: Story = {
|
||||
args: {
|
||||
createMode: false,
|
||||
modelValue: {
|
||||
analyzeDimensions: false,
|
||||
convertToCbz: false,
|
||||
emptyTrashAfterScan: false,
|
||||
hashFiles: false,
|
||||
hashKoreader: false,
|
||||
hashPages: false,
|
||||
importBarcodeIsbn: false,
|
||||
importComicInfoBook: false,
|
||||
importComicInfoCollection: false,
|
||||
importComicInfoReadList: false,
|
||||
importComicInfoSeries: false,
|
||||
importComicInfoSeriesAppendVolume: false,
|
||||
importEpubBook: false,
|
||||
importEpubSeries: false,
|
||||
importLocalArtwork: false,
|
||||
importMylarSeries: false,
|
||||
name: 'Existing',
|
||||
oneshotsDirectory: '_oneshots',
|
||||
repairExtensions: false,
|
||||
root: '/comics',
|
||||
scanCbx: true,
|
||||
scanDirectoryExclusions: [],
|
||||
scanEpub: true,
|
||||
scanForceModifiedTime: false,
|
||||
scanInterval: ScanInterval.DAILY,
|
||||
scanOnStartup: false,
|
||||
scanPdf: true,
|
||||
seriesCover: SeriesCover.FIRST,
|
||||
},
|
||||
},
|
||||
}
|
||||
127
next-ui/src/components/library/form/CreateEdit.vue
Normal file
127
next-ui/src/components/library/form/CreateEdit.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<v-stepper-vertical
|
||||
:hide-actions="editMode"
|
||||
eager
|
||||
flat
|
||||
>
|
||||
<template #default="{ step }">
|
||||
<v-stepper-vertical-item
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: General',
|
||||
defaultMessage: 'General',
|
||||
id: 'h6C8/l',
|
||||
})
|
||||
"
|
||||
value="1"
|
||||
:complete="createMode && step > 1"
|
||||
:editable="editMode"
|
||||
>
|
||||
<LibraryFormStepGeneral v-model="model" />
|
||||
|
||||
<template #next="{ next }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="next"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template #prev></template>
|
||||
</v-stepper-vertical-item>
|
||||
|
||||
<v-stepper-vertical-item
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner',
|
||||
defaultMessage: 'Scanner',
|
||||
id: 'yaa8so',
|
||||
})
|
||||
"
|
||||
value="2"
|
||||
:complete="createMode && step > 2"
|
||||
:editable="editMode"
|
||||
>
|
||||
<LibraryFormStepScanner v-model="model" />
|
||||
|
||||
<template #next="{ next }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="next"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template #prev="{ prev }">
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="prev"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-stepper-vertical-item>
|
||||
|
||||
<v-stepper-vertical-item
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options',
|
||||
defaultMessage: 'Options',
|
||||
id: 'uGC9fD',
|
||||
})
|
||||
"
|
||||
value="3"
|
||||
:complete="createMode && step > 3"
|
||||
:editable="editMode"
|
||||
>
|
||||
<LibraryFormStepOptions v-model="model" />
|
||||
|
||||
<template #next="{ next }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="next"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template #prev="{ prev }">
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="prev"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-stepper-vertical-item>
|
||||
|
||||
<v-stepper-vertical-item
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata',
|
||||
defaultMessage: 'Metadata',
|
||||
id: '0iT7Vf',
|
||||
})
|
||||
"
|
||||
value="4"
|
||||
:complete="createMode && step > 4"
|
||||
:editable="editMode"
|
||||
>
|
||||
<LibraryFormStepMetadata v-model="model" />
|
||||
|
||||
<template #next=""></template>
|
||||
|
||||
<template #prev="{ prev }">
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="prev"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-stepper-vertical-item>
|
||||
</template>
|
||||
</v-stepper-vertical>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
const { createMode } = defineProps<{
|
||||
createMode: boolean
|
||||
}>()
|
||||
|
||||
const editMode = computed(() => !createMode)
|
||||
|
||||
const model = defineModel<components['schemas']['LibraryCreationDto']>({ required: true })
|
||||
</script>
|
||||
30
next-ui/src/components/library/form/StepGeneral.stories.ts
Normal file
30
next-ui/src/components/library/form/StepGeneral.stories.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import StepGeneral from './StepGeneral.vue'
|
||||
|
||||
const meta = {
|
||||
component: StepGeneral,
|
||||
render: (args: object) => ({
|
||||
components: { StepGeneral },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StepGeneral :model-value="args.modelValue" v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof StepGeneral>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: {
|
||||
name: '',
|
||||
root: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
82
next-ui/src/components/library/form/StepGeneral.vue
Normal file
82
next-ui/src/components/library/form/StepGeneral.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<v-container class="px-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="model.name"
|
||||
:rules="['required']"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: General - library name',
|
||||
defaultMessage: 'Library name',
|
||||
id: 's1nzhU',
|
||||
})
|
||||
"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="model.root"
|
||||
:rules="['required']"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: General - root directory',
|
||||
defaultMessage: 'Root directory',
|
||||
id: 'afXGQS',
|
||||
})
|
||||
"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
id="ID01KC5HANV8QMDAW8GW4HFZCY0B"
|
||||
:text="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: General - root folder browse button',
|
||||
defaultMessage: 'Browse',
|
||||
id: 'E1kQun',
|
||||
})
|
||||
"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<DialogConfirmEdit
|
||||
v-model:record="model.root"
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: General - root directory selection dialog title',
|
||||
defaultMessage: 'Library root directory',
|
||||
id: 'CJaS7j',
|
||||
})
|
||||
"
|
||||
max-width="600"
|
||||
close-on-save
|
||||
scrollable
|
||||
:fullscreen="display.xs.value"
|
||||
activator="#ID01KC5HANV8QMDAW8GW4HFZCY0B"
|
||||
@update:record="(val) => (model.root = val as string)"
|
||||
>
|
||||
<template #text="{ proxyModel }">
|
||||
<RemoteFileList v-model="proxyModel.value as string" />
|
||||
</template>
|
||||
</DialogConfirmEdit>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import RemoteFileList from '@/components/RemoteFileList.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const display = useDisplay()
|
||||
|
||||
type LibraryCreationGeneral = Pick<components['schemas']['LibraryCreationDto'], 'name' | 'root'>
|
||||
|
||||
const model = defineModel<LibraryCreationGeneral>({ required: true })
|
||||
</script>
|
||||
38
next-ui/src/components/library/form/StepMetadata.stories.ts
Normal file
38
next-ui/src/components/library/form/StepMetadata.stories.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import StepMetadata from './StepMetadata.vue'
|
||||
|
||||
const meta = {
|
||||
component: StepMetadata,
|
||||
render: (args: object) => ({
|
||||
components: { StepMetadata },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StepMetadata :model-value="args.modelValue" v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof StepMetadata>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: {
|
||||
importBarcodeIsbn: false,
|
||||
importComicInfoBook: false,
|
||||
importComicInfoCollection: false,
|
||||
importComicInfoReadList: false,
|
||||
importComicInfoSeries: false,
|
||||
importComicInfoSeriesAppendVolume: false,
|
||||
importEpubBook: false,
|
||||
importEpubSeries: false,
|
||||
importLocalArtwork: false,
|
||||
importMylarSeries: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
231
next-ui/src/components/library/form/StepMetadata.vue
Normal file
231
next-ui/src/components/library/form/StepMetadata.vue
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<template>
|
||||
<v-container class="px-0">
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<div class="text-subtitle-2 d-flex ga-2 align-center">
|
||||
<div>
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - section header for comicinfo',
|
||||
defaultMessage: 'ComicInfo.xml (CBR/CBZ)',
|
||||
id: 'fVqdik',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importComicInfoBook"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - comicinfo book metadata',
|
||||
defaultMessage: 'Book metadata',
|
||||
id: 'YHQNM8',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importComicInfoSeries"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - comicinfo series metadata',
|
||||
defaultMessage: 'Series metadata',
|
||||
id: '6zF2Um',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importComicInfoSeriesAppendVolume"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description:
|
||||
'Form add/edit library: Metadata - comicinfo append volume to series title',
|
||||
defaultMessage: 'Append volume to series title',
|
||||
id: 'AjBiEw',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importComicInfoCollection"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - comcinfo collections',
|
||||
defaultMessage: 'Collections',
|
||||
id: 'Ahipg5',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importComicInfoReadList"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - comcinfo read lists',
|
||||
defaultMessage: 'Read lists',
|
||||
id: 'AV3Iae',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-2">
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - section header for epub',
|
||||
defaultMessage: 'EPUB',
|
||||
id: '4PDVX2',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<v-checkbox
|
||||
v-model="model.importEpubBook"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - epub book metadata',
|
||||
defaultMessage: 'Book metadata',
|
||||
id: '7bZ3Eg',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importEpubSeries"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - epub series metadata',
|
||||
defaultMessage: 'Series metadata',
|
||||
id: '8PXwjO',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-2">
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - section header for mylar series.json',
|
||||
defaultMessage: 'series.json (Mylar)',
|
||||
id: 'xofdkF',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importMylarSeries"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - mylar series metadata',
|
||||
defaultMessage: 'Series metadata',
|
||||
id: 'JvSGEY',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-2">
|
||||
{{
|
||||
$formatMessage({
|
||||
description:
|
||||
'Form add/edit library: Metadata - section header for local media assets',
|
||||
defaultMessage: 'Local media assets',
|
||||
id: 'COn5A6',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importLocalArtwork"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - local artwork',
|
||||
defaultMessage: 'Local artwork',
|
||||
id: 'iBj0mW',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-2 d-flex ga-2 align-center">
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - section header for ISBN barcode',
|
||||
defaultMessage: 'ISBN within barcode',
|
||||
id: 'eHS96Q',
|
||||
})
|
||||
}}
|
||||
<v-icon
|
||||
v-tooltip:bottom="$formatMessage(commonMessages.resourceIntensive)"
|
||||
size="small"
|
||||
color="warning"
|
||||
icon="i-mdi:alert-circle-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.importBarcodeIsbn"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Metadata - isbn barcode',
|
||||
defaultMessage: 'ISBN',
|
||||
id: 'cKUU8S',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
type LibraryCreationMetadata = Pick<
|
||||
components['schemas']['LibraryCreationDto'],
|
||||
| 'importComicInfoBook'
|
||||
| 'importComicInfoSeries'
|
||||
| 'importComicInfoSeriesAppendVolume'
|
||||
| 'importComicInfoCollection'
|
||||
| 'importComicInfoReadList'
|
||||
| 'importEpubBook'
|
||||
| 'importEpubSeries'
|
||||
| 'importMylarSeries'
|
||||
| 'importLocalArtwork'
|
||||
| 'importBarcodeIsbn'
|
||||
>
|
||||
|
||||
const model = defineModel<LibraryCreationMetadata>({ required: true })
|
||||
</script>
|
||||
37
next-ui/src/components/library/form/StepOptions.stories.ts
Normal file
37
next-ui/src/components/library/form/StepOptions.stories.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import StepOptions from './StepOptions.vue'
|
||||
import { SeriesCover } from '@/types/SeriesCover'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta = {
|
||||
component: StepOptions,
|
||||
render: (args: object) => ({
|
||||
components: { StepOptions },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StepOptions :model-value="args.modelValue" v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof StepOptions>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: {
|
||||
analyzeDimensions: false,
|
||||
convertToCbz: false,
|
||||
hashFiles: false,
|
||||
hashKoreader: false,
|
||||
hashPages: false,
|
||||
repairExtensions: false,
|
||||
seriesCover: SeriesCover.FIRST,
|
||||
},
|
||||
},
|
||||
}
|
||||
217
next-ui/src/components/library/form/StepOptions.vue
Normal file
217
next-ui/src/components/library/form/StepOptions.vue
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<template>
|
||||
<v-container class="px-0">
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<div class="text-subtitle-2 d-flex ga-2 align-center">
|
||||
<div>
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - section header for analysis',
|
||||
defaultMessage: 'Analysis',
|
||||
id: 'O/3awV',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<v-icon
|
||||
v-tooltip:bottom="$formatMessage(commonMessages.resourceIntensive)"
|
||||
size="small"
|
||||
color="warning"
|
||||
icon="i-mdi:alert-circle-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.hashFiles"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - hash files',
|
||||
defaultMessage: 'Compute hash for files',
|
||||
id: 'wJaip6',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-tooltip:bottom="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - hash files - information tooltip',
|
||||
defaultMessage: 'Required to restore from trash and detect duplicate files',
|
||||
id: '/8sSxS',
|
||||
})
|
||||
"
|
||||
icon="i-mdi:information-outline"
|
||||
></v-icon>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.hashPages"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - hash pages',
|
||||
defaultMessage: 'Compute hash for pages',
|
||||
id: '0qntYX',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-tooltip:bottom="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - hash pages - information tooltip',
|
||||
defaultMessage: 'Required for detecting duplicate pages',
|
||||
id: 'Pj29A+',
|
||||
})
|
||||
"
|
||||
icon="i-mdi:information-outline"
|
||||
></v-icon>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.hashKoreader"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - koreader hash',
|
||||
defaultMessage: 'Compute hash for KOReader',
|
||||
id: 'nXFVsQ',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-tooltip:bottom="
|
||||
$formatMessage({
|
||||
description:
|
||||
'Form add/edit library: Options - koreader hash - information tooltip',
|
||||
defaultMessage: 'Enable this if you use KOReader Sync',
|
||||
id: 'DNmepU',
|
||||
})
|
||||
"
|
||||
icon="i-mdi:information-outline"
|
||||
></v-icon>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.analyzeDimensions"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - analyze page dimensions',
|
||||
defaultMessage: 'Analyze pages dimensions',
|
||||
id: 'STdfYg',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-tooltip:bottom="
|
||||
$formatMessage({
|
||||
description:
|
||||
'Form add/edit library: Options - analyze page dimensions - information tooltip',
|
||||
defaultMessage: 'Required for the WebReader to detect landscape pages',
|
||||
id: 'ByRsV9',
|
||||
})
|
||||
"
|
||||
icon="i-mdi:information-outline"
|
||||
></v-icon>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-2">
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - section header for file management',
|
||||
defaultMessage: 'File management',
|
||||
id: 'rks1H9',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<v-checkbox
|
||||
v-model="model.repairExtensions"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - repair extensions',
|
||||
defaultMessage: 'Automatically repair incorrect file extensions',
|
||||
id: 'RwuMl5',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.convertToCbz"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - convert to cbz',
|
||||
defaultMessage: 'Automatically convert CBR to CBZ',
|
||||
id: 'b1hvh9',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-2">
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Options - section header for series cover',
|
||||
defaultMessage: 'Series cover',
|
||||
id: 'Bewgy6',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="model.seriesCover"
|
||||
:items="seriesCoverOptions"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { SeriesCover, seriesCoverMessages } from '@/types/SeriesCover'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
type LibraryCreationOptions = Pick<
|
||||
components['schemas']['LibraryCreationDto'],
|
||||
| 'hashFiles'
|
||||
| 'hashPages'
|
||||
| 'hashKoreader'
|
||||
| 'analyzeDimensions'
|
||||
| 'repairExtensions'
|
||||
| 'convertToCbz'
|
||||
| 'seriesCover'
|
||||
>
|
||||
|
||||
const model = defineModel<LibraryCreationOptions>({ required: true })
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const seriesCoverOptions = Object.values(SeriesCover).map((x) => ({
|
||||
title: intl.formatMessage(seriesCoverMessages[x]),
|
||||
value: x,
|
||||
}))
|
||||
</script>
|
||||
38
next-ui/src/components/library/form/StepScanner.stories.ts
Normal file
38
next-ui/src/components/library/form/StepScanner.stories.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import StepScanner from './StepScanner.vue'
|
||||
import { ScanInterval } from '@/types/ScanInterval'
|
||||
|
||||
const meta = {
|
||||
component: StepScanner,
|
||||
render: (args: object) => ({
|
||||
components: { StepScanner },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StepScanner :model-value="args.modelValue" v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof StepScanner>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: {
|
||||
emptyTrashAfterScan: false,
|
||||
oneshotsDirectory: '_oneshots',
|
||||
scanCbx: true,
|
||||
scanDirectoryExclusions: ['#recycle', '@eaDir', '@Recycle'],
|
||||
scanEpub: true,
|
||||
scanForceModifiedTime: false,
|
||||
scanInterval: ScanInterval.DAILY,
|
||||
scanOnStartup: false,
|
||||
scanPdf: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
216
next-ui/src/components/library/form/StepScanner.vue
Normal file
216
next-ui/src/components/library/form/StepScanner.vue
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<template>
|
||||
<v-container class="px-0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="model.scanInterval"
|
||||
:items="scanIntervalOptions"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - scan interval',
|
||||
defaultMessage: 'Scan interval',
|
||||
id: 'DwDa04',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="model.oneshotsDirectory"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - one-shots directory',
|
||||
defaultMessage: 'One-shots directory',
|
||||
id: '6OXY1N',
|
||||
})
|
||||
"
|
||||
:hint="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - one-shots directory - hint',
|
||||
defaultMessage: 'Leave empty to disable',
|
||||
id: 't5ZhnZ',
|
||||
})
|
||||
"
|
||||
persistent-hint
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-combobox
|
||||
v-model="model.scanDirectoryExclusions"
|
||||
multiple
|
||||
clearable
|
||||
chips
|
||||
closable-chips
|
||||
hide-details
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - directory exclusions',
|
||||
defaultMessage: 'Directory exclusions',
|
||||
id: 'bN2VbA',
|
||||
})
|
||||
"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1">
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - file types selection header',
|
||||
defaultMessage: 'Scan for these file types',
|
||||
id: 'K+fQO2',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<v-chip-group
|
||||
:model-value="scanTypes"
|
||||
multiple
|
||||
column
|
||||
@update:model-value="updateScanTypes"
|
||||
>
|
||||
<v-chip
|
||||
v-for="type in fileTypesOptions"
|
||||
:key="type.value"
|
||||
v-bind="type"
|
||||
filter
|
||||
rounded
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-chip-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-checkbox
|
||||
v-model="model.emptyTrashAfterScan"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - empty trash automatically',
|
||||
defaultMessage: 'Empty trash automatically after every scan',
|
||||
id: 'GySX8C',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.scanForceModifiedTime"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - Force directory modified time',
|
||||
defaultMessage: 'Force directory modified time',
|
||||
id: 'Xbf2fj',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-tooltip:bottom="
|
||||
$formatMessage({
|
||||
description:
|
||||
'Form add/edit library: Scanner - Force directory modified time - information tooltip',
|
||||
defaultMessage: 'You should enable this if the library is hosted on Google Drive',
|
||||
id: 'GyRV+/',
|
||||
})
|
||||
"
|
||||
icon="i-mdi:information-outline"
|
||||
></v-icon>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
|
||||
<v-checkbox
|
||||
v-model="model.scanOnStartup"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Form add/edit library: Scanner - scan on startup',
|
||||
defaultMessage: 'Scan on application startup',
|
||||
id: 'TUxJCd',
|
||||
})
|
||||
"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ScanInterval, scanIntervalMessages } from '@/types/ScanInterval'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
type LibraryCreationScanner = Pick<
|
||||
components['schemas']['LibraryCreationDto'],
|
||||
| 'scanInterval'
|
||||
| 'oneshotsDirectory'
|
||||
| 'scanDirectoryExclusions'
|
||||
| 'scanCbx'
|
||||
| 'scanEpub'
|
||||
| 'scanPdf'
|
||||
| 'emptyTrashAfterScan'
|
||||
| 'scanForceModifiedTime'
|
||||
| 'scanOnStartup'
|
||||
>
|
||||
|
||||
const model = defineModel<LibraryCreationScanner>({ required: true })
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const scanIntervalOptions = Object.values(ScanInterval).map((x) => ({
|
||||
title: intl.formatMessage(scanIntervalMessages[x]),
|
||||
value: x,
|
||||
}))
|
||||
|
||||
const scanTypes = computed(() => {
|
||||
const r = []
|
||||
if (model.value.scanCbx) r.push('cbx')
|
||||
if (model.value.scanPdf) r.push('pdf')
|
||||
if (model.value.scanEpub) r.push('epub')
|
||||
return r
|
||||
})
|
||||
|
||||
function updateScanTypes(val: string[]) {
|
||||
model.value.scanCbx = val.includes('cbx')
|
||||
model.value.scanPdf = val.includes('pdf')
|
||||
model.value.scanEpub = val.includes('epub')
|
||||
}
|
||||
|
||||
const fileTypesOptions = [
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
description: 'Form add/edit library: Scanner - file types: comic book archives',
|
||||
defaultMessage: 'Comic Book archives',
|
||||
id: 'iu31A4',
|
||||
}),
|
||||
value: 'cbx',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
description: 'Form add/edit library: Scanner - file types: pdf',
|
||||
defaultMessage: 'PDF',
|
||||
id: 'EOrAHc',
|
||||
}),
|
||||
value: 'pdf',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
description: 'Form add/edit library: Scanner - file types: epub',
|
||||
defaultMessage: 'Epub',
|
||||
id: '5WMvLb',
|
||||
}),
|
||||
value: 'epub',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { response400BadRequest, response404NotFound } from '@/mocks/api/handlers'
|
||||
|
||||
export const libraries = [
|
||||
{
|
||||
|
|
@ -69,4 +70,30 @@ export const libraries = [
|
|||
|
||||
export const librariesHandlers = [
|
||||
httpTyped.get('/api/v1/libraries', ({ response }) => response(200).json(libraries)),
|
||||
httpTyped.post('/api/v1/libraries', async ({ request, response }) => {
|
||||
const body = await request.json()
|
||||
|
||||
if (libraries.some((it) => it.id === body.name)) {
|
||||
return response.untyped(response400BadRequest())
|
||||
}
|
||||
|
||||
const lib = Object.assign({}, body, { unavailable: false, id: body.name })
|
||||
libraries.push(lib)
|
||||
|
||||
return response(200).json(lib)
|
||||
}),
|
||||
httpTyped.patch('/api/v1/libraries/{libraryId}', async ({ request, params, response }) => {
|
||||
const body = await request.json()
|
||||
const libraryId = params['libraryId']
|
||||
|
||||
const existing = libraries.find((it) => it.id === libraryId)
|
||||
|
||||
if (!existing) {
|
||||
return response.untyped(response404NotFound())
|
||||
}
|
||||
|
||||
libraries[libraries.indexOf(existing)] = Object.assign({}, existing, body)
|
||||
|
||||
return response(204).empty()
|
||||
}),
|
||||
]
|
||||
|
|
|
|||
36
next-ui/src/modules/libraries.ts
Normal file
36
next-ui/src/modules/libraries.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { ScanInterval } from '@/types/ScanInterval'
|
||||
import { SeriesCover } from '@/types/SeriesCover'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
export function getLibraryDefaults(): components['schemas']['LibraryCreationDto'] {
|
||||
return {
|
||||
analyzeDimensions: true,
|
||||
convertToCbz: false,
|
||||
emptyTrashAfterScan: false,
|
||||
hashFiles: true,
|
||||
hashKoreader: false,
|
||||
hashPages: false,
|
||||
importBarcodeIsbn: false,
|
||||
importComicInfoBook: true,
|
||||
importComicInfoCollection: true,
|
||||
importComicInfoReadList: true,
|
||||
importComicInfoSeries: true,
|
||||
importComicInfoSeriesAppendVolume: true,
|
||||
importEpubBook: true,
|
||||
importEpubSeries: true,
|
||||
importLocalArtwork: true,
|
||||
importMylarSeries: true,
|
||||
name: '',
|
||||
oneshotsDirectory: '_oneshots',
|
||||
repairExtensions: false,
|
||||
root: '',
|
||||
scanCbx: true,
|
||||
scanDirectoryExclusions: ['#recycle', '@eaDir', '@Recycle'],
|
||||
scanEpub: true,
|
||||
scanForceModifiedTime: false,
|
||||
scanInterval: ScanInterval.EVERY_6H,
|
||||
scanOnStartup: false,
|
||||
scanPdf: true,
|
||||
seriesCover: SeriesCover.FIRST,
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,8 @@ import { md3 } from 'vuetify/blueprints'
|
|||
|
||||
// Labs
|
||||
import { VFileUpload } from 'vuetify/labs/VFileUpload'
|
||||
import { VIconBtn } from 'vuetify/labs/components'
|
||||
import { VIconBtn } from 'vuetify/labs/VIconBtn'
|
||||
import { VStepperVertical, VStepperVerticalItem } from 'vuetify/labs/VStepperVertical'
|
||||
import { createRulesPlugin } from 'vuetify/labs/rules'
|
||||
|
||||
import { availableLocales, currentLocale, fallbackLocale } from '@/utils/i18n/locale-helper'
|
||||
|
|
@ -72,6 +73,8 @@ export const vuetify = createVuetify({
|
|||
components: {
|
||||
VFileUpload,
|
||||
VIconBtn,
|
||||
VStepperVertical,
|
||||
VStepperVerticalItem,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
43
next-ui/src/types/ScanInterval.ts
Normal file
43
next-ui/src/types/ScanInterval.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { defineMessages } from 'vue-intl'
|
||||
|
||||
export enum ScanInterval {
|
||||
DISABLED = 'DISABLED',
|
||||
HOURLY = 'HOURLY',
|
||||
EVERY_6H = 'EVERY_6H',
|
||||
EVERY_12H = 'EVERY_12H',
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
}
|
||||
|
||||
export const scanIntervalMessages = defineMessages({
|
||||
[ScanInterval.DISABLED]: {
|
||||
description: 'Scan interval: DISABLED',
|
||||
defaultMessage: 'Disabled',
|
||||
id: '8M9T3g',
|
||||
},
|
||||
[ScanInterval.HOURLY]: {
|
||||
description: 'Scan interval: HOURLY',
|
||||
defaultMessage: 'Hourly',
|
||||
id: 'rBFh/c',
|
||||
},
|
||||
[ScanInterval.EVERY_6H]: {
|
||||
description: 'Scan interval: EVERY_6H',
|
||||
defaultMessage: 'Every 6 hours',
|
||||
id: '4d2F5w',
|
||||
},
|
||||
[ScanInterval.EVERY_12H]: {
|
||||
description: 'Scan interval: EVERY_12H',
|
||||
defaultMessage: 'Every 12 hours',
|
||||
id: '5yu0g9',
|
||||
},
|
||||
[ScanInterval.DAILY]: {
|
||||
description: 'Scan interval: DAILY',
|
||||
defaultMessage: 'Daily',
|
||||
id: 'qLk+cl',
|
||||
},
|
||||
[ScanInterval.WEEKLY]: {
|
||||
description: 'Scan interval: WEEKLY',
|
||||
defaultMessage: 'Weekly',
|
||||
id: 'T6pXCK',
|
||||
},
|
||||
})
|
||||
31
next-ui/src/types/SeriesCover.ts
Normal file
31
next-ui/src/types/SeriesCover.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { defineMessages } from 'vue-intl'
|
||||
|
||||
export enum SeriesCover {
|
||||
FIRST = 'FIRST',
|
||||
FIRST_UNREAD_OR_FIRST = 'FIRST_UNREAD_OR_FIRST',
|
||||
FIRST_UNREAD_OR_LAST = 'FIRST_UNREAD_OR_LAST',
|
||||
LAST = 'LAST',
|
||||
}
|
||||
|
||||
export const seriesCoverMessages = defineMessages({
|
||||
[SeriesCover.FIRST]: {
|
||||
description: 'Series cover: FIRST',
|
||||
defaultMessage: 'First',
|
||||
id: 'j7cvLm',
|
||||
},
|
||||
[SeriesCover.FIRST_UNREAD_OR_FIRST]: {
|
||||
description: 'Series cover: FIRST_UNREAD_OR_FIRST',
|
||||
defaultMessage: 'First unread, else first',
|
||||
id: 'woVEgl',
|
||||
},
|
||||
[SeriesCover.FIRST_UNREAD_OR_LAST]: {
|
||||
description: 'Series cover: FIRST_UNREAD_OR_LAST',
|
||||
defaultMessage: 'First unread, else last',
|
||||
id: 'kLu/vI',
|
||||
},
|
||||
[SeriesCover.LAST]: {
|
||||
description: 'Series cover: LAST',
|
||||
defaultMessage: 'Last',
|
||||
id: 'pkqPAO',
|
||||
},
|
||||
})
|
||||
|
|
@ -31,4 +31,9 @@ export const commonMessages = {
|
|||
defaultMessage: 'Change Password',
|
||||
id: 'dHyAgE',
|
||||
}),
|
||||
resourceIntensive: defineMessage({
|
||||
description: 'Resource intensive analysis warning',
|
||||
defaultMessage: 'Can consume lots of resources on large libraries or slow hardware',
|
||||
id: 'uoc99F',
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue