mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
libraries/library menus
This commit is contained in:
parent
7c205ebc2f
commit
6a73408c3c
18 changed files with 698 additions and 26 deletions
|
|
@ -90,3 +90,75 @@ export const useUpdateLibrary = defineMutation(() => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const useDeleteLibrary = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: (libraryId: string) =>
|
||||
komgaClient.DELETE('/api/v1/libraries/{libraryId}', {
|
||||
params: {
|
||||
path: {
|
||||
libraryId: libraryId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryCache.invalidateQueries({ key: QUERY_KEYS_LIBRARIES.root })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const useRefreshMetadataLibrary = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (libraryId: string) =>
|
||||
komgaClient.POST('/api/v1/libraries/{libraryId}/metadata/refresh', {
|
||||
params: {
|
||||
path: {
|
||||
libraryId: libraryId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useEmptyTrashLibrary = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (libraryId: string) =>
|
||||
komgaClient.POST('/api/v1/libraries/{libraryId}/empty-trash', {
|
||||
params: {
|
||||
path: {
|
||||
libraryId: libraryId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useScanLibrary = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: ({ libraryId, deep = false }: { libraryId: string; deep?: boolean }) =>
|
||||
komgaClient.POST('/api/v1/libraries/{libraryId}/scan', {
|
||||
params: {
|
||||
path: {
|
||||
libraryId: libraryId,
|
||||
},
|
||||
query: {
|
||||
deep: deep,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useAnalyzeLibrary = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (libraryId: string) =>
|
||||
komgaClient.POST('/api/v1/libraries/{libraryId}/analyze', {
|
||||
params: {
|
||||
path: {
|
||||
libraryId: libraryId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
1
next-ui/src/components.d.ts
vendored
1
next-ui/src/components.d.ts
vendored
|
|
@ -54,6 +54,7 @@ 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']
|
||||
LibraryDeletionWarning: typeof import('./components/library/DeletionWarning.vue')['default']
|
||||
LibraryFormCreateEdit: typeof import('./components/library/form/CreateEdit.vue')['default']
|
||||
LibraryFormStepGeneral: typeof import('./components/library/form/StepGeneral.vue')['default']
|
||||
LibraryFormStepMetadata: typeof import('./components/library/form/StepMetadata.vue')['default']
|
||||
|
|
|
|||
14
next-ui/src/components/dialog/Confirm.mdx
Normal file
14
next-ui/src/components/dialog/Confirm.mdx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
import * as Stories from './Confirm.stories';
|
||||
|
||||
<Meta of={Stories} />
|
||||
|
||||
# DialogConfirm
|
||||
|
||||
A configurable confirmation dialog.
|
||||
|
||||
Confirmation can be one of:
|
||||
- click the confirm button
|
||||
- check a checkbox, then click the confirm button
|
||||
- type a validation text, then click the confirm button
|
||||
57
next-ui/src/components/dialog/Confirm.stories.ts
Normal file
57
next-ui/src/components/dialog/Confirm.stories.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Confirm from './Confirm.vue'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta = {
|
||||
component: Confirm,
|
||||
render: (args: object) => ({
|
||||
components: { Confirm },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<Confirm v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {
|
||||
dialog: true,
|
||||
onConfirm: fn(),
|
||||
},
|
||||
} satisfies Meta<typeof Confirm>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Click: Story = {
|
||||
args: {
|
||||
...meta.args,
|
||||
mode: 'click',
|
||||
},
|
||||
}
|
||||
|
||||
export const Checkbox: Story = {
|
||||
args: {
|
||||
...meta.args,
|
||||
mode: 'checkbox',
|
||||
color: 'warning',
|
||||
},
|
||||
}
|
||||
|
||||
export const TextInput: Story = {
|
||||
args: {
|
||||
...meta.args,
|
||||
mode: 'textinput',
|
||||
},
|
||||
}
|
||||
|
||||
export const TextInputCustomText: Story = {
|
||||
args: {
|
||||
...meta.args,
|
||||
title: 'dialog title',
|
||||
subtitle: 'dialog subtitle',
|
||||
validateText: "let's go",
|
||||
mode: 'textinput',
|
||||
},
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
<slot name="warning" />
|
||||
<slot name="text">
|
||||
<FormattedMessage
|
||||
v-if="mode === 'textinput'"
|
||||
:message-descriptor="
|
||||
defineMessage({
|
||||
description: 'Confirmation dialog: default hint to retype validation text',
|
||||
|
|
@ -31,7 +32,9 @@
|
|||
id: 'XnidLu',
|
||||
})
|
||||
"
|
||||
:values="{ validateText: validateText }"
|
||||
:values="{
|
||||
validateText: validateTextEffective,
|
||||
}"
|
||||
>
|
||||
<template #b="Content">
|
||||
<span class="font-weight-bold">
|
||||
|
|
@ -42,11 +45,26 @@
|
|||
</slot>
|
||||
|
||||
<v-text-field
|
||||
:rules="[['sameAsIgnoreCase', validateText]]"
|
||||
v-if="mode === 'textinput'"
|
||||
:rules="[['sameAsIgnoreCase', validateTextEffective]]"
|
||||
hide-details
|
||||
class="mt-2"
|
||||
autofocus
|
||||
/>
|
||||
<v-checkbox
|
||||
v-if="mode === 'checkbox'"
|
||||
:rules="['required']"
|
||||
hide-details
|
||||
:color="colorEffective"
|
||||
:label="
|
||||
checkboxLabel ||
|
||||
$formatMessage({
|
||||
description: 'Confirmation dialog: default checkbox label',
|
||||
defaultMessage: 'Click to confirm',
|
||||
id: '3rNj7/',
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
|
@ -63,19 +81,19 @@
|
|||
/>
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
:disabled="!formValid"
|
||||
:disabled="mode !== 'click' && !formValid"
|
||||
:text="
|
||||
okText ||
|
||||
$formatMessage({
|
||||
description: 'Confirmation dialog: Confirm button',
|
||||
description: 'Confirmation dialog: default confirm button',
|
||||
defaultMessage: 'Confirm',
|
||||
id: '33t+CB',
|
||||
id: 'ddthL2',
|
||||
})
|
||||
"
|
||||
type="submit"
|
||||
variant="elevated"
|
||||
rounded="xs"
|
||||
color="error"
|
||||
:color="colorEffective"
|
||||
/>
|
||||
</template>
|
||||
</v-card>
|
||||
|
|
@ -85,9 +103,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineMessage } from 'vue-intl'
|
||||
import { defineMessage, useIntl } from 'vue-intl'
|
||||
import type { DialogConfirmProps } from '@/types/dialog'
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const showDialog = defineModel<boolean>('dialog', { required: false })
|
||||
const emit = defineEmits<{
|
||||
confirm: []
|
||||
|
|
@ -104,11 +124,26 @@ async function submitForm(isActive: Ref<boolean, boolean>) {
|
|||
}
|
||||
}
|
||||
|
||||
const validateTextEffective = computed(
|
||||
() =>
|
||||
validateText ||
|
||||
intl.formatMessage({
|
||||
description: 'Confirmation dialog: default validation text',
|
||||
defaultMessage: 'confirm',
|
||||
id: 'j7CGMQ',
|
||||
}),
|
||||
)
|
||||
|
||||
const colorEffective = computed(() => color || 'error')
|
||||
|
||||
const {
|
||||
title = undefined,
|
||||
subtitle = undefined,
|
||||
okText = undefined,
|
||||
validateText = 'confirm',
|
||||
validateText = undefined,
|
||||
checkboxLabel = undefined,
|
||||
mode = 'click',
|
||||
color = undefined,
|
||||
maxWidth = undefined,
|
||||
activator = undefined,
|
||||
loading = false,
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|||
import Drawer from './Drawer.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
|
||||
import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
|
||||
import SnackQueue from '@/components/SnackQueue.vue'
|
||||
|
||||
const meta = {
|
||||
component: Drawer,
|
||||
render: (args: object) => ({
|
||||
components: { Drawer, DialogConfirmEditInstance, SnackQueue },
|
||||
components: { Drawer, DialogConfirmEditInstance, DialogConfirmInstance, SnackQueue },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<Drawer /><DialogConfirmEditInstance/><SnackQueue/>',
|
||||
template: '<Drawer /><DialogConfirmEditInstance/><DialogConfirmInstance/><SnackQueue/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/Clie
|
|||
import type { components } from '@/generated/openapi/komga'
|
||||
import { VList } from 'vuetify/components'
|
||||
import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
|
||||
import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
|
||||
import SnackQueue from '@/components/SnackQueue.vue'
|
||||
import { delay, http } from 'msw'
|
||||
import { response401Unauthorized } from '@/mocks/api/handlers'
|
||||
|
|
@ -15,11 +16,12 @@ import { response401Unauthorized } from '@/mocks/api/handlers'
|
|||
const meta = {
|
||||
component: Libraries,
|
||||
render: (args: object) => ({
|
||||
components: { Libraries, VList, DialogConfirmEditInstance, SnackQueue },
|
||||
components: { Libraries, VList, DialogConfirmEditInstance, DialogConfirmInstance, SnackQueue },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<v-list nav><Libraries /></v-list><DialogConfirmEditInstance/><SnackQueue/>',
|
||||
template:
|
||||
'<v-list nav><Libraries /></v-list><DialogConfirmEditInstance/><DialogConfirmInstance/><SnackQueue/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@
|
|||
id: '90yqRq',
|
||||
})
|
||||
"
|
||||
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
|
||||
@mouseenter="
|
||||
(event: Event) => (dialogConfirmEdit.activator = event.currentTarget as Element)
|
||||
"
|
||||
@click="createLibrary"
|
||||
/>
|
||||
<v-icon-btn
|
||||
|
|
|
|||
11
next-ui/src/components/library/DeletionWarning.mdx
Normal file
11
next-ui/src/components/library/DeletionWarning.mdx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
import * as Stories from './DeletionWarning.stories';
|
||||
|
||||
<Meta of={Stories} />
|
||||
|
||||
# LibraryDeletionWarning
|
||||
|
||||
Warning shown within a confirmation dialog before deleting a particular library.
|
||||
|
||||
<Canvas of={Stories.Default} />
|
||||
25
next-ui/src/components/library/DeletionWarning.stories.ts
Normal file
25
next-ui/src/components/library/DeletionWarning.stories.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import DeletionWarning from './DeletionWarning.vue'
|
||||
|
||||
const meta = {
|
||||
component: DeletionWarning,
|
||||
render: (args: object) => ({
|
||||
components: { DeletionWarning },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<DeletionWarning />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof DeletionWarning>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
36
next-ui/src/components/library/DeletionWarning.vue
Normal file
36
next-ui/src/components/library/DeletionWarning.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<v-alert
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<FormattedMessage :message-descriptor="message">
|
||||
<template #ul="Content">
|
||||
<ul class="ps-8">
|
||||
<component :is="Content" />
|
||||
</ul>
|
||||
</template>
|
||||
<template #li="Content">
|
||||
<li>
|
||||
<component :is="Content" />
|
||||
</li>
|
||||
</template>
|
||||
<template #b="Content">
|
||||
<div class="font-weight-bold mt-4">
|
||||
<component :is="Content" />
|
||||
</div>
|
||||
</template>
|
||||
</FormattedMessage>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineMessage } from 'vue-intl'
|
||||
|
||||
const message = defineMessage({
|
||||
description: 'Library deletion warning notice',
|
||||
defaultMessage:
|
||||
'The library will be deleted from this server.<ul><li>Series and books will be deleted, along with their metadata and read progress.</li><li>Media files will not be deleted from disk.</li></ul><b>This action cannot be undone.</b>',
|
||||
id: 'bk1tX8',
|
||||
})
|
||||
</script>
|
||||
|
|
@ -16,6 +16,11 @@
|
|||
<script setup lang="ts">
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useEmptyTrashLibrary, useLibraries, useScanLibrary } from '@/colada/libraries'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
import { useDisplay } from 'vuetify/framework'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
|
||||
const { activatorId } = defineProps<{
|
||||
activatorId: string
|
||||
|
|
@ -33,7 +38,61 @@ const actions = [
|
|||
}),
|
||||
onClick: () => (appStore.reorderLibraries = true),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Libraries menu: scan',
|
||||
defaultMessage: 'Scan all libraries',
|
||||
id: 'CY8sfH',
|
||||
}),
|
||||
onClick: () => scanAll(),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Libraries menu: empty trash',
|
||||
defaultMessage: 'Empty trash for all libraries',
|
||||
id: 'CwteMk',
|
||||
}),
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirm.value.activator = event.currentTarget as Element),
|
||||
onClick: () => emptyTrashAll(),
|
||||
},
|
||||
]
|
||||
|
||||
const { confirm: dialogConfirm } = storeToRefs(useDialogsStore())
|
||||
const display = useDisplay()
|
||||
|
||||
const { data: libraries } = useLibraries()
|
||||
|
||||
//region Scan
|
||||
const { mutate: mutateScan } = useScanLibrary()
|
||||
|
||||
function scanAll() {
|
||||
libraries.value?.forEach((it) => mutateScan({ libraryId: it.id }))
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Empty Trash
|
||||
const { mutate: mutateEmptyTrash } = useEmptyTrashLibrary()
|
||||
|
||||
function emptyTrashAll() {
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: intl.formatMessage(commonMessages.dialogEmptyTrashTitle),
|
||||
maxWidth: 600,
|
||||
mode: 'click',
|
||||
color: 'primary',
|
||||
okText: intl.formatMessage(commonMessages.dialogEmptyTrashConfirm),
|
||||
closeOnSave: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(h('div', intl.formatMessage(commonMessages.dialogEmptyTrashNotice))),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.callback = () => {
|
||||
libraries.value?.forEach((it) => mutateEmptyTrash(it.id))
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
|
|
|||
|
|
@ -29,15 +29,9 @@
|
|||
>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Library menu: manage > edit',
|
||||
defaultMessage: 'Edit',
|
||||
id: 'n4w2CE',
|
||||
})
|
||||
"
|
||||
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
|
||||
@click="updateLibrary()"
|
||||
v-for="(action, i) in manageActions"
|
||||
:key="i"
|
||||
v-bind="action"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
|
@ -50,13 +44,21 @@
|
|||
import { useIntl } from 'vue-intl'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
import { useUpdateLibrary } from '@/colada/libraries'
|
||||
import {
|
||||
useAnalyzeLibrary,
|
||||
useDeleteLibrary,
|
||||
useEmptyTrashLibrary,
|
||||
useRefreshMetadataLibrary,
|
||||
useScanLibrary,
|
||||
useUpdateLibrary,
|
||||
} from '@/colada/libraries'
|
||||
import { useMessagesStore } from '@/stores/messages'
|
||||
import CreateEdit from '@/components/library/form/CreateEdit.vue'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import type { ErrorCause } from '@/api/komga-client'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
import LibraryDeletionWarning from '@/components/library/DeletionWarning.vue'
|
||||
|
||||
const { activatorId, library } = defineProps<{
|
||||
activatorId: string
|
||||
|
|
@ -64,11 +66,11 @@ const { activatorId, library } = defineProps<{
|
|||
}>()
|
||||
|
||||
const intl = useIntl()
|
||||
const { confirmEdit: dialogConfirmEdit } = storeToRefs(useDialogsStore())
|
||||
const { confirmEdit: dialogConfirmEdit, confirm: dialogConfirm } = storeToRefs(useDialogsStore())
|
||||
const messagesStore = useMessagesStore()
|
||||
const display = useDisplay()
|
||||
|
||||
//region actions
|
||||
//region Actions
|
||||
const actions = [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
|
|
@ -76,7 +78,70 @@ const actions = [
|
|||
defaultMessage: 'Scan library files',
|
||||
id: 'GCwZB2',
|
||||
}),
|
||||
// onClick: () => (appStore.reorderLibraries = true),
|
||||
onClick: () => scanLibrary(),
|
||||
},
|
||||
]
|
||||
|
||||
const manageActions = [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Library menu: manage > edit',
|
||||
defaultMessage: 'Edit',
|
||||
id: 'n4w2CE',
|
||||
}),
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirmEdit.value.activator = event.currentTarget as Element),
|
||||
onClick: () => updateLibrary(),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Library menu: manage > deep scan',
|
||||
defaultMessage: 'Deep scan',
|
||||
id: 'foSDIW',
|
||||
}),
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirm.value.activator = event.currentTarget as Element),
|
||||
onClick: () => scanDeep(),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Library menu: manage > refresh all metadata',
|
||||
defaultMessage: 'Refresh all metadata',
|
||||
id: 'OUyleX',
|
||||
}),
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirm.value.activator = event.currentTarget as Element),
|
||||
onClick: () => refreshMetadata(),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Library menu: manage > empty trash',
|
||||
defaultMessage: 'Empty trash',
|
||||
id: 'sdNz1F',
|
||||
}),
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirm.value.activator = event.currentTarget as Element),
|
||||
onClick: () => emptyTrash(),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Library menu: manage > analyze',
|
||||
defaultMessage: 'Analyze',
|
||||
id: 'E5ZMyt',
|
||||
}),
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirm.value.activator = event.currentTarget as Element),
|
||||
onClick: () => analyzeLibrary(),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Library menu: manage > delete',
|
||||
defaultMessage: 'Delete',
|
||||
id: 'LFf8QB',
|
||||
}),
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirm.value.activator = event.currentTarget as Element),
|
||||
onClick: () => deleteLibrary(),
|
||||
},
|
||||
]
|
||||
//endregion
|
||||
|
|
@ -138,6 +203,206 @@ function updateLibrary() {
|
|||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Refresh Metadata
|
||||
const { mutate: mutateRefreshMetadata } = useRefreshMetadataLibrary()
|
||||
|
||||
function refreshMetadata() {
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: intl.formatMessage({
|
||||
description: 'Library refresh metadata dialog: title',
|
||||
defaultMessage: 'Refresh all metadata',
|
||||
id: 'xiyw1J',
|
||||
}),
|
||||
subtitle: library.name,
|
||||
maxWidth: 600,
|
||||
mode: 'click',
|
||||
color: 'primary',
|
||||
okText: intl.formatMessage({
|
||||
description: 'Library refresh metadata dialog: confirm button',
|
||||
defaultMessage: 'Refresh',
|
||||
id: 'i+kSy9',
|
||||
}),
|
||||
closeOnSave: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(
|
||||
h(
|
||||
'div',
|
||||
intl.formatMessage({
|
||||
description: 'Library refresh metadata dialog: warning text',
|
||||
defaultMessage:
|
||||
'Refreshes metadata for all the media files in the library. Depending on your library size, this may take a long time.',
|
||||
id: 'vs88Ef',
|
||||
}),
|
||||
),
|
||||
),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.callback = () => mutateRefreshMetadata(library.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Empty Trash
|
||||
const { mutate: mutateEmptyTrash } = useEmptyTrashLibrary()
|
||||
|
||||
function emptyTrash() {
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: intl.formatMessage(commonMessages.dialogEmptyTrashTitle),
|
||||
subtitle: library.name,
|
||||
maxWidth: 600,
|
||||
mode: 'click',
|
||||
color: 'primary',
|
||||
okText: intl.formatMessage(commonMessages.dialogEmptyTrashConfirm),
|
||||
closeOnSave: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(h('div', intl.formatMessage(commonMessages.dialogEmptyTrashNotice))),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.callback = () => mutateEmptyTrash(library.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Analyze
|
||||
const { mutate: mutateAnalyze } = useAnalyzeLibrary()
|
||||
|
||||
function analyzeLibrary() {
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: intl.formatMessage({
|
||||
description: 'Library analyze dialog: title',
|
||||
defaultMessage: 'Analyze library',
|
||||
id: '5JGOUU',
|
||||
}),
|
||||
subtitle: library.name,
|
||||
maxWidth: 600,
|
||||
mode: 'click',
|
||||
color: 'primary',
|
||||
okText: intl.formatMessage({
|
||||
description: 'Library analyze dialog: confirm button',
|
||||
defaultMessage: 'Analyze',
|
||||
id: 'jN3N1Q',
|
||||
}),
|
||||
closeOnSave: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(
|
||||
h(
|
||||
'div',
|
||||
intl.formatMessage({
|
||||
description: 'Library empty trash dialog: warning text',
|
||||
defaultMessage:
|
||||
'Analyzes all the media files in the library. The analysis captures information about the media. Depending on your library size, this may take a long time.',
|
||||
id: '8xonXJ',
|
||||
}),
|
||||
),
|
||||
),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.callback = () => mutateAnalyze(library.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Scan
|
||||
const { mutate: mutateScan } = useScanLibrary()
|
||||
|
||||
function scanLibrary() {
|
||||
mutateScan({ libraryId: library.id })
|
||||
}
|
||||
|
||||
function scanDeep() {
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: intl.formatMessage({
|
||||
description: 'Library deep scan dialog: title',
|
||||
defaultMessage: 'Deep scan',
|
||||
id: 'hV3EW+',
|
||||
}),
|
||||
subtitle: library.name,
|
||||
maxWidth: 600,
|
||||
mode: 'click',
|
||||
color: 'primary',
|
||||
okText: intl.formatMessage({
|
||||
description: 'Library deep scan: confirm button',
|
||||
defaultMessage: 'Deep scan',
|
||||
id: 'OxqfKF',
|
||||
}),
|
||||
closeOnSave: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(
|
||||
h(
|
||||
'div',
|
||||
intl.formatMessage({
|
||||
description: 'Library deep scan dialog: warning text',
|
||||
defaultMessage:
|
||||
'Performs a deep scan of the library files. Depending on your library size, this may take a long time.',
|
||||
id: 'y3nPgO',
|
||||
}),
|
||||
),
|
||||
),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.callback = () => mutateScan({ libraryId: library.id, deep: true })
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Delete
|
||||
const { mutateAsync: mutateDelete } = useDeleteLibrary()
|
||||
|
||||
function deleteLibrary() {
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: intl.formatMessage({
|
||||
description: 'Library delete dialog: title',
|
||||
defaultMessage: 'Delete library',
|
||||
id: '3T1ln7',
|
||||
}),
|
||||
subtitle: library.name,
|
||||
maxWidth: 600,
|
||||
mode: 'textinput',
|
||||
color: 'error',
|
||||
validateText: library.name,
|
||||
okText: intl.formatMessage({
|
||||
description: 'Library delete dialog: confirm button',
|
||||
defaultMessage: 'Delete',
|
||||
id: '/5Gb4y',
|
||||
}),
|
||||
closeOnSave: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(LibraryDeletionWarning),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.callback = () => {
|
||||
mutateDelete(library.id)
|
||||
.then(() => {
|
||||
messagesStore.messages.push({
|
||||
text: intl.formatMessage(
|
||||
{
|
||||
description: 'Snackbar notification shown upon successful library deletion',
|
||||
defaultMessage: 'Library deleted: {library}',
|
||||
id: 'PvKF7E',
|
||||
},
|
||||
{
|
||||
library: library.name,
|
||||
},
|
||||
),
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message ||
|
||||
intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
|
|
|||
|
|
@ -96,4 +96,25 @@ export const librariesHandlers = [
|
|||
|
||||
return response(204).empty()
|
||||
}),
|
||||
httpTyped.delete('/api/v1/libraries/{libraryId}', ({ params, response }) => {
|
||||
const libraryId = params['libraryId']
|
||||
|
||||
const existing = libraries.find((it) => it.id === libraryId)
|
||||
|
||||
if (!existing) {
|
||||
return response.untyped(response404NotFound())
|
||||
}
|
||||
|
||||
libraries.splice(libraries.indexOf(existing), 1)
|
||||
|
||||
return response(204).empty()
|
||||
}),
|
||||
httpTyped.post('/api/v1/libraries/{libraryId}/metadata/refresh', ({ response }) =>
|
||||
response(202).empty(),
|
||||
),
|
||||
httpTyped.post('/api/v1/libraries/{libraryId}/analyze', ({ response }) => response(202).empty()),
|
||||
httpTyped.post('/api/v1/libraries/{libraryId}/empty-trash', ({ response }) =>
|
||||
response(202).empty(),
|
||||
),
|
||||
httpTyped.post('/api/v1/libraries/{libraryId}/scan', ({ response }) => response(202).empty()),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ function showDialog(action: ACTION, apiKey?: components['schemas']['ApiKeyDto'])
|
|||
subtitle: apiKey?.comment,
|
||||
maxWidth: 600,
|
||||
validateText: apiKey?.comment,
|
||||
mode: 'textinput',
|
||||
okText: intl.formatMessage({
|
||||
description: 'Delete API Key dialog: confirmation button text',
|
||||
defaultMessage: 'Delete',
|
||||
|
|
@ -103,6 +104,7 @@ function showDialog(action: ACTION, apiKey?: components['schemas']['ApiKeyDto'])
|
|||
subtitle: apiKey?.comment,
|
||||
maxWidth: 600,
|
||||
validateText: apiKey?.comment,
|
||||
mode: 'textinput',
|
||||
okText: intl.formatMessage({
|
||||
description: 'Force Sync API Key dialog: confirmation button text',
|
||||
defaultMessage: 'I understand',
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
|
|||
subtitle: user?.email,
|
||||
maxWidth: 600,
|
||||
validateText: user?.email,
|
||||
mode: 'textinput',
|
||||
okText: intl.formatMessage({
|
||||
description: 'Delete user dialog: confirmation button text',
|
||||
defaultMessage: 'Delete',
|
||||
|
|
|
|||
|
|
@ -1,25 +1,77 @@
|
|||
type DialogBaseProps = {
|
||||
/**
|
||||
* Title of the dialog.
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* Subtitle of the dialog.
|
||||
*/
|
||||
subtitle?: string
|
||||
/**
|
||||
* Maximum width of the dialog.
|
||||
*/
|
||||
maxWidth?: string | number
|
||||
/**
|
||||
* Activator for the dialog.
|
||||
*/
|
||||
activator?: Element | string
|
||||
/**
|
||||
* Loading indicator, applies to the dialog's card.
|
||||
*/
|
||||
loading?: boolean
|
||||
/**
|
||||
* Whether the dialog should be displayed in full screen.
|
||||
*/
|
||||
fullscreen?: boolean
|
||||
/**
|
||||
* Whether the dialog is scrollable.
|
||||
*/
|
||||
scrollable?: boolean
|
||||
/**
|
||||
* Controls the dialog's visibility.
|
||||
*/
|
||||
shown?: boolean
|
||||
}
|
||||
|
||||
type DialogConfirmBaseProps = DialogBaseProps & {
|
||||
/**
|
||||
* Text for the confirmation button.
|
||||
*/
|
||||
okText?: string
|
||||
/**
|
||||
* Whether the dialog should close itself on save.
|
||||
* If disabled, the dialog should be closed inside the callback function.
|
||||
*/
|
||||
closeOnSave?: boolean
|
||||
}
|
||||
|
||||
export type DialogSimpleProps = DialogBaseProps
|
||||
|
||||
export type DialogConfirmProps = DialogConfirmBaseProps & {
|
||||
/**
|
||||
* Text that needs to be typed to validate. Only shown when `mode` is set to `textinput`.
|
||||
*/
|
||||
validateText?: string
|
||||
/**
|
||||
* Label shown next to the confirmation checkbox. Only shown when `mode` is set to `checkbox`.
|
||||
*/
|
||||
checkboxLabel?: string
|
||||
/**
|
||||
* Confirmation mechanism:
|
||||
* - `textinput`: requires typing the content of 'validateText' into a text field.
|
||||
* - `checkbox`: requires checking a checkbox.
|
||||
* - `click`: no extra validation, just click on the confirm button of the dialog.
|
||||
*/
|
||||
mode?: 'textinput' | 'checkbox' | 'click'
|
||||
/**
|
||||
* Color used for the checkbox and confirm button. Defaults to `error`.
|
||||
*/
|
||||
color?: string
|
||||
}
|
||||
|
||||
export type DialogConfirmEditProps = DialogConfirmBaseProps & {
|
||||
/**
|
||||
* CSS classes applied to the card.
|
||||
*/
|
||||
cardTextClass?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,20 @@ export const commonMessages = {
|
|||
defaultMessage: 'Can consume lots of resources on large libraries or slow hardware',
|
||||
id: 'uoc99F',
|
||||
}),
|
||||
dialogEmptyTrashTitle: defineMessage({
|
||||
description: 'Library empty trash dialog: title',
|
||||
defaultMessage: 'Empty trash',
|
||||
id: 'ELttw/',
|
||||
}),
|
||||
dialogEmptyTrashConfirm: defineMessage({
|
||||
description: 'Library empty trash dialog: confirm button',
|
||||
defaultMessage: 'Empty trash',
|
||||
id: '7M1pUf',
|
||||
}),
|
||||
dialogEmptyTrashNotice: defineMessage({
|
||||
description: 'Library empty trash dialog: warning text',
|
||||
defaultMessage:
|
||||
"By default the media server doesn't remove information for media right away. This helps if a drive is temporarily disconnected. When you empty the trash for a library, all information about missing media is deleted.",
|
||||
id: 'kDc7YL',
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue