vuetify stuff

This commit is contained in:
Gauthier Roebroeck 2025-06-05 12:50:52 +08:00
parent 87102c9917
commit 49f924c8d5
84 changed files with 10846 additions and 10351 deletions

8
next-ui/.prettierrc.json Normal file
View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"singleQuote": true,
"printWidth": 100,
"trailingComma": "all",
"semi": false,
"singleAttributePerLine": true
}

View file

@ -8,7 +8,7 @@ declare module "*i18n?dir2json&ext=.json&1" {
export default json;
}
declare module "*dir2json" {
const json: any;
export default json;
}
declare module '*dir2json' {
const json: any
export default json
}

View file

@ -5,8 +5,9 @@
*/
import pluginVue from 'eslint-plugin-vue'
import {defineConfigWithVueTs, vueTsConfigs} from '@vue/eslint-config-typescript'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import formatjs from 'eslint-plugin-formatjs'
import eslintConfigPrettier from 'eslint-config-prettier'
export default defineConfigWithVueTs(
{
@ -16,14 +17,21 @@ export default defineConfigWithVueTs(
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', 'openapi-generator.mts'],
ignores: [
'**/dist/**',
'**/dist-ssr/**',
'**/coverage/**',
'openapi-generator.mts',
'**/generated/openapi/komga.d.ts',
],
},
...pluginVue.configs['flat/recommended'],
...pluginVue.configs['flat/essential'],
vueTsConfigs.recommendedTypeChecked,
{
rules: {
'prefer-promise-reject-errors': 'off',
'@typescript-eslint/no-unused-expressions': [
'error',
{
@ -31,8 +39,13 @@ export default defineConfigWithVueTs(
allowTernary: true,
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' },
],
'no-empty': ['error', { allowEmptyCatch: true }],
'vue/multi-word-component-names': 'off',
}
},
},
formatjs.configs.recommended,
@ -46,9 +59,11 @@ export default defineConfigWithVueTs(
'error',
{
idInterpolationPattern: '[sha512:contenthash:base64:6]',
idWhitelist: ['app.*']
idWhitelist: ['app.*'],
},
],
},
},
eslintConfigPrettier,
)

View file

@ -1,13 +1,22 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Vuetify 3</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<head>
<meta charset="UTF-8" />
<link
rel="icon"
href="/favicon.ico"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Welcome to Vuetify 3</title>
</head>
<body>
<div id="app"></div>
<script
type="module"
src="/src/main.ts"
></script>
</body>
</html>

View file

@ -14,6 +14,7 @@
"core-js": "^3.37.1",
"marked": "^15.0.12",
"openapi-fetch": "^0.14.0",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.3.0",
"vue": "^3.5.14",
"vue-intl": "^6.5.25",
@ -26,14 +27,16 @@
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.15.21",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-vue": "^10.1.0",
"npm-run-all2": "^8.0.3",
"openapi-typescript": "^7.8.0",
"pinia": "^3.0.2",
"prettier": "^3.5.3",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"typescript": "^5.8.3",
@ -1384,6 +1387,19 @@
"@pinia/colada": ">=0.16.0"
}
},
"node_modules/@pkgr/core": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
"integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@redocly/ajv": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz",
@ -2140,6 +2156,21 @@
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/eslint-config-prettier": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz",
"integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2"
},
"peerDependencies": {
"eslint": ">= 8.21.0",
"prettier": ">= 3.0.0"
}
},
"node_modules/@vue/eslint-config-typescript": {
"version": "14.5.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.5.0.tgz",
@ -3110,6 +3141,22 @@
}
}
},
"node_modules/eslint-config-prettier": {
"version": "10.1.5",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"funding": {
"url": "https://opencollective.com/eslint-config-prettier"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-formatjs": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-formatjs/-/eslint-plugin-formatjs-5.3.1.tgz",
@ -3131,6 +3178,37 @@
"eslint": "^9.23.0"
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz",
"integrity": "sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.11.7"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint-plugin-prettier"
},
"peerDependencies": {
"@types/eslint": ">=8.0.0",
"eslint": ">=8.0.0",
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
"prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
"@types/eslint": {
"optional": true
},
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-vue": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.1.0.tgz",
@ -3316,6 +3394,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@ -4639,6 +4724,35 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -5439,6 +5553,22 @@
"node": ">=16.0.0"
}
},
"node_modules/synckit": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.4"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",

View file

@ -9,7 +9,10 @@
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --fix",
"lint": "eslint .",
"lint:fix": "npm run lint -- --fix",
"prettier": "prettier --check \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
"prettier:fix": "npm run prettier -- --write",
"openapi-generate": "npx tsx ./openapi-generator.mts",
"i18n-extract": "formatjs extract \"src/**/*.{ts,tsx,vue}\" --ignore=\"**/*.d.ts\" --out-file i18n/en.json",
"i18n-compile": "formatjs compile-folder i18n src/i18n",
@ -22,6 +25,7 @@
"core-js": "^3.37.1",
"marked": "^15.0.12",
"openapi-fetch": "^0.14.0",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.3.0",
"vue": "^3.5.14",
"vue-intl": "^6.5.25",
@ -34,14 +38,16 @@
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.15.21",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-vue": "^10.1.0",
"npm-run-all2": "^8.0.3",
"openapi-typescript": "^7.8.0",
"pinia": "^3.0.2",
"prettier": "^3.5.3",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"typescript": "^5.8.3",

View file

@ -6,7 +6,7 @@
<script lang="ts" setup>
import { useTheme } from 'vuetify'
import {useAppStore} from '@/stores/app'
import { useAppStore } from '@/stores/app'
import { usePreferredDark } from '@vueuse/core'
const appStore = useAppStore()
@ -14,19 +14,21 @@ const theme = useTheme()
const prefersDark = usePreferredDark()
function updateTheme(selectedTheme: string, prefersDark: boolean) {
if(selectedTheme === 'system') {
if (selectedTheme === 'system') {
theme.global.name.value = prefersDark ? 'dark' : 'light'
} else {
theme.global.name.value = selectedTheme
}
}
watch([() => appStore.theme, prefersDark], ([selectedTheme, prefersDark]) => updateTheme(selectedTheme, prefersDark))
watch([() => appStore.theme, prefersDark], ([selectedTheme, prefersDark]) =>
updateTheme(selectedTheme, prefersDark),
)
// trigger an update on startup to get the proper theme loaded
updateTheme(appStore.theme, prefersDark.value)
</script>
<style>
@import "styles/global.scss";
@import 'styles/global.scss';
</style>

View file

@ -1,17 +1,32 @@
import type {Middleware} from 'openapi-fetch'
import type { Middleware } from 'openapi-fetch'
import createClient from 'openapi-fetch'
import type {paths} from '@/generated/openapi/komga'
import type { paths } from '@/generated/openapi/komga'
// Middleware that throws on error, so it works with Pinia Colada
const coladaMiddleware: Middleware = {
async onResponse({response}: { response: Response }) {
async onResponse({ response }: { response: Response }) {
if (!response.ok) {
const body = await response.json()
throw new Error(`${response.url}: ${response.status} ${response.statusText}`, {cause: body})
let body: unknown
try {
body = await response.json()
} catch (ignoreErr) {}
throw new Error(`${response.url}: ${response.status} ${response.statusText}`, {
cause: {
body: body,
status: response.status,
},
})
}
// return response untouched
return undefined
},
onError() {
throw new Error('error', {
cause: {
message: 'Server is unreachable',
},
})
},
}
const client = createClient<paths>({
@ -19,8 +34,14 @@ const client = createClient<paths>({
// required to pass the session cookie on all requests
credentials: 'include',
// required to avoid browser basic-auth popups
headers: {'X-Requested-With': 'XMLHttpRequest'},
headers: { 'X-Requested-With': 'XMLHttpRequest' },
})
client.use(coladaMiddleware)
export interface ErrorCause {
body?: unknown
status?: number
message?: string
}
export const komgaClient = client

View file

@ -1,77 +1,94 @@
/* eslint-disable */
/* prettier-ignore */
// prettier-ignore
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router/auto')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const EffectScope: (typeof import('vue'))['EffectScope']
const computed: (typeof import('vue'))['computed']
const createApp: (typeof import('vue'))['createApp']
const customRef: (typeof import('vue'))['customRef']
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
const defineComponent: (typeof import('vue'))['defineComponent']
const effectScope: (typeof import('vue'))['effectScope']
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
const h: (typeof import('vue'))['h']
const inject: (typeof import('vue'))['inject']
const isProxy: (typeof import('vue'))['isProxy']
const isReactive: (typeof import('vue'))['isReactive']
const isReadonly: (typeof import('vue'))['isReadonly']
const isRef: (typeof import('vue'))['isRef']
const markRaw: (typeof import('vue'))['markRaw']
const nextTick: (typeof import('vue'))['nextTick']
const onActivated: (typeof import('vue'))['onActivated']
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave']
const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate']
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
const onDeactivated: (typeof import('vue'))['onDeactivated']
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
const onMounted: (typeof import('vue'))['onMounted']
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
const onUnmounted: (typeof import('vue'))['onUnmounted']
const onUpdated: (typeof import('vue'))['onUpdated']
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
const provide: (typeof import('vue'))['provide']
const reactive: (typeof import('vue'))['reactive']
const readonly: (typeof import('vue'))['readonly']
const ref: (typeof import('vue'))['ref']
const resolveComponent: (typeof import('vue'))['resolveComponent']
const shallowReactive: (typeof import('vue'))['shallowReactive']
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
const shallowRef: (typeof import('vue'))['shallowRef']
const toRaw: (typeof import('vue'))['toRaw']
const toRef: (typeof import('vue'))['toRef']
const toRefs: (typeof import('vue'))['toRefs']
const toValue: (typeof import('vue'))['toValue']
const triggerRef: (typeof import('vue'))['triggerRef']
const unref: (typeof import('vue'))['unref']
const useAttrs: (typeof import('vue'))['useAttrs']
const useCssModule: (typeof import('vue'))['useCssModule']
const useCssVars: (typeof import('vue'))['useCssVars']
const useId: (typeof import('vue'))['useId']
const useLink: (typeof import('vue-router'))['useLink']
const useModel: (typeof import('vue'))['useModel']
const useRoute: (typeof import('vue-router/auto'))['useRoute']
const useRouter: (typeof import('vue-router/auto'))['useRouter']
const useSlots: (typeof import('vue'))['useSlots']
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
const watch: (typeof import('vue'))['watch']
const watchEffect: (typeof import('vue'))['watchEffect']
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type {
Component,
Slot,
Slots,
ComponentPublicInstance,
ComputedRef,
DirectiveBinding,
ExtractDefaultPropTypes,
ExtractPropTypes,
ExtractPublicPropTypes,
InjectionKey,
PropType,
Ref,
MaybeRef,
MaybeRefOrGetter,
VNode,
WritableComputedRef,
} from 'vue'
import('vue')
}
@ -80,63 +97,63 @@ import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly EffectScope: UnwrapRef<(typeof import('vue'))['EffectScope']>
readonly computed: UnwrapRef<(typeof import('vue'))['computed']>
readonly createApp: UnwrapRef<(typeof import('vue'))['createApp']>
readonly customRef: UnwrapRef<(typeof import('vue'))['customRef']>
readonly defineAsyncComponent: UnwrapRef<(typeof import('vue'))['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<(typeof import('vue'))['defineComponent']>
readonly effectScope: UnwrapRef<(typeof import('vue'))['effectScope']>
readonly getCurrentInstance: UnwrapRef<(typeof import('vue'))['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<(typeof import('vue'))['getCurrentScope']>
readonly h: UnwrapRef<(typeof import('vue'))['h']>
readonly inject: UnwrapRef<(typeof import('vue'))['inject']>
readonly isProxy: UnwrapRef<(typeof import('vue'))['isProxy']>
readonly isReactive: UnwrapRef<(typeof import('vue'))['isReactive']>
readonly isReadonly: UnwrapRef<(typeof import('vue'))['isReadonly']>
readonly isRef: UnwrapRef<(typeof import('vue'))['isRef']>
readonly markRaw: UnwrapRef<(typeof import('vue'))['markRaw']>
readonly nextTick: UnwrapRef<(typeof import('vue'))['nextTick']>
readonly onActivated: UnwrapRef<(typeof import('vue'))['onActivated']>
readonly onBeforeMount: UnwrapRef<(typeof import('vue'))['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<(typeof import('vue'))['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<(typeof import('vue'))['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<(typeof import('vue'))['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<(typeof import('vue'))['onErrorCaptured']>
readonly onMounted: UnwrapRef<(typeof import('vue'))['onMounted']>
readonly onRenderTracked: UnwrapRef<(typeof import('vue'))['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<(typeof import('vue'))['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<(typeof import('vue'))['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<(typeof import('vue'))['onServerPrefetch']>
readonly onUnmounted: UnwrapRef<(typeof import('vue'))['onUnmounted']>
readonly onUpdated: UnwrapRef<(typeof import('vue'))['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<(typeof import('vue'))['onWatcherCleanup']>
readonly provide: UnwrapRef<(typeof import('vue'))['provide']>
readonly reactive: UnwrapRef<(typeof import('vue'))['reactive']>
readonly readonly: UnwrapRef<(typeof import('vue'))['readonly']>
readonly ref: UnwrapRef<(typeof import('vue'))['ref']>
readonly resolveComponent: UnwrapRef<(typeof import('vue'))['resolveComponent']>
readonly shallowReactive: UnwrapRef<(typeof import('vue'))['shallowReactive']>
readonly shallowReadonly: UnwrapRef<(typeof import('vue'))['shallowReadonly']>
readonly shallowRef: UnwrapRef<(typeof import('vue'))['shallowRef']>
readonly toRaw: UnwrapRef<(typeof import('vue'))['toRaw']>
readonly toRef: UnwrapRef<(typeof import('vue'))['toRef']>
readonly toRefs: UnwrapRef<(typeof import('vue'))['toRefs']>
readonly toValue: UnwrapRef<(typeof import('vue'))['toValue']>
readonly triggerRef: UnwrapRef<(typeof import('vue'))['triggerRef']>
readonly unref: UnwrapRef<(typeof import('vue'))['unref']>
readonly useAttrs: UnwrapRef<(typeof import('vue'))['useAttrs']>
readonly useCssModule: UnwrapRef<(typeof import('vue'))['useCssModule']>
readonly useCssVars: UnwrapRef<(typeof import('vue'))['useCssVars']>
readonly useId: UnwrapRef<(typeof import('vue'))['useId']>
readonly useModel: UnwrapRef<(typeof import('vue'))['useModel']>
readonly useRoute: UnwrapRef<(typeof import('vue-router/auto'))['useRoute']>
readonly useRouter: UnwrapRef<(typeof import('vue-router/auto'))['useRouter']>
readonly useSlots: UnwrapRef<(typeof import('vue'))['useSlots']>
readonly useTemplateRef: UnwrapRef<(typeof import('vue'))['useTemplateRef']>
readonly watch: UnwrapRef<(typeof import('vue'))['watch']>
readonly watchEffect: UnwrapRef<(typeof import('vue'))['watchEffect']>
readonly watchPostEffect: UnwrapRef<(typeof import('vue'))['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<(typeof import('vue'))['watchSyncEffect']>
}
}
}

View file

@ -1,13 +1,12 @@
import {defineMutation, useMutation, useQueryCache} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import { defineMutation, useMutation, useQueryCache } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
export const useLogout = defineMutation(() => {
const queryCache = useQueryCache()
return useMutation({
mutation: () =>
komgaClient.POST('/api/logout'),
mutation: () => komgaClient.POST('/api/logout'),
onSuccess: () => {
void queryCache.invalidateQueries({key: ['current-user']})
void queryCache.invalidateQueries({ key: ['current-user'] })
},
onError: (error) => {
console.log('logout error', error)

View file

@ -1,13 +1,13 @@
import {defineMutation, useMutation, useQueryCache} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import { defineMutation, useMutation, useQueryCache } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
export const useMarkAnnouncementsRead = defineMutation(() => {
const queryCache = useQueryCache()
return useMutation({
mutation: (announcementIds: string[]) =>
komgaClient.PUT('/api/v1/announcements', {body: announcementIds}),
komgaClient.PUT('/api/v1/announcements', { body: announcementIds }),
onSuccess: () => {
void queryCache.invalidateQueries({key: ['announcements']})
void queryCache.invalidateQueries({ key: ['announcements'] })
},
onError: (error) => {
console.log('announcements mark read error', error)

View file

@ -1,6 +1,6 @@
import {defineMutation, useMutation, useQueryCache} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import type {components} from '@/generated/openapi/komga'
import { defineMutation, useMutation, useQueryCache } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
import type { components } from '@/generated/openapi/komga'
export const useCreateUser = defineMutation(() => {
const queryCache = useQueryCache()
@ -10,7 +10,7 @@ export const useCreateUser = defineMutation(() => {
body: user,
}),
onSuccess: () => {
void queryCache.invalidateQueries({key: ['users']})
void queryCache.invalidateQueries({ key: ['users'] })
},
onError: (error) => {
console.log('create user error', error)
@ -22,11 +22,11 @@ export const useUpdateUser = defineMutation(() => {
return useMutation({
mutation: (user: components['schemas']['UserDto']) =>
komgaClient.PATCH('/api/v2/users/{id}', {
params: {path: {id: user.id}},
params: { path: { id: user.id } },
body: user,
}),
onSuccess: () => {
void queryCache.invalidateQueries({key: ['users']})
void queryCache.invalidateQueries({ key: ['users'] })
},
onError: (error) => {
console.log('update user error', error)
@ -36,9 +36,9 @@ export const useUpdateUser = defineMutation(() => {
export const useUpdateUserPassword = defineMutation(() => {
return useMutation({
mutation: ({userId, newPassword}: { userId: string, newPassword: string }) =>
mutation: ({ userId, newPassword }: { userId: string; newPassword: string }) =>
komgaClient.PATCH('/api/v2/users/{id}/password', {
params: {path: {id: userId}},
params: { path: { id: userId } },
body: {
password: newPassword,
},
@ -54,10 +54,10 @@ export const useDeleteUser = defineMutation(() => {
return useMutation({
mutation: (userId: string) =>
komgaClient.DELETE('/api/v2/users/{id}', {
params: {path: {id: userId}},
params: { path: { id: userId } },
}),
onSuccess: () => {
void queryCache.invalidateQueries({key: ['users']})
void queryCache.invalidateQueries({ key: ['users'] })
},
onError: (error) => {
console.log('delete user error', error)

View file

@ -1,13 +1,15 @@
import {defineQuery, useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import type {ActuatorInfo} from '@/types/Actuator'
import { defineQuery, useQuery } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
import type { ActuatorInfo } from '@/types/Actuator'
export const useActuatorInfo = defineQuery(() => {
const {data, ...rest} = useQuery({
const { data, ...rest } = useQuery({
key: () => ['actuator-info'],
query: () => komgaClient.GET('/actuator/info')
// unwrap the openapi-fetch structure on success
.then((res) => res.data as ActuatorInfo),
query: () =>
komgaClient
.GET('/actuator/info')
// unwrap the openapi-fetch structure on success
.then((res) => res.data as ActuatorInfo),
// 1 hour
staleTime: 60 * 60 * 1000,
gcTime: false,

View file

@ -1,21 +1,22 @@
import {defineQuery, useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import { defineQuery, useQuery } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
export const useAnnouncements = defineQuery(() => {
const {data, ...rest} = useQuery({
const { data, ...rest } = useQuery({
key: () => ['announcements'],
query: () => komgaClient.GET('/api/v1/announcements')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
query: () =>
komgaClient
.GET('/api/v1/announcements')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
// 1 hour
staleTime: 60 * 60 * 1000,
gcTime: false,
})
const unreadCount = computed(() => data.value?.items
?.filter((x) => false == x._komga?.read)
?.length || 0
const unreadCount = computed(
() => data.value?.items?.filter((x) => false == x._komga?.read)?.length || 0,
)
return {...rest, data, unreadCount}
return { ...rest, data, unreadCount }
})

View file

@ -1,23 +1,25 @@
import {defineQuery, useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import {useActuatorInfo} from '@/colada/queries/actuator-info.ts'
import { defineQuery, useQuery } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
import { useActuatorInfo } from '@/colada/queries/actuator-info'
export const useAppReleases = defineQuery(() => {
const {data, ...rest} = useQuery({
const { data, ...rest } = useQuery({
key: () => ['app-releases'],
query: () => komgaClient.GET('/api/v1/releases')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
query: () =>
komgaClient
.GET('/api/v1/releases')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
// 1 hour
staleTime: 60 * 60 * 1000,
gcTime: false,
})
const {buildVersion} = useActuatorInfo()
const latestRelease = computed(() => data.value?.find(x => x.latest))
const { buildVersion } = useActuatorInfo()
const latestRelease = computed(() => data.value?.find((x) => x.latest))
const isLatestVersion = computed(() => {
if(buildVersion.value && latestRelease.value)
if (buildVersion.value && latestRelease.value)
return buildVersion.value == latestRelease.value?.version
else return undefined
})

View file

@ -1,20 +1,22 @@
import {defineQuery, useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import {UserRoles} from '@/types/UserRoles.ts'
import { defineQuery, useQuery } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
import { UserRoles } from '@/types/UserRoles'
export const useCurrentUser = defineQuery(() => {
const {data, ...rest} = useQuery({
const { data, ...rest } = useQuery({
key: () => ['current-user'],
query: () => komgaClient.GET('/api/v2/users/me')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
query: () =>
komgaClient
.GET('/api/v2/users/me')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
// 10 minutes
staleTime: 10 * 60 * 1000,
gcTime: false,
autoRefetch: true,
})
const hasRole =(role: UserRoles) => data.value?.roles.includes(role)
const hasRole = (role: UserRoles) => data.value?.roles.includes(role)
const isAdmin = computed(() => hasRole(UserRoles.ADMIN))
return {

View file

@ -1,12 +1,14 @@
import {defineQuery, useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import { defineQuery, useQuery } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
export const useLibraries = defineQuery(() => {
return useQuery({
key: () => ['libraries'],
query: () => komgaClient.GET('/api/v1/libraries')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
query: () =>
komgaClient
.GET('/api/v1/libraries')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
// 1 hour
staleTime: 60 * 60 * 1000,
gcTime: false,

View file

@ -1,12 +1,14 @@
import {defineQuery, useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import { defineQuery, useQuery } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
export const useSharingLabels = defineQuery(() => {
return useQuery({
key: () => ['sharing-labels'],
query: () => komgaClient.GET('/api/v1/sharing-labels')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
query: () =>
komgaClient
.GET('/api/v1/sharing-labels')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
// 1 hour
staleTime: 60 * 60 * 1000,
gcTime: false,

View file

@ -1,11 +1,13 @@
import {defineQuery, useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import { defineQuery, useQuery } from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
export const useUsers = defineQuery(() => {
return useQuery({
key: () => ['users'],
query: () => komgaClient.GET('/api/v2/users')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
query: () =>
komgaClient
.GET('/api/v2/users')
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
})
})

View file

@ -23,12 +23,10 @@ declare module 'vue' {
BuildVersion: typeof import('./components/BuildVersion.vue')['default']
DialogConfirm: typeof import('./components/dialogs/DialogConfirm.vue')['default']
DialogConfirmEdit: typeof import('./components/dialogs/DialogConfirmEdit.vue')['default']
Discord: typeof import('./components/icons/discord.vue')['default']
FormUserChangePassword: typeof import('./components/forms/user/FormUserChangePassword.vue')['default']
FormUserEdit: typeof import('./components/forms/user/FormUserEdit.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
LoginForm: typeof import('./components/LoginForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ThemeSelector: typeof import('./components/ThemeSelector.vue')['default']

View file

@ -24,37 +24,41 @@
class="text-caption"
href="https://komga.org"
target="_blank"
:text="$formatMessage({
description:'Drawer menu footer: documentation link',
defaultMessage:'Documentation',
id: 'ccAMWS',
})"
:text="
$formatMessage({
description: 'Drawer menu footer: documentation link',
defaultMessage: 'Documentation',
id: 'ccAMWS',
})
"
/>
</div>
</v-footer>
</template>
<script setup lang="ts">
const items = [
{
title: 'Komga GitHub',
icon: `mdi-github`,
href: 'https://github.com/gotson/komga',
},
{
title: 'Komga Discord',
icon: ['M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z'],
href: 'https://discord.gg/TdRpkDu',
},
]
const items = [
{
title: 'Komga GitHub',
icon: `mdi-github`,
href: 'https://github.com/gotson/komga',
},
{
title: 'Komga Discord',
icon: [
'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z',
],
href: 'https://discord.gg/TdRpkDu',
},
]
</script>
<style scoped lang="sass">
.social-link :deep(.v-icon)
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
text-decoration: none
transition: .2s ease-in-out
.social-link :deep(.v-icon)
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
text-decoration: none
transition: .2s ease-in-out
&:hover
color: rgba(25, 118, 210, 1)
&:hover
color: rgba(25, 118, 210, 1)
</style>

View file

@ -15,7 +15,7 @@
</template>
<script setup lang="ts">
import {useActuatorInfo} from '@/colada/queries/actuator-info'
import { useActuatorInfo } from '@/colada/queries/actuator-info'
const {commitId} = useActuatorInfo()
const { commitId } = useActuatorInfo()
</script>

View file

@ -20,7 +20,7 @@
</template>
<script setup lang="ts">
import {useAppReleases} from '@/colada/queries/app-releases.ts'
import { useAppReleases } from '@/colada/queries/app-releases'
const {buildVersion, isLatestVersion} = useAppReleases()
const { buildVersion, isLatestVersion } = useAppReleases()
</script>

View file

@ -27,22 +27,16 @@
</template>
<template #title>
<h2 class="text-h5 font-weight-bold">
Get started
</h2>
<h2 class="text-h5 font-weight-bold">Get started</h2>
</template>
<template #subtitle>
<div class="text-subtitle-1">
Replace this page by removing
<v-kbd>
{{
`
{{ `
<HelloWorld/>
<HelloWorld />
` }}
</v-kbd>
in
@ -161,8 +155,6 @@
</v-container>
</template>
<script setup lang="ts">
</script>
<script setup lang="ts"></script>
<script lang="ts">
</script>
<script lang="ts"></script>

View file

@ -12,11 +12,13 @@
color="primary"
>
<v-list-subheader
:title="$formatMessage({
description: 'Translations pop-up menu header',
defaultMessage: 'Translations',
id: 'InW6ko'
})"
:title="
$formatMessage({
description: 'Translations pop-up menu header',
defaultMessage: 'Translations',
id: 'InW6ko',
})
"
class="text-high-emphasis text-uppercase font-weight-black"
/>
@ -37,7 +39,7 @@
$formatMessage({
description: 'Translations pop-up menu footer',
defaultMessage: 'Help us translate',
id: 'FLqm9f'
id: 'FLqm9f',
})
}}
</v-list-item-title>
@ -47,7 +49,7 @@
</template>
<script setup lang="ts">
import {availableLocales, currentLocale, setLocale} from '@/utils/locale-helper.ts'
import { availableLocales, currentLocale, setLocale } from '@/utils/i18n/locale-helper'
const locales = Object.entries(availableLocales).map(([k, v]) => ({
title: v,
@ -55,9 +57,6 @@ const locales = Object.entries(availableLocales).map(([k, v]) => ({
}))
</script>
<script lang="ts">
</script>
<script lang="ts"></script>
<style scoped>
</style>
<style scoped></style>

View file

@ -1,113 +0,0 @@
<template>
<v-container max-width="550px">
<v-row justify="center">
<v-col>
<v-img src="@/assets/logo.svg" />
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="username"
:label="$formatMessage({
description: 'Login screen: email field label',
defaultMessage: 'Email',
id: 'QIr0z7'
})"
autofocus
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="password"
:label="$formatMessage({
description: 'Login screen: password field label',
defaultMessage: 'Password',
id: '5AAGkA'
})"
type="password"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox
v-model="rememberMe"
:label="$formatMessage({
description: 'Login screen: Remember Me checkbox',
defaultMessage: 'Remember Me',
id: '0YG9GQ'
})"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
:text="$formatMessage({
description: 'Login screen: Sign In button',
defaultMessage: 'Sign in',
id: '02SRax'
})"
@click="performLogin"
/>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import {komgaClient} from '@/api/komga-client'
import {useMutation, useQueryCache} from '@pinia/colada'
const username = ref('')
const password = ref('')
const rememberMe = ref(false)
const router = useRouter()
const route = useRoute()
function performLogin() {
const queryCache = useQueryCache()
const {mutate} = useMutation({
mutation: () =>
komgaClient.GET('/api/v2/users/me', {
headers: {
authorization: 'Basic ' + btoa(username.value + ':' + password.value),
'X-Requested-With': 'XMLHttpRequest',
},
params: {
query: {
'remember-me': rememberMe.value,
}
}
}),
onSuccess: ({data}) => {
queryCache.setQueryData(['current-user'], data)
queryCache.cancelQueries({key: ['current-user']})
if(route.query.redirect)
void router.push({path: route.query.redirect.toString()})
else
void router.push('/')
},
onError: (error) => {
//TODO: handle error
},
})
mutate()
}
</script>
<script lang="ts">
</script>
<style scoped>
</style>

View file

@ -10,13 +10,13 @@ The following example assumes a component located at `src/components/MyComponent
```vue
<template>
<div>
<MyComponent />
</div>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
//
//
</script>
```
@ -24,12 +24,12 @@ When your template is rendered, the component's import will automatically be inl
```vue
<template>
<div>
<MyComponent />
</div>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
import MyComponent from '@/components/MyComponent.vue'
import MyComponent from '@/components/MyComponent.vue'
</script>
```

View file

@ -6,39 +6,36 @@
</template>
<script setup lang="ts">
import {useAppStore} from '@/stores/app'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
const themes= [
const themes = [
{
value: 'light',
icon: 'mdi-weather-sunny'
icon: 'mdi-weather-sunny',
},
{
value: 'dark',
icon: 'mdi-weather-night'
icon: 'mdi-weather-night',
},
{
value: 'system',
icon: 'mdi-theme-light-dark'
icon: 'mdi-theme-light-dark',
},
]
const themeIcon = computed(
() => themes.find(x => x.value === appStore.theme)?.icon || 'mdi-theme-light-dark'
() => themes.find((x) => x.value === appStore.theme)?.icon || 'mdi-theme-light-dark',
)
function cycleTheme() {
const index = themes.findIndex(x => x.value === appStore.theme)
const index = themes.findIndex((x) => x.value === appStore.theme)
const newIndex = (index + 1) % themes.length
appStore.theme = themes[newIndex]!.value
}
</script>
<script lang="ts">
</script>
<script lang="ts"></script>
<style scoped>
</style>
<style scoped></style>

View file

@ -17,14 +17,11 @@
</template>
<script setup lang="ts">
import {useAppStore} from '@/stores/app'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
</script>
<script lang="ts">
</script>
<script lang="ts"></script>
<style scoped>
</style>
<style scoped></style>

View file

@ -1,7 +1,5 @@
<template>
<v-navigation-drawer
v-model="appStore.drawer"
>
<v-navigation-drawer v-model="appStore.drawer">
<AppDrawerMenu />
<template #append>
@ -11,7 +9,7 @@
</template>
<script setup lang="ts">
import {useAppStore} from '@/stores/app'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
</script>

View file

@ -10,11 +10,13 @@
class="text-caption"
href="https://komga.org"
target="_blank"
:text="$formatMessage({
description:'Drawer menu footer: documentation link',
defaultMessage:'Documentation',
id: 'ccAMWS',
})"
:text="
$formatMessage({
description: 'Drawer menu footer: documentation link',
defaultMessage: 'Documentation',
id: 'ccAMWS',
})
"
/>
</div>
</div>
@ -28,11 +30,11 @@
</div>
</div>
<!-- <AppFooter />-->
<!-- <AppFooter />-->
</template>
<script setup lang="ts">
import {useCurrentUser} from '@/colada/queries/current-user.ts'
import { useCurrentUser } from '@/colada/queries/current-user'
const {isAdmin} = useCurrentUser()
const { isAdmin } = useCurrentUser()
</script>

View file

@ -10,7 +10,7 @@
</template>
<script setup lang="ts">
import {useCurrentUser} from '@/colada/queries/current-user.ts'
import { useCurrentUser } from '@/colada/queries/current-user'
const {isAdmin} = useCurrentUser()
const { isAdmin } = useCurrentUser()
</script>

View file

@ -3,56 +3,62 @@
<template #activator="{ props }">
<v-list-item
v-bind="props"
:title="$formatMessage({
description: 'Drawer menu for My Account',
defaultMessage: 'My Account',
id: 'od545m'
})"
:title="
$formatMessage({
description: 'Drawer menu for My Account',
defaultMessage: 'My Account',
id: 'od545m',
})
"
prepend-icon="mdi-account"
/>
</template>
<v-list-item
to="/account/details"
:title="$formatMessage({
description: 'Drawer menu for My Account > Details',
defaultMessage: 'Details',
id: 'xYGXuU'
})"
:title="
$formatMessage({
description: 'Drawer menu for My Account > Details',
defaultMessage: 'Details',
id: 'xYGXuU',
})
"
/>
<v-list-item
to="/account/api-keys"
:title="$formatMessage({
description: 'Drawer menu for My Account > API Keys',
defaultMessage: 'API Keys',
id: 'oFOkWZ'
})"
:title="
$formatMessage({
description: 'Drawer menu for My Account > API Keys',
defaultMessage: 'API Keys',
id: 'oFOkWZ',
})
"
/>
<v-list-item
to="/account/ui"
:title="$formatMessage({
description: 'Drawer menu for My Account > User Interface',
defaultMessage: 'User Interface',
id: 'rw/Dkw'
})"
:title="
$formatMessage({
description: 'Drawer menu for My Account > User Interface',
defaultMessage: 'User Interface',
id: 'rw/Dkw',
})
"
/>
<v-list-item
to="/account/activity"
:title="$formatMessage({
description: 'Drawer menu for My Account > Activity',
defaultMessage: 'Activity',
id: 'cGFtPg'
})"
:title="
$formatMessage({
description: 'Drawer menu for My Account > Activity',
defaultMessage: 'Activity',
id: 'cGFtPg',
})
"
/>
</v-list-group>
</template>
<script setup lang="ts">
</script>
<script setup lang="ts"></script>
<script lang="ts">
</script>
<script lang="ts"></script>
<style scoped>
</style>
<style scoped></style>

View file

@ -1,11 +1,13 @@
<template>
<v-list-item
to="/history"
:title="$formatMessage({
description: 'Drawer menu for History',
defaultMessage: 'History',
id: 'l/To3S'
})"
:title="
$formatMessage({
description: 'Drawer menu for History',
defaultMessage: 'History',
id: 'l/To3S',
})
"
prepend-icon="mdi-clock-time-four-outline"
/>
</template>

View file

@ -3,31 +3,37 @@
<template #activator="{ props }">
<v-list-item
v-bind="props"
:title="$formatMessage({
description: 'Drawer menu for Import',
defaultMessage: 'Import',
id: 'N7+QXi'
})"
:title="
$formatMessage({
description: 'Drawer menu for Import',
defaultMessage: 'Import',
id: 'N7+QXi',
})
"
prepend-icon="mdi-import"
/>
</template>
<v-list-item
to="/import/books"
:title="$formatMessage({
description: 'Drawer menu for Import > Books',
defaultMessage: 'Books',
id: 'fQIepD'
})"
:title="
$formatMessage({
description: 'Drawer menu for Import > Books',
defaultMessage: 'Books',
id: 'fQIepD',
})
"
/>
<v-list-item
to="/import/readlist"
:title="$formatMessage({
description: 'Drawer menu for Import > Read List',
defaultMessage: 'Read List',
id: 'Y6VlM9'
})"
:title="
$formatMessage({
description: 'Drawer menu for Import > Read List',
defaultMessage: 'Read List',
id: 'Y6VlM9',
})
"
/>
</v-list-group>
</template>

View file

@ -1,20 +1,22 @@
<template>
<v-list-item
:title="$formatMessage({
description: 'Drawer menu for Logout',
defaultMessage: 'Logout',
id: 'ti4Pzo'
})"
:title="
$formatMessage({
description: 'Drawer menu for Logout',
defaultMessage: 'Logout',
id: 'ti4Pzo',
})
"
prepend-icon="mdi-power"
@click="performLogout"
/>
</template>
<script setup lang="ts">
import {useLogout} from '@/colada/mutations/logout'
import { useLogout } from '@/colada/mutations/logout'
const router = useRouter()
const {mutateAsync: logoutAsync} = useLogout()
const { mutateAsync: logoutAsync } = useLogout()
function performLogout() {
void logoutAsync().then(() => router.push('/login'))

View file

@ -3,71 +3,83 @@
<template #activator="{ props }">
<v-list-item
v-bind="props"
:title="$formatMessage({
description: 'Drawer menu for Media',
defaultMessage: 'Media',
id: 'Hl9H/B'
})"
:title="
$formatMessage({
description: 'Drawer menu for Media',
defaultMessage: 'Media',
id: 'Hl9H/B',
})
"
prepend-icon="mdi-book-cog"
/>
</template>
<v-list-item
to="/media/analysis"
:title="$formatMessage({
description: 'Drawer menu for Media > Media Analysis',
defaultMessage: 'Media Analysis',
id: 'DxtDpt'
})"
:title="
$formatMessage({
description: 'Drawer menu for Media > Media Analysis',
defaultMessage: 'Media Analysis',
id: 'DxtDpt',
})
"
/>
<v-list-item
to="/media/missing-posters"
:title="$formatMessage({
description: 'Drawer menu for Media > Missing Posters',
defaultMessage: 'Missing Posters',
id: 'Nb0V0p'
})"
:title="
$formatMessage({
description: 'Drawer menu for Media > Missing Posters',
defaultMessage: 'Missing Posters',
id: 'Nb0V0p',
})
"
/>
<v-list-item
to="/media/duplicate-files"
:title="$formatMessage({
description: 'Drawer menu for Media > Duplicate Files',
defaultMessage: 'Duplicate Files',
id: 'eW3fXu'
})"
:title="
$formatMessage({
description: 'Drawer menu for Media > Duplicate Files',
defaultMessage: 'Duplicate Files',
id: 'eW3fXu',
})
"
/>
<v-list-group value="Duplicate Pages">
<template #activator="{ props }">
<v-list-item
v-bind="props"
:title="$formatMessage({
description: 'Drawer menu for Media > Duplicate Pages',
defaultMessage: 'Duplicate Pages',
id: 'cAu/I6'
})"
:title="
$formatMessage({
description: 'Drawer menu for Media > Duplicate Pages',
defaultMessage: 'Duplicate Pages',
id: 'cAu/I6',
})
"
/>
</template>
<v-list-item
to="/media/duplicate-pages/known"
:title="$formatMessage({
description: 'Drawer menu for Media > Duplicate Pages > Known',
defaultMessage: 'Known',
id: 'MvwDsn'
})"
:title="
$formatMessage({
description: 'Drawer menu for Media > Duplicate Pages > Known',
defaultMessage: 'Known',
id: 'MvwDsn',
})
"
/>
<v-list-item
to="/media/duplicate-pages/unknown"
:title="$formatMessage({
description: 'Drawer menu for Media > Duplicate Pages > Unknown',
defaultMessage: 'Unknown',
id: 'qiZm6U'
})"
:title="
$formatMessage({
description: 'Drawer menu for Media > Duplicate Pages > Unknown',
defaultMessage: 'Unknown',
id: 'qiZm6U',
})
"
/>
</v-list-group>
</v-list-group>
</template>
<script setup lang="ts">
</script>
<script setup lang="ts"></script>

View file

@ -6,11 +6,13 @@
<template #activator="{ props }">
<v-list-item
v-bind="props"
:title="$formatMessage({
description: 'Drawer menu for Server',
defaultMessage: 'Server',
id: 'IpvWiZ'
})"
:title="
$formatMessage({
description: 'Drawer menu for Server',
defaultMessage: 'Server',
id: 'IpvWiZ',
})
"
prepend-icon="mdi-cog"
>
<template #prepend>
@ -28,44 +30,54 @@
<v-list-item
to="/server/users"
:title="$formatMessage({
description: 'Drawer menu for Server > Users',
defaultMessage: 'Users',
id: 'JGOfZq'
})"
:title="
$formatMessage({
description: 'Drawer menu for Server > Users',
defaultMessage: 'Users',
id: 'JGOfZq',
})
"
/>
<v-list-item
to="/server/settings"
:title="$formatMessage({
description: 'Drawer menu for Server > Settings',
defaultMessage: 'Settings',
id: 'HaWCi3'
})"
:title="
$formatMessage({
description: 'Drawer menu for Server > Settings',
defaultMessage: 'Settings',
id: 'HaWCi3',
})
"
/>
<v-list-item
to="/server/ui"
:title="$formatMessage({
description: 'Drawer menu for Server > User Interface',
defaultMessage: 'User Interface',
id: 'Yf4DJ2'
})"
:title="
$formatMessage({
description: 'Drawer menu for Server > User Interface',
defaultMessage: 'User Interface',
id: 'Yf4DJ2',
})
"
/>
<v-list-item
to="/server/metrics"
:title="$formatMessage({
description: 'Drawer menu for Server > Metrics',
defaultMessage: 'Metrics',
id: '2g7iOx'
})"
:title="
$formatMessage({
description: 'Drawer menu for Server > Metrics',
defaultMessage: 'Metrics',
id: '2g7iOx',
})
"
/>
<v-list-item
to="/server/announcements"
:title="$formatMessage({
description: 'Drawer menu for Server > Announcements',
defaultMessage: 'Announcements',
id: 'G7quju'
})"
:title="
$formatMessage({
description: 'Drawer menu for Server > Announcements',
defaultMessage: 'Announcements',
id: 'G7quju',
})
"
>
<template #append>
<v-badge
@ -79,17 +91,19 @@
<v-list-item
to="/server/updates"
:title="$formatMessage({
description: 'Drawer menu for Server > Updates',
defaultMessage: 'Updates',
id: 'lDnmZD'
})"
:title="
$formatMessage({
description: 'Drawer menu for Server > Updates',
defaultMessage: 'Updates',
id: 'lDnmZD',
})
"
/>
</v-list-group>
</template>
<script setup lang="ts">
import {useAnnouncements} from '@/colada/queries/announcements.ts'
import { useAnnouncements } from '@/colada/queries/announcements'
const {unreadCount} = useAnnouncements()
const { unreadCount } = useAnnouncements()
</script>

View file

@ -20,11 +20,12 @@
{
description: 'Confirmation dialog: default hint to retype validation text',
defaultMessage: 'Please type {validateText} to confirm.',
id: 'eVoe+D'
id: 'eVoe+D',
},
{
validateText: validateText,
})
},
)
}}
</slot>
@ -38,11 +39,13 @@
<template #actions>
<v-spacer />
<v-btn
:text="$formatMessage({
description: 'Confirmation dialog: Cancel button',
defaultMessage: 'Cancel',
id: 'pENCUD'
})"
:text="
$formatMessage({
description: 'Confirmation dialog: Cancel button',
defaultMessage: 'Cancel',
id: 'pENCUD',
})
"
@click="close()"
/>
<v-btn
@ -60,9 +63,9 @@
</template>
<script setup lang="ts">
import {useRules} from 'vuetify/labs/rules'
import { useRules } from 'vuetify/labs/rules'
const showDialog = defineModel<boolean>('dialog', {required: false})
const showDialog = defineModel<boolean>('dialog', { required: false })
const emit = defineEmits<{
confirm: []
}>()
@ -72,19 +75,19 @@ const formValid = ref<boolean>(false)
const rules = useRules()
function submitForm() {
if(formValid.value) {
if (formValid.value) {
emit('confirm')
close()
}
}
export interface Props {
title?: string,
subtitle?: string,
okText?: string,
validateText?: string,
maxWidth?: string | number,
activator?: Element | string,
title?: string
subtitle?: string
okText?: string
validateText?: string
maxWidth?: string | number
activator?: Element | string
}
const {
@ -100,4 +103,3 @@ function close() {
showDialog.value = false
}
</script>

View file

@ -31,19 +31,23 @@
<template #actions>
<v-spacer />
<v-btn
:text="$formatMessage({
description: 'ConfirmEdit dialog: Cancel button',
defaultMessage: 'Cancel',
id: 'G/T8/2'
})"
:text="
$formatMessage({
description: 'ConfirmEdit dialog: Cancel button',
defaultMessage: 'Cancel',
id: 'G/T8/2',
})
"
@click="close()"
/>
<v-btn
:text="$formatMessage({
description: 'ConfirmEdit dialog: Save button',
defaultMessage: 'Save',
id: 'N9WFH4'
})"
:text="
$formatMessage({
description: 'ConfirmEdit dialog: Save button',
defaultMessage: 'Save',
id: 'N9WFH4',
})
"
type="submit"
/>
</template>
@ -55,13 +59,13 @@
</template>
<script setup lang="ts">
const showDialog = defineModel<boolean>('dialog', {required: false})
const record = defineModel<unknown>('record', {required: true})
const showDialog = defineModel<boolean>('dialog', { required: false })
const record = defineModel<unknown>('record', { required: true })
const formValid = ref<boolean>(false)
function submitForm(callback: () => void) {
if(formValid.value) callback()
if (formValid.value) callback()
}
interface Props {
@ -69,10 +73,10 @@ interface Props {
* Dialog title
* @type string
*/
title?: string,
subtitle?: string,
maxWidth?: string | number,
activator?: Element | string,
title?: string
subtitle?: string
maxWidth?: string | number
activator?: Element | string
}
const {

View file

@ -2,11 +2,13 @@
<v-text-field
v-model="newPassword"
:rules="[rules.required()]"
:label="$formatMessage({
description: 'User password change dialog: New Password field label',
defaultMessage: 'New password',
id: 'WhasCZ'
})"
:label="
$formatMessage({
description: 'User password change dialog: New Password field label',
defaultMessage: 'New password',
id: 'WhasCZ',
})
"
autocomplete="off"
autofocus
:type="showPassword ? 'text' : 'password'"
@ -16,16 +18,23 @@
<v-text-field
v-model="confirmPassword"
class="mt-2"
:rules="[rules.sameAs(newPassword, $formatMessage({
description: 'User password change dialog: Error message if passwords differ',
defaultMessage: 'Passwords must be identical',
id: 'LaxrEO'
}))]"
:label="$formatMessage({
description: 'User password change dialog: Confirm Password field label',
defaultMessage: 'Confirm password',
id: 'nJiYF7'
})"
:rules="[
rules.sameAs(
newPassword,
$formatMessage({
description: 'User password change dialog: Error message if passwords differ',
defaultMessage: 'Passwords must be identical',
id: 'LaxrEO',
}),
),
]"
:label="
$formatMessage({
description: 'User password change dialog: Confirm Password field label',
defaultMessage: 'Confirm password',
id: 'nJiYF7',
})
"
autocomplete="off"
:type="showPassword ? 'text' : 'password'"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@ -38,7 +47,7 @@ import { useRules } from 'vuetify/labs/rules'
const rules = useRules()
const newPassword = defineModel<string>({required: true})
const newPassword = defineModel<string>({ required: true })
const confirmPassword = ref<string>()
const showPassword = ref<boolean>(false)

View file

@ -4,22 +4,26 @@
v-model="user!.email"
autofocus
:rules="[rules.required(), rules.email()]"
:label="$formatMessage({
description: 'User creation dialog: Email field',
defaultMessage: 'Email',
id: 'ToD0+o'
})"
:label="
$formatMessage({
description: 'User creation dialog: Email field',
defaultMessage: 'Email',
id: 'ToD0+o',
})
"
prepend-icon="mdi-account"
/>
<v-text-field
v-model="user.password"
class="mt-1 mb-2"
:rules="[rules.required()]"
:label="$formatMessage({
description: 'User creation dialog: Password field',
defaultMessage: 'Password',
id: 'o+A10T'
})"
:label="
$formatMessage({
description: 'User creation dialog: Password field',
defaultMessage: 'Password',
id: 'o+A10T',
})
"
autocomplete="off"
:type="showPassword ? 'text' : 'password'"
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
@ -34,11 +38,13 @@
chips
closable-chips
multiple
:label="$formatMessage({
description: 'User creation/edit dialog: Roles field',
defaultMessage: 'Roles',
id: 'CUxhzL'
})"
:label="
$formatMessage({
description: 'User creation/edit dialog: Roles field',
defaultMessage: 'Roles',
id: 'CUxhzL',
})
"
prepend-icon="mdi-key-chain"
:items="userRoles"
/>
@ -47,11 +53,13 @@
<v-select
v-model="user.sharedLibraries!.libraryIds"
multiple
:label="$formatMessage({
description: 'User creation/edit dialog: Shared Libraries field',
defaultMessage: 'Shared Libraries',
id: 'UvhIIT'
})"
:label="
$formatMessage({
description: 'User creation/edit dialog: Shared Libraries field',
defaultMessage: 'Shared Libraries',
id: 'UvhIIT',
})
"
:items="libraries"
item-title="name"
item-value="id"
@ -62,11 +70,14 @@
<!-- 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'
})"
: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>
@ -82,11 +93,14 @@
<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'
})"
: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>
@ -100,7 +114,7 @@
:disabled="user.sharedLibraries?.all"
v-bind="itemProps"
>
<template #prepend="{isSelected}">
<template #prepend="{ isSelected }">
<v-checkbox-btn :model-value="isSelected" />
</template>
</v-list-item>
@ -112,11 +126,13 @@
<v-col>
<v-select
v-model="user.ageRestriction!.restriction"
:label="$formatMessage({
description: 'User creation/edit dialog: Age restriction field label',
defaultMessage: 'Age restriction',
id: 'hEOGa9'
})"
:label="
$formatMessage({
description: 'User creation/edit dialog: Age restriction field label',
defaultMessage: 'Age restriction',
id: 'hEOGa9',
})
"
:items="ageRestrictions"
prepend-icon="mdi-folder-lock"
/>
@ -125,11 +141,13 @@
<v-number-input
v-model="user.ageRestriction!.age"
:disabled="user.ageRestriction?.restriction?.toString() === 'NONE'"
:label="$formatMessage({
description: 'User creation/edit dialog: Age Restriction > Age field label',
defaultMessage: 'Age',
id: 'jywpqq'
})"
:label="
$formatMessage({
description: 'User creation/edit dialog: Age Restriction > Age field label',
defaultMessage: 'Age',
id: 'jywpqq',
})
"
:min="0"
:rules="[rules.required()]"
/>
@ -139,11 +157,13 @@
<!-- 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'
})"
:label="
$formatMessage({
description: 'User creation/edit dialog: Allow only labels field label',
defaultMessage: 'Allow only labels',
id: 'Sj0HXz',
})
"
chips
closable-chips
multiple
@ -157,7 +177,7 @@
$formatMessage({
description: 'User creation/edit dialog: Allow only labels field selection',
defaultMessage: 'Select an item or create one',
id: 'app.user-create-dialog.select_create_one'
id: 'app.user-create-dialog.select_create_one',
})
}}
</span>
@ -168,11 +188,13 @@
<!-- Exclude labels -->
<v-combobox
v-model="user.labelsExclude"
:label="$formatMessage({
description: 'User creation/edit dialog: Exclude labels field label',
defaultMessage: 'Exclude labels',
id: '3W0jUi'
})"
:label="
$formatMessage({
description: 'User creation/edit dialog: Exclude labels field label',
defaultMessage: 'Exclude labels',
id: '3W0jUi',
})
"
chips
closable-chips
multiple
@ -186,7 +208,7 @@
$formatMessage({
description: 'User creation/edit dialog: Exclude labels field selection',
defaultMessage: 'Select an item or create one',
id: 'app.user-create-dialog.select_create_one'
id: 'app.user-create-dialog.select_create_one',
})
}}
</span>
@ -196,65 +218,67 @@
</template>
<script setup lang="ts">
import {UserRoles} from '@/types/UserRoles.ts'
import type {components} from '@/generated/openapi/komga'
import {useRules} from 'vuetify/labs/rules'
import {useLibraries} from '@/colada/queries/libraries.ts'
import {useSharingLabels} from '@/colada/queries/referential.ts'
import {useIntl} from 'vue-intl'
import { UserRoles } from '@/types/UserRoles'
import type { components } from '@/generated/openapi/komga'
import { useRules } from 'vuetify/labs/rules'
import { useLibraries } from '@/colada/queries/libraries'
import { useSharingLabels } from '@/colada/queries/referential'
import { useIntl } from 'vue-intl'
const rules = useRules()
const intl = useIntl()
interface UserExtend {
id?: string,
email: string,
password?: string,
id?: string
email: string
password?: string
}
type UserCreation = components['schemas']['UserCreationDto'] & UserExtend
type UserUpdate = components['schemas']['UserUpdateDto'] & UserExtend
const user = defineModel<UserCreation | UserUpdate>({required: true})
const user = defineModel<UserCreation | UserUpdate>({ required: true })
const showPassword = ref<boolean>(false)
const {data: libraries} = useLibraries()
const {data: sharingLabels} = useSharingLabels()
const { data: libraries } = useLibraries()
const { data: sharingLabels } = useSharingLabels()
function selectAllLibraries() {
user.value.sharedLibraries!.all = !user.value.sharedLibraries?.all
user.value.sharedLibraries!.libraryIds = libraries.value?.map(x => x.id) || []
user.value.sharedLibraries!.libraryIds = libraries.value?.map((x) => x.id) || []
}
const userRoles = computed(() => Object.keys(UserRoles).map(x => ({
title: x,
value: x,
})))
const userRoles = computed(() =>
Object.keys(UserRoles).map((x) => ({
title: x,
value: x,
})),
)
const ageRestrictions = [
{
title: intl.formatMessage({
description: 'User creation/edit dialog: Age restriction field possible option',
defaultMessage: 'No restriction',
id: 'AeA9Ka'
id: 'AeA9Ka',
}),
value: 'NONE'
value: 'NONE',
},
{
title: intl.formatMessage({
description: 'User creation/edit dialog: Age restriction field possible option',
defaultMessage: 'Allow only under',
id: '/bathK'
id: '/bathK',
}),
value: 'ALLOW_ONLY'
value: 'ALLOW_ONLY',
},
{
title: intl.formatMessage({
description: 'User creation/edit dialog: Age restriction field possible option',
defaultMessage: 'Exclude over',
id: 'wmGcF+'
id: 'wmGcF+',
}),
value: 'EXCLUDE'
value: 'EXCLUDE',
},
]
</script>

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,4 @@
</v-main>
</template>
<script lang="ts" setup>
</script>
<script lang="ts" setup></script>

View file

@ -5,5 +5,5 @@
</template>
<script lang="ts" setup>
//
//
</script>

View file

@ -5,13 +5,13 @@
*/
// Plugins
import {registerPlugins} from '@/plugins'
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
// Composables
import {createApp} from 'vue'
import { createApp } from 'vue'
const app = createApp(App)

View file

@ -3,5 +3,5 @@
</template>
<script lang="ts" setup>
//
//
</script>

View file

@ -3,5 +3,5 @@
</template>
<script lang="ts" setup>
//
//
</script>

View file

@ -3,5 +3,5 @@
</template>
<script lang="ts" setup>
//
//
</script>

View file

@ -3,5 +3,5 @@
</template>
<script lang="ts" setup>
//
//
</script>

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,5 +3,5 @@
</template>
<script lang="ts" setup>
//
//
</script>

View file

@ -1,9 +1,150 @@
<template>
<LoginForm />
<v-form
v-model="formValid"
:disabled="isLoading"
@submit.prevent="submitForm()"
>
<v-container max-width="550px">
<v-row justify="center">
<v-col>
<v-img src="@/assets/logo.svg" />
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="username"
:label="
$formatMessage({
description: 'Login screen: email field label',
defaultMessage: 'Email',
id: 'QIr0z7',
})
"
autofocus
:rules="[rules.required(), rules.email()]"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="password"
:label="
$formatMessage({
description: 'Login screen: password field label',
defaultMessage: 'Password',
id: '5AAGkA',
})
"
type="password"
:rules="[rules.required()]"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox
v-model="rememberMe"
:label="
$formatMessage({
description: 'Login screen: Remember Me checkbox',
defaultMessage: 'Remember Me',
id: '0YG9GQ',
})
"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
:text="
$formatMessage({
description: 'Login screen: Sign In button',
defaultMessage: 'Sign in',
id: '02SRax',
})
"
:loading="isLoading"
type="submit"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-snackbar
v-model="showError"
color="error"
:text="showErrorText"
>
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="showError = false"
>
Dismiss
</v-btn>
</template></v-snackbar
>
</v-col>
</v-row>
</v-container>
</v-form>
</template>
<script lang="ts" setup>
//
import { type ErrorCause, komgaClient } from '@/api/komga-client'
import { useMutation, useQueryCache } from '@pinia/colada'
import { useRules } from 'vuetify/labs/rules'
const rules = useRules()
const formValid = ref<boolean>(false)
const username = ref('')
const password = ref('')
const rememberMe = ref(false)
const showError = ref<boolean>(false)
const showErrorText = ref<string>('')
const router = useRouter()
const route = useRoute()
const queryCache = useQueryCache()
const { mutate: performLogin, isLoading } = useMutation({
mutation: () =>
komgaClient.GET('/api/v2/users/me', {
headers: {
authorization: 'Basic ' + btoa(username.value + ':' + password.value),
'X-Requested-With': 'XMLHttpRequest',
},
params: {
query: {
'remember-me': rememberMe.value,
},
},
}),
onSuccess: ({ data }) => {
queryCache.setQueryData(['current-user'], data)
queryCache.cancelQueries({ key: ['current-user'] })
if (route.query.redirect) void router.push({ path: route.query.redirect.toString() })
else void router.push('/')
},
onError: (error) => {
showErrorText.value = (error.cause as ErrorCause).message || 'Invalid authentication'
showError.value = true
},
})
function submitForm() {
if (formValid.value) performLogin()
}
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -16,60 +16,47 @@
v-for="(item, index) in announcements.items"
:key="index"
>
<v-row
justify="space-between"
align="center"
>
<v-col cols="auto">
<div class="ml-n2">
<a
:href="item.url"
target="_blank"
class="text-h3 font-weight-medium link-underline"
>{{ item.title }}</a>
</div>
<div class="mt-2 subtitle-1">
{{ $formatDate(item.date_modified, {dateStyle: 'long'}) }}
</div>
</v-col>
<v-col cols="auto">
<v-tooltip
:text="$formatMessage({
description: 'Announcements view: mark as read button tooltip',
defaultMessage: 'Mark as read',
id: 'sUSVQS'
})"
:disabled="item._komga?.read"
<v-card class="mb-4">
<template #title>
<a
:href="item.url"
target="_blank"
class="text-h3 font-weight-medium link-underline"
>{{ item.title }}</a
>
<template #activator="{ props }">
<v-fab
v-bind="props"
icon="mdi-check"
elevation="3"
color="success"
variant="outlined"
size="small"
:disabled="item._komga?.read"
@click="markRead(item.id)"
/>
</template>
</v-tooltip>
</v-col>
</v-row>
</template>
<template #subtitle>
{{ $formatDate(item.date_modified, { dateStyle: 'long' }) }}
</template>
<v-row>
<v-col cols="12">
<template #text>
<!-- eslint-disable vue/no-v-html -->
<div
class="announcement"
v-html="item.content_html"
/>
<!-- eslint-enable vue/no-v-html -->
</v-col>
</v-row>
<v-divider
v-if="index != announcements.items.length - 1"
class="my-8"
</template>
<template #actions>
<v-spacer />
<v-btn
:text="
$formatMessage({
description: 'Announcements view: mark as read button tooltip',
defaultMessage: 'Mark as read',
id: 'sUSVQS',
})
"
:disabled="item._komga?.read"
@click="markRead(item.id)"
/>
</template>
</v-card>
<div
v-if="index == announcements.items.length - 1"
class="mb-16"
/>
</div>
@ -84,11 +71,13 @@
>
<!-- 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'
})"
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="mdi-check-all"
@ -98,17 +87,17 @@
</template>
<script lang="ts" setup>
import {useAnnouncements} from '@/colada/queries/announcements.ts'
import {useMarkAnnouncementsRead} from '@/colada/mutations/mark-announcements-read.ts'
import {commonMessages} from '@/utils/common-messages.ts'
import { useAnnouncements } from '@/colada/queries/announcements'
import { useMarkAnnouncementsRead } from '@/colada/mutations/mark-announcements-read'
import { commonMessages } from '@/utils/i18n/common-messages'
const {data: announcements, error, unreadCount, isLoading} = useAnnouncements()
const { data: announcements, error, unreadCount, isLoading } = useAnnouncements()
const {mutate: markAnnouncementsRead} = useMarkAnnouncementsRead()
const { mutate: markAnnouncementsRead } = useMarkAnnouncementsRead()
function markAllRead() {
const ids = announcements.value?.items.map(x => x.id)
if(ids) markAnnouncementsRead(ids)
const ids = announcements.value?.items.map((x) => x.id)
if (ids) markAnnouncementsRead(ids)
}
function markRead(id: string) {

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
//
//
</script>
<route lang="yaml">

View file

@ -23,7 +23,7 @@
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'The latest version of Komga is already installed',
id: 'WNY0pu'
id: 'WNY0pu',
})
}}
</v-alert>
@ -37,7 +37,7 @@
$formatMessage({
description: 'Updates view: banner shown at the top',
defaultMessage: 'Updates are available',
id: 'n1Ik+L'
id: 'n1Ik+L',
})
}}
</v-alert>
@ -49,19 +49,15 @@
v-for="(release, index) in releases"
:key="index"
>
<v-row
justify="space-between"
align="center"
>
<v-col cols="auto">
<v-card class="my-4">
<template #title>
<div>
<a
:href="release.url"
target="_blank"
class="text-h4 font-weight-medium link-underline me-2"
>{{
release.version
}}</a>
>{{ release.version }}</a
>
<v-chip
v-if="release.version == currentVersion"
class="mx-2 mt-n3"
@ -71,9 +67,10 @@
>
{{
$formatMessage({
description: 'Updates view: badge showing next to the currently installed release number',
description:
'Updates view: badge showing next to the currently installed release number',
defaultMessage: 'Currently installed',
id: '3jrAF6'
id: '3jrAF6',
})
}}
</v-chip>
@ -87,42 +84,43 @@
$formatMessage({
description: 'Updates view: badge showing next to the latest release number',
defaultMessage: 'Latest',
id: '2Bh8F2'
id: '2Bh8F2',
})
}}
</v-chip>
</div>
<div class="mt-2 subtitle-1">
{{ $formatDate(release.releaseDate, {dateStyle: 'long'}) }}
</div>
</v-col>
</v-row>
</template>
<v-row>
<v-col cols="12">
<template #subtitle>
{{ $formatDate(release.releaseDate, { dateStyle: 'long' }) }}
</template>
<template #text>
<!-- eslint-disable vue/no-v-html -->
<div
class="release"
v-html="marked(release.description)"
/>
<!-- eslint-enable vue/no-v-html -->
</v-col>
</v-row>
<v-divider
v-if="index != releases.length - 1"
class="my-8"
/>
</template>
</v-card>
</div>
</template>
</template>
<script lang="ts" setup>
import {useAppReleases} from '@/colada/queries/app-releases.ts'
import {marked} from 'marked'
import {commonMessages} from '@/utils/common-messages.ts'
import { useAppReleases } from '@/colada/queries/app-releases'
import { marked } from 'marked'
import { commonMessages } from '@/utils/i18n/common-messages'
const {data: releases, error, buildVersion: currentVersion, isLatestVersion, latestRelease: latest, isLoading} = useAppReleases()
const {
data: releases,
error,
buildVersion: currentVersion,
isLatestVersion,
latestRelease: latest,
isLoading,
} = useAppReleases()
</script>
<style lang="scss">

View file

@ -50,7 +50,7 @@
</div>
</template>
<template #[`item.actions`]="{ item : user }">
<template #[`item.actions`]="{ item: user }">
<div class="d-flex ga-1 justify-end">
<v-icon-btn
v-tooltip:bottom="'Change password'"
@ -84,7 +84,7 @@
:max-width="currentAction === ACTION.PASSWORD ? 400 : 600"
@update:record="handleDialogConfirmation()"
>
<template #text="{proxyModel}">
<template #text="{ proxyModel }">
<component
:is="dialogComponent"
v-model="proxyModel.value"
@ -112,9 +112,7 @@
<li>The read progress for this user account will be permanently deleted.</li>
<li>Authentication activity for this user will be permanently deleted.</li>
</ul>
<div class="font-weight-bold mt-4">
This action cannot be undone.
</div>
<div class="font-weight-bold mt-4">This action cannot be undone.</div>
</v-alert>
</template>
</DialogConfirm>
@ -122,64 +120,72 @@
</template>
<script lang="ts" setup>
import {useUsers} from '@/colada/queries/users.ts'
import {komgaClient} from '@/api/komga-client.ts'
import type {components} from '@/generated/openapi/komga'
import {useCurrentUser} from '@/colada/queries/current-user.ts'
import {UserRoles} from '@/types/UserRoles.ts'
import {useCreateUser, useDeleteUser, useUpdateUser, useUpdateUserPassword} from '@/colada/mutations/update-user.ts'
import { useUsers } from '@/colada/queries/users'
import { komgaClient } from '@/api/komga-client'
import type { components } from '@/generated/openapi/komga'
import { useCurrentUser } from '@/colada/queries/current-user'
import { UserRoles } from '@/types/UserRoles'
import {
useCreateUser,
useDeleteUser,
useUpdateUser,
useUpdateUserPassword,
} from '@/colada/mutations/update-user'
import FormUserChangePassword from '@/components/forms/user/FormUserChangePassword.vue'
import FormUserEdit from '@/components/forms/user/FormUserEdit.vue'
import type {Component} from 'vue'
import {useLibraries} from '@/colada/queries/libraries.ts'
import {commonMessages} from '@/utils/common-messages.ts'
import type { Component } from 'vue'
import { useLibraries } from '@/colada/queries/libraries'
import { commonMessages } from '@/utils/i18n/common-messages'
// API data
const {data: users, error, isLoading, refetch: refetchUsers} = useUsers()
const {data: me} = useCurrentUser()
const { data: users, error, isLoading, refetch: refetchUsers } = useUsers()
const { data: me } = useCurrentUser()
// Table
const hideFooter = computed(() => users.value && users.value.length < 11)
const headers = [
{title: 'Email', key: 'email'},
{title: 'Latest Activity', key: 'activity', value: (item: components["schemas"]["UserDto"]) => latestActivity[item.id]},
{title: 'Roles', value: 'roles', sortable: false},
{title: 'Actions', key: 'actions', align: 'end', sortable: false},
{ title: 'Email', key: 'email' },
{
title: 'Latest Activity',
key: 'activity',
value: (item: components['schemas']['UserDto']) => latestActivity[item.id],
},
{ title: 'Roles', value: 'roles', sortable: false },
{ title: 'Actions', key: 'actions', align: 'end', sortable: false },
] as const // workaround for https://github.com/vuetifyjs/vuetify/issues/18901
function getRoleColor(role: UserRoles) {
if(role === UserRoles.ADMIN) return 'error'
if (role === UserRoles.ADMIN) return 'error'
}
// store each user's latest activity in a map
// when the 'users' change, we call the API for each user
const latestActivity: Record<string, Date | undefined> = reactive({})
function getLatestActivity(userId: string) {
komgaClient.GET('/api/v2/users/{id}/authentication-activity/latest', {
params: {
path: { id: userId }
}
})
komgaClient
.GET('/api/v2/users/{id}/authentication-activity/latest', {
params: {
path: { id: userId },
},
})
// unwrap the openapi-fetch structure on success
.then((res) => latestActivity[userId] = res.data?.dateTime)
.then((res) => (latestActivity[userId] = res.data?.dateTime))
.catch(() => {})
}
watch(users, (users) => {
if(users) for (const user of users) {
getLatestActivity(user.id)
}
if (users)
for (const user of users) {
getLatestActivity(user.id)
}
})
onMounted(() => refetchUsers())
// Dialogs handling
// stores the user being actioned upon
const userRecord = ref<components["schemas"]["UserDto"]>()
const userRecord = ref<components['schemas']['UserDto']>()
// stores the ongoing action, so we can handle the action when the dialog is closed with changes
const currentAction = ref<ACTION>()
// the record passed to the dialog's form's model
@ -190,17 +196,20 @@ const dialogTitle = ref<string>()
// dynamic component for the dialog's inner form
const dialogComponent = shallowRef<Component>()
const {mutate: mutateCreateUser} = useCreateUser()
const {mutate: mutateUser} = useUpdateUser()
const {mutate: mutateUserPassword} = useUpdateUserPassword()
const {mutate: mutateDeleteUser} = useDeleteUser()
const {data: libraries} = useLibraries()
const { mutate: mutateCreateUser } = useCreateUser()
const { mutate: mutateUser } = useUpdateUser()
const { mutate: mutateUserPassword } = useUpdateUserPassword()
const { mutate: mutateDeleteUser } = useDeleteUser()
const { data: libraries } = useLibraries()
enum ACTION {
ADD, EDIT, DELETE, PASSWORD
ADD,
EDIT,
DELETE,
PASSWORD,
}
function showDialog(action: ACTION, user?: components["schemas"]["UserDto"]) {
function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
currentAction.value = action
switch (action) {
case ACTION.ADD:
@ -213,36 +222,38 @@ function showDialog(action: ACTION, user?: components["schemas"]["UserDto"]) {
sharedLibraries: {
all: true,
// we fill the array with all libraries for a nicer display in the edit dialog
libraryIds: libraries.value?.map(x => x.id) || [],
libraryIds: libraries.value?.map((x) => x.id) || [],
},
ageRestriction: {
age: 0,
restriction: 'NONE',
}
} as components["schemas"]["UserCreationDto"]
break;
},
} as components['schemas']['UserCreationDto']
break
case ACTION.EDIT:
dialogTitle.value = 'Edit User'
dialogComponent.value = FormUserEdit
dialogRecord.value = {
...user,
roles: user?.roles.filter(x => x !== 'USER'),
roles: user?.roles.filter((x) => x !== 'USER'),
sharedLibraries: {
all: user?.sharedAllLibraries,
// we fill the array with all libraries for a nicer display in the edit dialog
libraryIds: user?.sharedAllLibraries ? libraries.value?.map(x => x.id) || [] : user?.sharedLibrariesIds,
libraryIds: user?.sharedAllLibraries
? libraries.value?.map((x) => x.id) || []
: user?.sharedLibrariesIds,
},
ageRestriction: user?.ageRestriction || {
age: 0,
restriction: 'NONE',
}
} as components["schemas"]["UserUpdateDto"]
break;
},
} as components['schemas']['UserUpdateDto']
break
case ACTION.DELETE:
dialogTitle.value = 'Delete User'
dialogComponent.value = FormUserEdit
dialogRecord.value = user
break;
break
case ACTION.PASSWORD:
dialogTitle.value = 'Change Password'
dialogComponent.value = FormUserChangePassword
@ -255,20 +266,20 @@ function showDialog(action: ACTION, user?: components["schemas"]["UserDto"]) {
function handleDialogConfirmation() {
switch (currentAction.value) {
case ACTION.ADD:
mutateCreateUser(dialogRecord.value as components["schemas"]["UserCreationDto"])
break;
mutateCreateUser(dialogRecord.value as components['schemas']['UserCreationDto'])
break
case ACTION.EDIT:
mutateUser(dialogRecord.value as components["schemas"]["UserDto"])
break;
mutateUser(dialogRecord.value as components['schemas']['UserDto'])
break
case ACTION.DELETE:
mutateDeleteUser(userRecord.value!.id)
break;
break
case ACTION.PASSWORD:
mutateUserPassword({
userId: userRecord.value!.id,
newPassword: dialogRecord.value as string,
})
break;
break
}
}
</script>

View file

@ -9,22 +9,20 @@
</template>
<script lang="ts" setup>
import {useCurrentUser} from '@/colada/queries/current-user'
import { useCurrentUser } from '@/colada/queries/current-user'
async function checkAuthenticated() {
const router = useRouter()
const route = useRoute()
const {data, error, refresh} = useCurrentUser()
const { data, error, refresh } = useCurrentUser()
await refresh()
if (data.value) {
if(route.query.redirect)
await router.push({path: route.query.redirect.toString()})
else
await router.push('/')
if (route.query.redirect) await router.push({ path: route.query.redirect.toString() })
else await router.push('/')
}
if (error.value) {
await router.push({name: '/login', query: {redirect: route.query.redirect}})
await router.push({ name: '/login', query: { redirect: route.query.redirect } })
}
}

View file

@ -5,18 +5,18 @@
*/
// Plugins
import {vuetify, vuetifyRulesPlugin} from './vuetify'
import { vuetify, vuetifyRulesPlugin } from './vuetify'
import pinia from '../stores'
import router from '../router'
import {PiniaColada} from '@pinia/colada'
import { PiniaColada } from '@pinia/colada'
import { PiniaColadaAutoRefetch } from '@pinia/colada-plugin-auto-refetch'
import {vueIntl} from '@/plugins/vue-intl.ts'
import { vueIntl } from '@/plugins/vue-intl'
// Types
import type {App} from 'vue'
import type { App } from 'vue'
// Navigation guards
import {useLoginGuard} from '@/router/login-guard'
import {useRoleGuard} from '@/router/role-guard.ts'
import { useLoginGuard } from '@/router/login-guard'
import { useRoleGuard } from '@/router/role-guard'
export function registerPlugins(app: App) {
app
@ -27,9 +27,7 @@ export function registerPlugins(app: App) {
.use(router)
.use(pinia)
.use(PiniaColada, {
plugins: [
PiniaColadaAutoRefetch()
]
plugins: [PiniaColadaAutoRefetch()],
})
// register navigation guards

View file

@ -1,10 +1,10 @@
import {createIntl} from 'vue-intl'
import {currentLocale, defaultLocale, loadLocale} from '@/utils/locale-helper'
import { createIntl } from 'vue-intl'
import { currentLocale, defaultLocale, loadLocale } from '@/utils/i18n/locale-helper'
const messages = loadLocale(currentLocale)
export const vueIntl = createIntl({
locale: currentLocale,
defaultLocale: defaultLocale,
messages
messages,
})

View file

@ -9,27 +9,26 @@ import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import {createVuetify} from 'vuetify'
import {md3} from 'vuetify/blueprints'
import { createVuetify } from 'vuetify'
import { md3 } from 'vuetify/blueprints'
// Labs
import {VIconBtn} from 'vuetify/labs/components'
import {createRulesPlugin} from 'vuetify/labs/rules'
import { VIconBtn } from 'vuetify/labs/components'
import { createRulesPlugin } from 'vuetify/labs/rules'
import {availableLocales, currentLocale, defaultLocale} from '@/utils/locale-helper'
import { availableLocales, currentLocale, defaultLocale } from '@/utils/i18n/locale-helper'
// load vuetify locales only for the available locales in i18n
async function loadVuetifyLocale(locale: string) {
return await import(`../../node_modules/vuetify/lib/locale/${locale}.js`)
}
const messages: Record<string, string> = {};
void (async()=>{
for (const locale of Object.keys(availableLocales)) {
messages[locale] = (await loadVuetifyLocale(locale)).default
}
})();
const messages: Record<string, string> = {}
void (async () => {
for (const locale of Object.keys(availableLocales)) {
messages[locale] = (await loadVuetifyLocale(locale)).default
}
})()
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export const vuetify = createVuetify({
@ -65,10 +64,13 @@ export const vuetify = createVuetify({
},
})
export const vuetifyRulesPlugin = createRulesPlugin({
aliases: {
sameAs: (other?: string, err?: string) => {
return (v: unknown) => other === v || err || 'Field must have the same value'
export const vuetifyRulesPlugin = createRulesPlugin(
{
aliases: {
sameAs: (other?: string, err?: string) => {
return (v: unknown) => other === v || err || 'Field must have the same value'
},
},
},
}, vuetify.locale)
vuetify.locale,
)

View file

@ -5,9 +5,9 @@
*/
// Composables
import {createRouter, createWebHistory} from 'vue-router/auto'
import {setupLayouts} from 'virtual:generated-layouts'
import {routes} from 'vue-router/auto-routes'
import { createRouter, createWebHistory } from 'vue-router/auto'
import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),

View file

@ -1,5 +1,5 @@
import type {Router} from 'vue-router'
import {useCurrentUser} from '@/colada/queries/current-user'
import type { Router } from 'vue-router'
import { useCurrentUser } from '@/colada/queries/current-user'
// check if the user is authenticated before navigating to any page
// the authentication is cached by Pinia Colada
@ -7,11 +7,11 @@ import {useCurrentUser} from '@/colada/queries/current-user'
export function useLoginGuard(router: Router) {
router.beforeEach((to) => {
if (!to.meta.noAuth) {
const {data} = useCurrentUser()
const { data } = useCurrentUser()
const authenticated = data.value
if(!authenticated) {
const query = Object.assign({}, to.query, {redirect: to.fullPath})
return {name: '/startup', query: query}
if (!authenticated) {
const query = Object.assign({}, to.query, { redirect: to.fullPath })
return { name: '/startup', query: query }
}
}
})

View file

@ -1,5 +1,5 @@
import type {Router} from 'vue-router'
import {useCurrentUser} from '@/colada/queries/current-user'
import type { Router } from 'vue-router'
import { useCurrentUser } from '@/colada/queries/current-user'
// check if the user has the necessary role before navigating to restricted pages
// the authentication is cached by Pinia Colada
@ -7,9 +7,9 @@ import {useCurrentUser} from '@/colada/queries/current-user'
export function useRoleGuard(router: Router) {
router.beforeEach((to) => {
if (to.meta.requiresRole) {
const {data} = useCurrentUser()
if(!data.value?.roles?.includes(to.meta.requiresRole)) {
return {name: '/'}
const { data } = useCurrentUser()
if (!data.value?.roles?.includes(to.meta.requiresRole)) {
return { name: '/' }
}
}
})

View file

@ -1,6 +1,6 @@
// Utilities
import {defineStore} from 'pinia'
import {useDisplay} from 'vuetify'
import { defineStore } from 'pinia'
import { useDisplay } from 'vuetify'
export const useAppStore = defineStore('app', {
state: () => ({

View file

@ -8,7 +8,7 @@ html {
.link-underline {
text-decoration: none;
color: var(--v-anchor-base)
color: var(--v-anchor-base);
}
.link-underline:hover {

View file

@ -8,4 +8,3 @@
// @use 'vuetify/settings' with (
// $reset: true
// );

View file

@ -1,22 +1,22 @@
export interface ActuatorInfo {
git: ActuatorGit,
git: ActuatorGit
build: ActuatorBuild
}
export interface ActuatorGit {
commit: ActuatorGitCommit,
commit: ActuatorGitCommit
branch: string
}
export interface ActuatorGitCommit {
time: Date,
time: Date
id: string
}
export interface ActuatorBuild {
version: string,
artifact: string,
name: string,
group: string,
version: string
artifact: string
name: string
group: string
time: Date
}

View file

@ -2,7 +2,7 @@
// It can also be added to a `.d.ts` file. Make sure it's included in
// project's tsconfig.json "files"
import 'vue-router'
import type {UserRoles} from '@/types/UserRoles.ts'
import type { UserRoles } from '@/types/UserRoles'
// To ensure it is treated as a module, add at least one `export` statement
export {}

View file

@ -3,5 +3,5 @@ export enum UserRoles {
FILE_DOWNLOAD = 'FILE_DOWNLOAD',
PAGE_STREAMING = 'PAGE_STREAMING',
KOBO_SYNC = 'KOBO_SYNC',
KOREADER_SYNC = 'KOREADER_SYNC'
KOREADER_SYNC = 'KOREADER_SYNC',
}

View file

@ -1,14 +0,0 @@
import {defineMessage} from 'vue-intl'
export const commonMessages = {
somethingWentWrongTitle: defineMessage({
description: 'Common message: an error happened while loading data',
defaultMessage: 'Something went wrong',
id: 'ixQlWv',
}),
somethingWentWrongSubTitle: defineMessage({
description: 'Common message: an error happened while loading data, explanation',
defaultMessage: 'There might be a problem with your connection or your server.',
id: 'hYO2n6',
}),
}

View file

@ -0,0 +1,14 @@
import { defineMessage } from 'vue-intl'
export const commonMessages = {
somethingWentWrongTitle: defineMessage({
description: 'Common message: an error happened while loading data',
defaultMessage: 'Something went wrong',
id: 'ixQlWv',
}),
somethingWentWrongSubTitle: defineMessage({
description: 'Common message: an error happened while loading data, explanation',
defaultMessage: 'There might be a problem with your connection or your server.',
id: 'hYO2n6',
}),
}

View file

@ -1,12 +1,13 @@
import {defineMessage} from 'vue-intl'
import { defineMessage } from 'vue-intl'
import localeMessages from '../i18n?dir2json&ext=.json&1'
export const defaultLocale = 'en'
const localeName = defineMessage({
description: 'The name of the locale, shown in the language selection menu. Must be translated to the language\'s name',
description:
"The name of the locale, shown in the language selection menu. Must be translated to the language's name",
defaultMessage: 'English',
id: 'app.locale-name'
id: 'app.locale-name',
})
/**
@ -16,14 +17,16 @@ const localeName = defineMessage({
*/
export function loadLocale(locale: string): Record<string, string> {
const localeToLoad = locale in availableLocales ? locale : defaultLocale
return (localeMessages as unknown as Record<string, Record<string, string>>)[localeToLoad]!;
return (localeMessages as unknown as Record<string, Record<string, string>>)[localeToLoad]!
}
function loadAvailableLocales(): Record<string, string> {
const localesInfo: Record<string, string> = {}
Object.keys(localeMessages).forEach(x =>
localesInfo[x] = (localeMessages as unknown as Record<string, Record<string, string>>)[x]![localeName.id]!
Object.keys(localeMessages).forEach(
(x) =>
(localesInfo[x] = (localeMessages as unknown as Record<string, Record<string, string>>)[x]![
localeName.id
]!),
)
return localesInfo
}
@ -51,7 +54,7 @@ export const currentLocale = getLocale()
* @param locale the new locale
*/
export function setLocale(locale: string) {
if(locale !== currentLocale) {
if (locale !== currentLocale) {
localStorage.setItem('userLocale', locale)
window.location.reload()
}

View file

@ -0,0 +1 @@
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms))

View file

@ -3,17 +3,28 @@
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./dir2json.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": false,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true, // openapi-ts
"allowImportingTsExtensions":true,
"target": "ESNext",
"moduleResolution": "bundler", // Komga: https://uvr.esm.is/introduction.html#setup
"noUncheckedIndexedAccess": true, // Komga: https://openapi-ts.dev/introduction
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["vite-plugin-vue-layouts-next/client"],
"types": [
"unplugin-vue-router/client", // Komga: https://uvr.esm.is/introduction.html#setup
"vite-plugin-vue-layouts-next/client" // Komga: https://github.com/loicduong/vite-plugin-vue-layouts-next#client-types
],
"skipLibCheck": true,
"isolatedModules": true,
"module": "preserve",
"noEmit": true,
"strict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noImplicitOverride": true,
"allowImportingTsExtensions": false,
"exactOptionalPropertyTypes": true,
"lib": ["esnext", "dom", "dom.iterable"]
}
}