better mobile UI

This commit is contained in:
Gauthier Roebroeck 2025-07-22 13:38:34 +08:00
parent f741de8dbc
commit 7f622accf2
14 changed files with 749 additions and 632 deletions

View file

@ -1,4 +1,4 @@
import { defineMutation, useMutation, useQueryCache } from '@pinia/colada'
import { defineMutation, useMutation } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
export const useDeleteSyncPoints = defineMutation(() => {

View file

@ -4,6 +4,7 @@
:activator="activator"
:max-width="maxWidth"
:fullscreen="fullscreen"
:transition="fullscreen ? 'dialog-bottom-transition' : undefined"
>
<template #default="{ isActive }">
<v-form

View file

@ -4,6 +4,8 @@
:activator="activator"
:max-width="maxWidth"
:fullscreen="fullscreen"
:transition="fullscreen ? 'dialog-bottom-transition' : undefined"
:scrollable="scrollable"
>
<template #default="{ isActive }">
<v-confirm-edit
@ -87,6 +89,7 @@ export interface DialogConfirmEditProps {
loading?: boolean
closeOnSave?: boolean
fullscreen?: boolean
scrollable?: boolean
}
const {
@ -97,5 +100,6 @@ const {
loading = false,
closeOnSave = true,
fullscreen = undefined,
scrollable = undefined,
} = defineProps<DialogConfirmEditProps>()
</script>

View file

@ -8,36 +8,36 @@
class="text-h4 font-weight-medium link-underline me-2"
>{{ release.version }}</a
>
<v-chip
v-if="current"
class="mx-2 mt-n3"
size="small"
rounded
color="info"
>
{{
$formatMessage({
description:
'Updates view: badge showing next to the currently installed release number',
defaultMessage: 'Currently installed',
id: '3jrAF6',
})
}}
</v-chip>
<v-chip
v-if="latest"
class="mx-2 mt-n3"
size="small"
rounded
>
{{
$formatMessage({
description: 'Updates view: badge showing next to the latest release number',
defaultMessage: 'Latest',
id: '2Bh8F2',
})
}}
</v-chip>
<span class="d-inline-flex mt-n3 ga-2 ms-2">
<v-chip
v-if="current"
size="small"
rounded
color="info"
>
{{
$formatMessage({
description:
'Updates view: badge showing next to the currently installed release number',
defaultMessage: 'Installed',
id: 'WADecv',
})
}}
</v-chip>
<v-chip
v-if="latest"
size="small"
rounded
>
{{
$formatMessage({
description: 'Updates view: badge showing next to the latest release number',
defaultMessage: 'Latest',
id: '2Bh8F2',
})
}}
</v-chip>
</span>
</div>
</template>

View file

@ -9,101 +9,110 @@
@submit.prevent="submitForm(save)"
:disabled="loading"
>
<v-list>
<v-list-subheader
:class="headerClass"
class="mb-2"
>{{
$formatMessage({
description: 'Server settings: section header for posters',
defaultMessage: 'Posters',
id: 'a5MYiP',
})
}}
</v-list-subheader>
<v-select
v-model="proxyModel.value.thumbnailSize"
:label="
$formatMessage({
description: 'Server settings: selection of poster size',
defaultMessage: 'Generated poster size',
id: 'eDA9Gm',
})
"
:items="thumbnailSizes"
/>
</v-list>
<v-row>
<v-col>
<div class="text-subtitle-2">
{{
$formatMessage({
description: 'Server settings: section header for posters',
defaultMessage: 'Posters',
id: 'a5MYiP',
})
}}
</div>
</v-col>
</v-row>
<v-row>
<v-col>
<v-select
v-model="proxyModel.value.thumbnailSize"
:label="
$formatMessage({
description: 'Server settings: selection of poster size',
defaultMessage: 'Generated poster size',
id: 'eDA9Gm',
})
"
:items="thumbnailSizes"
/>
</v-col>
</v-row>
<v-divider />
<v-divider class="mb-4" />
<v-list>
<v-list-subheader :class="headerClass"
>{{
$formatMessage({
description: 'Server settings: section header for scan behaviour',
defaultMessage: 'Overall scan behaviour',
id: 'dSlkbn',
})
}}
</v-list-subheader>
<v-checkbox
v-model="proxyModel.value.deleteEmptyCollections"
:label="
$formatMessage({
description: 'Server settings: checkbox to delete empty collections after scan',
defaultMessage: 'Delete empty collections after scan',
id: 'pHdVzh',
})
"
hide-details
/>
<v-row>
<v-col>
<div class="text-subtitle-2">
{{
$formatMessage({
description: 'Server settings: section header for scan behaviour',
defaultMessage: 'Overall scan behaviour',
id: 'dSlkbn',
})
}}
</div>
<v-checkbox
v-model="proxyModel.value.deleteEmptyCollections"
:label="
$formatMessage({
description: 'Server settings: checkbox to delete empty collections after scan',
defaultMessage: 'Delete empty collections after scan',
id: 'pHdVzh',
})
"
hide-details
/>
<v-checkbox
v-model="proxyModel.value.deleteEmptyReadLists"
:label="
$formatMessage({
description: 'Server settings: checkbox to delete empty readlists after scan',
defaultMessage: 'Delete empty read lists after scan',
id: 'kqV7EJ',
})
"
hide-details
/>
</v-col>
</v-row>
<v-checkbox
v-model="proxyModel.value.deleteEmptyReadLists"
:label="
$formatMessage({
description: 'Server settings: checkbox to delete empty readlists after scan',
defaultMessage: 'Delete empty read lists after scan',
id: 'kqV7EJ',
})
"
hide-details
/>
</v-list>
<v-divider class="mb-4" />
<v-divider />
<v-row>
<v-col>
<div class="text-subtitle-2">
{{
$formatMessage({
description: 'Server settings: section header for tasks',
defaultMessage: 'Tasks',
id: '8hC76W',
})
}}
</div>
</v-col>
</v-row>
<v-row>
<v-col>
<v-number-input
v-model="proxyModel.value.taskPoolSize"
:label="
$formatMessage({
description: 'Server settings: input field for task threads',
defaultMessage: 'Task threads',
id: 'rHwSrF',
})
"
:min="1"
:rules="['required']"
></v-number-input>
</v-col>
</v-row>
<v-list>
<v-list-subheader
:class="headerClass"
class="mb-2"
>{{
$formatMessage({
description: 'Server settings: section header for tasks',
defaultMessage: 'Tasks',
id: '8hC76W',
})
}}
</v-list-subheader>
<v-number-input
v-model="proxyModel.value.taskPoolSize"
:label="
$formatMessage({
description: 'Server settings: input field for task threads',
defaultMessage: 'Task threads',
id: 'rHwSrF',
})
"
:min="1"
:rules="['required']"
></v-number-input>
</v-list>
<v-divider class="mb-4" />
<v-divider />
<v-list>
<v-list-subheader class="mb-4">
<div :class="headerClass">
<v-row>
<v-col>
<div class="text-subtitle-2">
{{
$formatMessage({
description: 'Server settings: section header for remember me',
@ -113,38 +122,41 @@
}}
</div>
<div class="text-caption">{{ messageRequiresRestart }}</div>
</v-list-subheader>
<v-number-input
v-model="proxyModel.value.rememberMeDurationDays"
:label="
$formatMessage({
description: 'Server settings: input field for remember me duration',
defaultMessage: 'Remember me duration (in days)',
id: 'iDU5FS',
})
"
:min="1"
:rules="['required']"
></v-number-input>
</v-col>
</v-row>
<v-row>
<v-col>
<v-number-input
v-model="proxyModel.value.rememberMeDurationDays"
:label="
$formatMessage({
description: 'Server settings: input field for remember me duration',
defaultMessage: 'Remember me duration (in days)',
id: 'iDU5FS',
})
"
:min="1"
:rules="['required']"
></v-number-input>
<v-checkbox
v-model="proxyModel.value.renewRememberMeKey"
:label="
$formatMessage({
description: 'Server settings: checkbox to regenerate the remember me key',
defaultMessage: 'Regenerate the \'remember me\' key',
id: 'UaD47n',
})
"
hide-details
/>
</v-col>
</v-row>
<v-checkbox
v-model="proxyModel.value.renewRememberMeKey"
:label="
$formatMessage({
description: 'Server settings: checkbox to regenerate the remember me key',
defaultMessage: 'Regenerate the \'remember me\' key',
id: 'UaD47n',
})
"
hide-details
/>
</v-list>
<v-divider class="mb-4" />
<v-divider />
<v-list>
<v-list-subheader class="mb-4">
<div :class="headerClass">
<v-row>
<v-col>
<div class="text-subtitle-2">
{{
$formatMessage({
description: 'Server settings: section header for HTTP server',
@ -154,144 +166,159 @@
}}
</div>
<div class="text-caption">{{ messageRequiresRestart }}</div>
</v-list-subheader>
</v-col>
</v-row>
<v-number-input
v-model="proxyModel.value.serverPort"
:label="
$formatMessage({
description: 'Server settings: input field for server port',
defaultMessage: 'Server listening port',
id: '+r8FCS',
})
"
:min="1"
:max="65535"
:placeholder="settings?.serverPort.configurationSource?.toString()"
:persistent-placeholder="!!settings?.serverPort.configurationSource"
clearable
>
<template
v-slot:append-inner
v-if="!!settings?.serverPort.configurationSource"
>
<v-icon
icon="i-mdi:information-outline"
v-tooltip:bottom="messagePrecedence"
></v-icon>
</template>
</v-number-input>
<v-text-field
v-model="proxyModel.value.serverContextPath"
:label="
$formatMessage({
description: 'Server settings: input field for server base URL',
defaultMessage: 'Server base URL',
id: 'eRJOa6',
})
"
:placeholder="settings?.serverContextPath.configurationSource?.toString()"
:persistent-placeholder="!!settings?.serverContextPath.configurationSource"
clearable
:rules="[
[
'pattern',
/^\/[-a-zA-Z0-9_\/]*[a-zA-Z0-9]$/,
<v-row>
<v-col>
<v-number-input
v-model="proxyModel.value.serverPort"
:label="
$formatMessage({
description: 'Server settings: error message when server context path is invalid',
defaultMessage:
'Must start with \'/\', not end with \'/-_\', and contain only \'/-_a-z0-9\'',
id: 'Lto2Lg',
}),
],
]"
><template
v-slot:append-inner
v-if="!!settings?.serverContextPath.configurationSource"
description: 'Server settings: input field for server port',
defaultMessage: 'Server listening port',
id: '+r8FCS',
})
"
:min="1"
:max="65535"
:placeholder="settings?.serverPort.configurationSource?.toString()"
:persistent-placeholder="!!settings?.serverPort.configurationSource"
clearable
hide-details
>
<v-icon
icon="i-mdi:information-outline"
v-tooltip:bottom="messagePrecedence"
></v-icon> </template
></v-text-field>
</v-list>
<template
v-slot:append-inner
v-if="!!settings?.serverPort.configurationSource"
>
<v-icon
icon="i-mdi:information-outline"
v-tooltip:bottom="messagePrecedence"
></v-icon>
</template>
</v-number-input>
</v-col>
</v-row>
<v-divider />
<v-row>
<v-col>
<v-text-field
v-model="proxyModel.value.serverContextPath"
:label="
$formatMessage({
description: 'Server settings: input field for server base URL',
defaultMessage: 'Server base URL',
id: 'eRJOa6',
})
"
:placeholder="settings?.serverContextPath.configurationSource?.toString()"
:persistent-placeholder="!!settings?.serverContextPath.configurationSource"
clearable
:rules="[
[
'pattern',
/^\/[-a-zA-Z0-9_\/]*[a-zA-Z0-9]$/,
$formatMessage({
description:
'Server settings: error message when server context path is invalid',
defaultMessage:
'Must start with \'/\', not end with \'/-_\', and contain only \'/-_a-z0-9\'',
id: 'Lto2Lg',
}),
],
]"
><template
v-slot:append-inner
v-if="!!settings?.serverContextPath.configurationSource"
>
<v-icon
icon="i-mdi:information-outline"
v-tooltip:bottom="messagePrecedence"
></v-icon> </template
></v-text-field>
</v-col>
</v-row>
<v-list>
<v-list-subheader :class="headerClass"
>{{
$formatMessage({
description: 'Server settings: section header for Kobo Sync',
defaultMessage: 'Kobo Sync',
id: 'rRFQKU',
})
}}
</v-list-subheader>
<v-checkbox
v-model="proxyModel.value.koboProxy"
:label="
$formatMessage({
description:
'Server settings: checkbox to enable Kobo Store proxying for Kobo Sync ',
defaultMessage: 'Proxy unhandled requests to Kobo Store',
id: 'iNBto3',
})
"
hide-details
/>
<v-divider class="mb-4" />
<v-number-input
v-model="proxyModel.value.koboPort"
:label="
$formatMessage({
description: 'Server settings: input field for kobo sync port',
defaultMessage: 'Kobo Sync external port',
id: '4AKIbg',
})
"
:min="1"
:max="65535"
:hint="
$formatMessage({
description: 'Server settings: input field hint for kobo sync port',
defaultMessage: 'Set only in case of sync issues with covers and downloads',
id: 'TwB29u',
})
"
persistent-hint
clearable
></v-number-input>
</v-list>
<v-row>
<v-col>
<div class="text-subtitle-2">
{{
$formatMessage({
description: 'Server settings: section header for Kobo Sync',
defaultMessage: 'Kobo Sync',
id: 'rRFQKU',
})
}}
</div>
<v-checkbox
v-model="proxyModel.value.koboProxy"
:label="
$formatMessage({
description:
'Server settings: checkbox to enable Kobo Store proxying for Kobo Sync ',
defaultMessage: 'Proxy unhandled requests to Kobo Store',
id: 'iNBto3',
})
"
hide-details
/>
<v-number-input
v-model="proxyModel.value.koboPort"
:label="
$formatMessage({
description: 'Server settings: input field for kobo sync port',
defaultMessage: 'Kobo Sync external port',
id: '4AKIbg',
})
"
:min="1"
:max="65535"
:hint="
$formatMessage({
description: 'Server settings: input field hint for kobo sync port',
defaultMessage: 'Set only in case of sync issues with covers and downloads',
id: 'TwB29u',
})
"
persistent-hint
clearable
></v-number-input>
</v-col>
</v-row>
<v-list>
<v-btn
:text="
$formatMessage({
description: 'Server settings: button to discard any changes made',
defaultMessage: 'Discard',
id: 'kh49ZJ',
})
"
@click="cancel"
:disabled="isPristine"
variant="text"
></v-btn>
<v-btn
:text="
$formatMessage({
description: 'Server settings: button to save any changes made',
defaultMessage: 'Save changes',
id: 'FpwJlU',
})
"
type="submit"
:disabled="isPristine"
variant="text"
:loading="loading"
></v-btn>
</v-list>
<v-row>
<v-col cols="auto">
<v-btn
:text="
$formatMessage({
description: 'Server settings: button to discard any changes made',
defaultMessage: 'Discard',
id: 'kh49ZJ',
})
"
@click="cancel"
:disabled="isPristine"
variant="text"
></v-btn>
</v-col>
<v-col cols="auto">
<v-btn
:text="
$formatMessage({
description: 'Server settings: button to save any changes made',
defaultMessage: 'Save changes',
id: 'FpwJlU',
})
"
type="submit"
:disabled="isPristine"
variant="text"
:loading="loading"
></v-btn>
</v-col>
</v-row>
</v-form>
</template>
</v-confirm-edit>
@ -303,7 +330,6 @@ import { useIntl } from 'vue-intl'
import type { components } from '@/generated/openapi/komga'
const intl = useIntl()
const headerClass = 'text-subtitle1 font-weight-bold'
const { settings, loading = false } = defineProps<{
settings?: components['schemas']['SettingsDto']

View file

@ -1,13 +1,17 @@
<template>
<v-dialog
ref="dialogRef"
v-model="showDialog"
:activator="activator"
:fullscreen="fullscreen"
:transition="fullscreen ? 'dialog-bottom-transition' : undefined"
max-width="600px"
@after-leave="reset()"
>
<template #default="{ isActive }">
<v-form @submit.prevent="generateApiKey()">
<v-form
@submit.prevent="generateApiKey()"
class="fill-height"
>
<v-card
:title="
$formatMessage({
@ -150,7 +154,11 @@ const messagesStore = useMessagesStore()
const { isSupported: clipboardSupported, copy, copied } = useClipboard({ copiedDuring: 3000 })
const showDialog = defineModel<boolean>('dialog', { required: false })
const activator = defineModel<Element | string>('activator', { required: false })
const { fullscreen = undefined, activator = undefined } = defineProps<{
fullscreen?: boolean
activator?: Element | string
}>()
const comment = ref<string>('')
const creationError = ref<string>('')

View file

@ -1,129 +1,160 @@
<template>
<template v-if="!user.id">
<v-text-field
v-model="user!.email"
autofocus
:rules="['required', 'email']"
:label="
$formatMessage({
description: 'User creation dialog: Email field',
defaultMessage: 'Email',
id: 'ToD0+o',
})
"
prepend-icon="i-mdi:account"
/>
<v-text-field
v-model="user.password"
class="mt-1 mb-2"
:rules="['required']"
:label="
$formatMessage({
description: 'User creation dialog: Password field',
defaultMessage: 'Password',
id: 'o+A10T',
})
"
autocomplete="off"
:type="showPassword ? 'text' : 'password'"
:append-inner-icon="showPassword ? 'i-mdi:eye' : 'i-mdi:eye-off'"
prepend-icon="mdi-none"
@click:append-inner="showPassword = !showPassword"
/>
<v-row>
<v-col>
<v-text-field
v-model="user!.email"
autofocus
:rules="['required', 'email']"
:label="
$formatMessage({
description: 'User creation dialog: Email field',
defaultMessage: 'Email',
id: 'ToD0+o',
})
"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="user.password"
:rules="['required']"
:label="
$formatMessage({
description: 'User creation dialog: Password field',
defaultMessage: 'Password',
id: 'o+A10T',
})
"
autocomplete="off"
:type="showPassword ? 'text' : 'password'"
:append-inner-icon="showPassword ? 'i-mdi:eye' : 'i-mdi:eye-off'"
@click:append-inner="showPassword = !showPassword"
/>
</v-col>
</v-row>
</template>
<v-row>
<v-col>
<v-divider
class="mb-4"
v-if="!user.id"
/>
<div class="text-subtitle-2">Permissions</div>
</v-col>
</v-row>
<!-- Roles -->
<v-select
v-model="user.roles"
chips
closable-chips
multiple
:label="
$formatMessage({
description: 'User creation/edit dialog: Roles field',
defaultMessage: 'Roles',
id: 'CUxhzL',
})
"
prepend-icon="i-mdi:key-chain"
:items="userRoles"
/>
<v-row>
<v-col>
<v-select
v-model="user.roles"
chips
closable-chips
multiple
hide-details
:label="
$formatMessage({
description: 'User creation/edit dialog: Roles field',
defaultMessage: 'Roles',
id: 'CUxhzL',
})
"
:items="userRoles"
/>
</v-col>
</v-row>
<!-- Shared libraries -->
<v-select
v-model="user.sharedLibraries!.libraryIds"
multiple
:label="
$formatMessage({
description: 'User creation/edit dialog: Shared Libraries field',
defaultMessage: 'Shared Libraries',
id: 'UvhIIT',
})
"
:items="libraries"
item-title="name"
item-value="id"
prepend-icon="i-mdi:book-multiple"
>
<!-- Workaround for the lack of a slot to override the whole selection -->
<template #prepend-inner>
<!-- Show an All Libraries chip instead of the selection -->
<v-chip
v-if="user.sharedLibraries?.all"
:text="
<v-row>
<v-col>
<v-select
v-model="user.sharedLibraries!.libraryIds"
multiple
hide-details
:label="
$formatMessage({
description:
'User creation/edit dialog: Shared Libraries field, value shown when user has access to all libraries',
defaultMessage: 'All libraries',
id: 'app.user-create-dialog.all_libraries',
description: 'User creation/edit dialog: Shared Libraries field',
defaultMessage: 'Shared Libraries',
id: 'UvhIIT',
})
"
size="small"
/>
</template>
<template #selection="{ item }">
<!-- Show the selection only if 'all' is false -->
<v-chip
v-if="!user.sharedLibraries?.all"
size="small"
:text="item.title"
/>
</template>
<template #prepend-item>
<v-list-item
:title="
$formatMessage({
description:
'User creation/edit dialog: Shared Libraries field, value shown when user has access to all libraries',
defaultMessage: 'All libraries',
id: 'app.user-create-dialog.all_libraries',
})
"
@click="selectAllLibraries"
:items="libraries"
item-title="name"
item-value="id"
>
<template #prepend>
<v-checkbox-btn :model-value="user.sharedLibraries?.all" />
<!-- Workaround for the lack of a slot to override the whole selection -->
<template #prepend-inner>
<!-- Show an All Libraries chip instead of the selection -->
<v-chip
v-if="user.sharedLibraries?.all"
:text="
$formatMessage({
description:
'User creation/edit dialog: Shared Libraries field, value shown when user has access to all libraries',
defaultMessage: 'All libraries',
id: 'app.user-create-dialog.all_libraries',
})
"
size="small"
/>
</template>
</v-list-item>
</template>
<template #item="{ props: itemProps }">
<v-list-item
:disabled="user.sharedLibraries?.all"
v-bind="itemProps"
>
<template #prepend="{ isSelected }">
<v-checkbox-btn :model-value="isSelected" />
<template #selection="{ item }">
<!-- Show the selection only if 'all' is false -->
<v-chip
v-if="!user.sharedLibraries?.all"
size="small"
:text="item.title"
/>
</template>
</v-list-item>
</template>
</v-select>
<template #prepend-item>
<v-list-item
:title="
$formatMessage({
description:
'User creation/edit dialog: Shared Libraries field, value shown when user has access to all libraries',
defaultMessage: 'All libraries',
id: 'app.user-create-dialog.all_libraries',
})
"
@click="selectAllLibraries"
>
<template #prepend>
<v-checkbox-btn :model-value="user.sharedLibraries?.all" />
</template>
</v-list-item>
</template>
<template #item="{ props: itemProps }">
<v-list-item
:disabled="user.sharedLibraries?.all"
v-bind="itemProps"
>
<template #prepend="{ isSelected }">
<v-checkbox-btn :model-value="isSelected" />
</template>
</v-list-item>
</template>
</v-select>
</v-col>
</v-row>
<!-- Age restriction -->
<v-row>
<v-col>
<v-divider class="mb-4" />
<div class="text-subtitle-2">Age restriction</div>
</v-col>
</v-row>
<v-row>
<v-col
cols="12"
sm="6"
>
<v-select
v-model="user.ageRestriction!.restriction"
:label="
@ -134,10 +165,13 @@
})
"
:items="ageRestrictions"
prepend-icon="i-mdi:folder-lock"
hide-details
/>
</v-col>
<v-col>
<v-col
cols="12"
sm="6"
>
<v-number-input
v-model="user.ageRestriction!.age"
:disabled="user.ageRestriction?.restriction?.toString() === 'NONE'"
@ -154,59 +188,73 @@
</v-col>
</v-row>
<v-row>
<v-col>
<v-divider class="mb-4" />
<div class="text-subtitle-2">Label restrictions</div>
</v-col>
</v-row>
<!-- Allow labels -->
<v-combobox
v-model="user.labelsAllow"
:label="
$formatMessage({
description: 'User creation/edit dialog: Allow only labels field label',
defaultMessage: 'Allow only labels',
id: 'Sj0HXz',
})
"
chips
closable-chips
multiple
:items="sharingLabels"
prepend-icon="mdi-none"
>
<template #prepend-item>
<v-list-item>
<span class="font-weight-medium">
{{ $formatMessage(commonMessages.selectItemOrCreateOne) }}
</span>
</v-list-item>
</template>
</v-combobox>
<v-row>
<v-col>
<v-combobox
v-model="user.labelsAllow"
:label="
$formatMessage({
description: 'User creation/edit dialog: Allow only labels field label',
defaultMessage: 'Allow only labels',
id: 'Sj0HXz',
})
"
chips
closable-chips
multiple
hide-details
:items="sharingLabels"
>
<template #prepend-item>
<v-list-item>
<span class="font-weight-medium">
{{ $formatMessage(commonMessages.selectItemOrCreateOne) }}
</span>
</v-list-item>
</template>
</v-combobox>
</v-col>
</v-row>
<!-- Exclude labels -->
<v-combobox
v-model="user.labelsExclude"
:label="
$formatMessage({
description: 'User creation/edit dialog: Exclude labels field label',
defaultMessage: 'Exclude labels',
id: '3W0jUi',
})
"
chips
closable-chips
multiple
:items="sharingLabels"
prepend-icon="mdi-none"
>
<template #prepend-item>
<v-list-item>
<span class="font-weight-medium">
{{ $formatMessage(commonMessages.selectItemOrCreateOne) }}
</span>
</v-list-item>
</template>
</v-combobox>
<v-row>
<v-col>
<v-combobox
v-model="user.labelsExclude"
:label="
$formatMessage({
description: 'User creation/edit dialog: Exclude labels field label',
defaultMessage: 'Exclude labels',
id: '3W0jUi',
})
"
chips
closable-chips
multiple
hide-details
:items="sharingLabels"
>
<template #prepend-item>
<v-list-item>
<span class="font-weight-medium">
{{ $formatMessage(commonMessages.selectItemOrCreateOne) }}
</span>
</v-list-item>
</template>
</v-combobox>
</v-col>
</v-row>
</template>
<script setup lang="ts">
import { UserRoles } from '@/types/UserRoles'
import { UserRoles, userRolesMessages } from '@/types/UserRoles'
import type { components } from '@/generated/openapi/komga'
import { useLibraries } from '@/colada/libraries'
import { useSharingLabels } from '@/colada/referential'
@ -237,7 +285,7 @@ function selectAllLibraries() {
const userRoles = computed(() =>
Object.keys(UserRoles).map((x) => ({
title: x,
title: intl.formatMessage(userRolesMessages[x as UserRoles]),
value: x,
})),
)

View file

@ -4,12 +4,7 @@
<LayoutAppDrawer />
<v-main scrollable>
<v-container
fluid
class="pa-6"
>
<router-view />
</v-container>
<router-view />
</v-main>
</template>

View file

@ -1,24 +1,32 @@
<template>
<v-empty-state
v-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else>
<FragmentApikeyTable
:api-keys="apiKeys"
:loading="isLoading"
@enter-add-api-key="(target) => (dialogGenerateActivator = target)"
@enter-force-sync-api-key="(target) => (dialogConfirm.activator = target)"
@force-sync-api-key="(apiKey) => showDialog(ACTION.FORCE_SYNC, apiKey)"
@enter-delete-api-key="(target) => (dialogConfirm.activator = target)"
@delete-api-key="(apiKey) => showDialog(ACTION.DELETE, apiKey)"
<v-container
fluid
class="pa-0 pa-sm-4"
>
<v-empty-state
v-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
</template>
<FragmentApikeyGenerateDialog :activator="dialogGenerateActivator" />
<template v-else>
<FragmentApikeyTable
:api-keys="apiKeys"
:loading="isLoading"
@enter-add-api-key="(target) => (dialogGenerateActivator = target)"
@enter-force-sync-api-key="(target) => (dialogConfirm.activator = target)"
@force-sync-api-key="(apiKey) => showDialog(ACTION.FORCE_SYNC, apiKey)"
@enter-delete-api-key="(target) => (dialogConfirm.activator = target)"
@delete-api-key="(apiKey) => showDialog(ACTION.DELETE, apiKey)"
/>
</template>
<FragmentApikeyGenerateDialog
:activator="dialogGenerateActivator"
:fullscreen="display.xs.value"
/>
</v-container>
</template>
<script lang="ts" setup>
@ -33,8 +41,10 @@ import ApikeyDeletionWarning from '@/components/apikey/DeletionWarning.vue'
import ForceSyncWarning from '@/components/apikey/ForceSyncWarning.vue'
import { useApiKeys, useDeleteApiKey } from '@/colada/users'
import { useDeleteSyncPoints } from '@/colada/syncpoints'
import { useDisplay } from 'vuetify'
const intl = useIntl()
const display = useDisplay()
// API data
const { data: apiKeys, error, isLoading, refetch: refetchApiKeys } = useApiKeys()
@ -79,6 +89,7 @@ function showDialog(action: ACTION, apiKey?: components['schemas']['ApiKeyDto'])
id: 'IE0XzE',
}),
closeOnSave: false,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(ApikeyDeletionWarning),
@ -102,6 +113,7 @@ function showDialog(action: ACTION, apiKey?: components['schemas']['ApiKeyDto'])
id: 'W3BUf7',
}),
closeOnSave: false,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(ForceSyncWarning),

View file

@ -1,26 +1,31 @@
<template>
<v-empty-state
v-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else-if="currentUser">
<UserDetails :user="currentUser" />
<v-btn
:text="
$formatMessage({
description: 'User details screen: change password button',
defaultMessage: 'Change password',
id: 'sGsWvI',
})
"
class="mt-8"
@click="changePassword()"
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
<v-container
fluid
class="pa-4"
>
<v-empty-state
v-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
</template>
<template v-else-if="currentUser">
<UserDetails :user="currentUser" />
<v-btn
:text="
$formatMessage({
description: 'User details screen: change password button',
defaultMessage: 'Change password',
id: 'sGsWvI',
})
"
class="mt-8"
@click="changePassword()"
@mouseenter="dialogConfirmEdit.activator = $event.currentTarget"
/>
</template>
</v-container>
</template>
<script lang="ts" setup>

View file

@ -1,59 +1,61 @@
<template>
<v-skeleton-loader
v-if="isPending"
type="heading, text, paragraph@3"
/>
<v-container fluid>
<v-skeleton-loader
v-if="isPending"
type="heading, text, paragraph@3"
/>
<v-empty-state
v-else-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<v-empty-state
v-else-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else-if="announcements">
<div
v-for="(item, index) in announcements.items"
:key="index"
>
<AnnouncementCard
class="mb-4"
:item="item"
@mark-read="markRead"
/>
<!-- Bottom spacing for the FAB -->
<template v-else-if="announcements">
<div
v-if="index == announcements.items.length - 1"
class="mb-16"
/>
</div>
v-for="(item, index) in announcements.items"
:key="index"
>
<AnnouncementCard
class="mb-4"
:item="item"
@mark-read="markRead"
/>
<v-fab
:active="unreadCount > 0"
location="bottom right"
app
icon
size="x-large"
class="ms-n5"
@click="markAllRead()"
>
<!-- Workaround for https://github.com/vuetifyjs/vuetify/issues/21439 -->
<v-btn
v-tooltip:start="
$formatMessage({
description: 'Announcements view: mark all as read button tooltip',
defaultMessage: 'Mark all as read',
id: 'da/wb0',
})
"
color="success"
<!-- Bottom spacing for the FAB -->
<div
v-if="index == announcements.items.length - 1"
class="mb-16"
/>
</div>
<v-fab
:active="unreadCount > 0"
location="bottom right"
app
icon
size="x-large"
icon="i-mdi:check-all"
aria-label="mark all read"
/>
</v-fab>
</template>
class="ms-n5"
@click="markAllRead()"
>
<!-- Workaround for https://github.com/vuetifyjs/vuetify/issues/21439 -->
<v-btn
v-tooltip:start="
$formatMessage({
description: 'Announcements view: mark all as read button tooltip',
defaultMessage: 'Mark all as read',
id: 'da/wb0',
})
"
color="success"
size="x-large"
icon="i-mdi:check-all"
aria-label="mark all read"
/>
</v-fab>
</template>
</v-container>
</template>
<script lang="ts" setup>

View file

@ -1,24 +1,29 @@
<template>
<v-skeleton-loader
type="article@6, button@2"
v-if="isPending"
/>
<v-container
fluid
class="pa-4"
>
<v-skeleton-loader
type="article@6, button@2"
v-if="isPending"
/>
<v-empty-state
v-else-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else-if="settings">
<div class="d-flex">
<ServerSettings
:settings="settings"
:loading="loading"
@update-settings="(s) => saveSettings(s)"
/>
</div>
</template>
<v-empty-state
v-else-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else-if="settings">
<div class="d-flex">
<ServerSettings
:settings="settings"
:loading="loading"
@update-settings="(s) => saveSettings(s)"
/>
</div>
</template>
</v-container>
</template>
<script lang="ts" setup>

View file

@ -1,62 +1,64 @@
<template>
<v-skeleton-loader
v-if="isLoading"
type="heading, text, paragraph@3"
/>
<v-container fluid>
<v-skeleton-loader
v-if="isLoading"
type="heading, text, paragraph@3"
/>
<v-empty-state
v-else-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<v-empty-state
v-else-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else-if="releases">
<v-row>
<v-col>
<div v-if="isLatestVersion == true">
<v-alert
type="success"
variant="tonal"
>
{{
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'The latest version of Komga is already installed',
id: 'WNY0pu',
})
}}
</v-alert>
</div>
<div v-if="isLatestVersion == false">
<v-alert
type="warning"
variant="tonal"
>
{{
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'Updates are available',
id: 'n1Ik+L',
})
}}
</v-alert>
</div>
</v-col>
</v-row>
<template v-else-if="releases">
<v-row>
<v-col>
<div v-if="isLatestVersion == true">
<v-alert
type="success"
variant="tonal"
>
{{
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'The latest version of Komga is already installed',
id: 'WNY0pu',
})
}}
</v-alert>
</div>
<div v-if="isLatestVersion == false">
<v-alert
type="warning"
variant="tonal"
>
{{
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'Updates are available',
id: 'n1Ik+L',
})
}}
</v-alert>
</div>
</v-col>
</v-row>
<div
v-for="(release, index) in releases"
:key="index"
>
<ReleaseCard
:release="release"
:current="release.version == currentVersion"
:latest="release.version == latest?.version"
class="my-4"
/>
</div>
</template>
<div
v-for="(release, index) in releases"
:key="index"
>
<ReleaseCard
:release="release"
:current="release.version == currentVersion"
:latest="release.version == latest?.version"
class="my-4"
/>
</div>
</template>
</v-container>
</template>
<script lang="ts" setup>

View file

@ -1,25 +1,30 @@
<template>
<v-empty-state
v-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
<template v-else>
<FragmentUserTable
:users="users"
:loading="isLoading"
@add-user="showDialog(ACTION.ADD)"
@change-password="(user) => showDialog(ACTION.PASSWORD, user)"
@edit-user="(user) => showDialog(ACTION.EDIT, user)"
@delete-user="(user) => showDialog(ACTION.DELETE, user)"
@enter-add-user="(target) => (dialogConfirmEdit.activator = target)"
@enter-edit-user="(target) => (dialogConfirmEdit.activator = target)"
@enter-change-password="(target) => (dialogConfirmEdit.activator = target)"
@enter-delete-user="(target) => (dialogConfirm.activator = target)"
<v-container
fluid
class="pa-0 pa-sm-4"
>
<v-empty-state
v-if="error"
icon="i-mdi:connection"
:title="$formatMessage(commonMessages.somethingWentWrongTitle)"
:text="$formatMessage(commonMessages.somethingWentWrongSubTitle)"
/>
</template>
<template v-else>
<FragmentUserTable
:users="users"
:loading="isLoading"
@add-user="showDialog(ACTION.ADD)"
@change-password="(user) => showDialog(ACTION.PASSWORD, user)"
@edit-user="(user) => showDialog(ACTION.EDIT, user)"
@delete-user="(user) => showDialog(ACTION.DELETE, user)"
@enter-add-user="(target) => (dialogConfirmEdit.activator = target)"
@enter-edit-user="(target) => (dialogConfirmEdit.activator = target)"
@enter-change-password="(target) => (dialogConfirmEdit.activator = target)"
@enter-delete-user="(target) => (dialogConfirm.activator = target)"
/>
</template>
</v-container>
</template>
<script lang="ts" setup>
@ -85,6 +90,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
defaultMessage: 'Add User',
id: 'Bl30xt',
}),
scrollable: true,
maxWidth: 600,
closeOnSave: false,
fullscreen: display.xs.value,
@ -117,6 +123,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
id: 'Zh8AOV',
}),
subtitle: user?.email,
scrollable: true,
maxWidth: 600,
closeOnSave: false,
fullscreen: display.xs.value,
@ -158,6 +165,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
id: 'o8WeX3',
}),
closeOnSave: false,
fullscreen: display.xs.value,
}
dialogConfirm.value.slotWarning = {
component: markRaw(UserDeletionWarning),
@ -171,6 +179,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
subtitle: user?.email,
maxWidth: 400,
closeOnSave: false,
fullscreen: display.xs.value,
}
dialogConfirmEdit.value.slot = {
component: markRaw(UserFormChangePassword),