next-ui wip

This commit is contained in:
Gauthier Roebroeck 2025-05-20 10:55:39 +08:00
parent ced89c5c54
commit a4ea8eeab3
91 changed files with 18126 additions and 1 deletions

2
.nvmrc
View file

@ -1 +1 @@
18
22

4
next-ui/.browserslistrc Normal file
View file

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

6
next-ui/.editorconfig Normal file
View file

@ -0,0 +1,6 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

View file

@ -0,0 +1,78 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"DirectiveBinding": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"onWatcherCleanup": true,
"useId": true,
"useModel": true,
"useTemplateRef": true,
"Slot": true,
"Slots": true
}
}

22
next-ui/.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
next-ui/env.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />
/// <reference types="vite-plugin-vue-layouts/client" />

36
next-ui/eslint.config.js Normal file
View file

@ -0,0 +1,36 @@
/**
* .eslint.js
*
* ESLint configuration file.
*/
import pluginVue from 'eslint-plugin-vue'
import vueTsEslintConfig from '@vue/eslint-config-typescript'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
...pluginVue.configs['flat/recommended'],
...vueTsEslintConfig(),
{
rules: {
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
},
],
'vue/multi-word-component-names': 'off',
}
}
]

13
next-ui/index.html Normal file
View file

@ -0,0 +1,13 @@
<!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>
</html>

View file

@ -0,0 +1,26 @@
import fs from "node:fs";
import openapiTS, { astToString } from "openapi-typescript";
import ts from "typescript";
// From https://openapi-ts.dev/node
// We use the Node.js API as the CLI does not support Date types
const mySchema = new URL("../komga/docs/openapi.json", import.meta.url)
const DATE = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Date")); // `Date`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
const ast = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "date-time") {
return schemaObject.nullable
? ts.factory.createUnionTypeNode([DATE, NULL])
: DATE;
}
},
});
const contents = astToString(ast);
// (optional) write to file
fs.writeFileSync("./src/generated/openapi/komga.d.ts", contents);

6446
next-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

52
next-ui/package.json Normal file
View file

@ -0,0 +1,52 @@
{
"name": "next-ui",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --fix",
"openapi-generate": "npx tsx ./openapi-generator.ts"
},
"dependencies": {
"@pinia/colada": "^0.15.3",
"@pinia/colada-plugin-auto-refetch": "^0.0.6",
"@vueuse/core": "^13.1.0",
"core-js": "^3.37.1",
"marked": "^15.0.11",
"openapi-fetch": "^0.14.0",
"pinia-plugin-persistedstate": "^4.3.0",
"vue": "^3.4.31",
"vuetify": "^3.6.14"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
"@mdi/font": "7.4.47",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.9.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^10.1.0",
"npm-run-all2": "^8.0.1",
"openapi-typescript": "^7.8.0",
"pinia": "^3.0.2",
"sass": "1.77.8",
"sass-embedded": "^1.77.8",
"typescript": "^5.8.3",
"unplugin-auto-import": "^19.2.0",
"unplugin-fonts": "^1.1.1",
"unplugin-vue-components": "^28.5.0",
"unplugin-vue-router": "^0.12.0",
"vite": "^6.3.5",
"vite-plugin-vue-layouts-next": "^0.1.1",
"vite-plugin-vuetify": "^2.0.3",
"vue-router": "^4.4.0",
"vue-tsc": "^2.1.10"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
next-ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

32
next-ui/src/App.vue Normal file
View file

@ -0,0 +1,32 @@
<template>
<v-app>
<router-view />
</v-app>
</template>
<script lang="ts" setup>
import { useTheme } from 'vuetify'
import {useAppStore} from '@/stores/app'
import { usePreferredDark } from '@vueuse/core'
const appStore = useAppStore()
const theme = useTheme()
const prefersDark = usePreferredDark()
function updateTheme(selectedTheme: string, prefersDark: boolean) {
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))
// trigger an update on startup to get the proper theme loaded
updateTheme(appStore.theme, prefersDark.value)
</script>
<style>
@import "styles/global.scss";
</style>

View file

@ -0,0 +1,24 @@
import type {Middleware} from 'openapi-fetch'
import createClient from 'openapi-fetch'
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}) {
if (!response.ok)
throw new Error(`${response.url}: ${response.status} ${response.statusText}`)
// return response untouched
return undefined
},
}
const client = createClient<paths>({
baseUrl: 'http://localhost:8080',
// required to pass the session cookie on all requests
credentials: 'include',
// required to avoid browser basic-auth popups
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
client.use(coladaMiddleware)
export const komgaClient = client

113
next-ui/src/assets/logo.svg Normal file
View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="512pt"
viewBox="0 0 512 512"
width="512pt"
version="1.1"
id="svg4586"
sodipodi:docname="komga - Copy.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata4592">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4590">
<linearGradient
id="linearGradient6082"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop6080"/>
</linearGradient>
<linearGradient
id="linearGradient6076"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop6074"/>
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6082"
id="linearGradient6084"
x1="77.866812"
y1="386.00679"
x2="217.20259"
y2="386.00679"
gradientUnits="userSpaceOnUse"/>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1656"
inkscape:window-height="1368"
id="namedview4588"
showgrid="false"
inkscape:zoom="1.2512475"
inkscape:cx="264.73114"
inkscape:cy="305.20589"
inkscape:window-x="-7"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg4586"/>
<path
d="m512 256c0 141.386719-114.613281 256-256 256s-256-114.613281-256-256 114.613281-256 256-256 256 114.613281 256 256zm0 0"
fill="#005ed3"
id="path4556"/>
<path
d="m 512,256 c 0,-11.71094 -0.80469,-23.23047 -2.32422,-34.52344 L 382.48047,94.28125 320.52344,121.85938 256,56.933594 212.69531,131.30469 129.51953,94.28125 141.86719,178.42187 49.949219,193.81641 114.32031,256 l -64.371091,62.18359 82.121091,82.16016 -2.55078,17.375 91.95703,91.95703 C 232.76953,511.19531 244.28906,512 256,512 397.38672,512 512,397.38672 512,256 Z"
id="path4558"
inkscape:connector-curvature="0"
style="fill:#00459f"
sodipodi:nodetypes="scccccccccccccss"/>
<path
d="m256 86.742188 37.109375 63.738281 70.574219-31.414063-10.527344 71.71875 77.078125 12.910156-54.144531 52.304688 54.144531 52.304688-77.078125 12.910156 10.527344 71.71875-70.574219-31.414063-37.109375 63.738281-37.109375-63.738281-70.574219 31.414063 10.527344-71.71875-77.078125-12.910156 54.144531-52.304688-54.144531-52.304688 77.078125-12.910156-10.527344-71.71875 70.574219 31.414063zm0 0"
fill="#ff0335"
id="path4560"/>
<path
d="m430.230469 308.300781-77.070313 12.910157 10.519532 71.71875-70.570313-31.410157-37.109375 63.742188v-338.523438l37.109375 63.742188 70.570313-31.410157-6.757813 46.101563-3.761719 25.617187 58.800782 9.851563 18.269531 3.058594-13.390625 12.929687-40.75 39.371094 11.378906 10.988281zm0 0"
fill="#c2001b"
id="path4562"/>
<path
d="m256 455.066406-43.304688-74.371094-83.175781 37.023438 12.347657-84.140625-91.917969-15.394531 64.371093-62.183594-64.371093-62.183594 91.917969-15.394531-12.347657-84.140625 83.179688 37.023438 43.300781-74.371094 43.304688 74.371094 83.175781-37.023438-12.347657 84.140625 91.917969 15.394531-64.371093 62.183594 64.371093 62.183594-91.917969 15.398437 12.347657 84.136719-83.175781-37.023438zm-30.917969-112.722656 30.917969 53.101562 30.917969-53.101562 57.964843 25.800781-8.703124-59.292969 62.238281-10.425781-43.917969-42.425781 43.917969-42.425781-62.238281-10.425781 8.703124-59.292969-57.964843 25.800781-30.917969-53.101562-30.917969 53.101562-57.964843-25.800781 8.703124 59.292969-62.238281 10.425781 43.917969 42.425781-43.917969 42.425781 62.238281 10.425781-8.703124 59.292969zm0 0"
fill="#ffdf47"
id="path4564"/>
<path
d="m403.308594 261.441406-5.628906-5.441406 25.160156-24.300781 39.210937-37.878907-55.75-9.339843-36.171875-6.058594 2.800782-19.09375 9.550781-65.046875-83.179688 37.019531-43.300781-74.371093v59.621093l30.921875 53.109375 57.957031-25.808594-3.910156 26.667969-2.546875 17.378907-2.242187 15.25 2.480468.421874 59.761719 10.007813-43.921875 42.421875 16.96875 16.390625 26.953125 26.03125-62.242187 10.429687 8.699218 59.296876-57.957031-25.808594-30.921875 53.109375v59.621093l43.300781-74.371093 83.179688 37.019531-12.351563-84.140625 91.921875-15.398437zm0 0"
fill="#fec000"
id="path4566"/>
<g
aria-label="K"
transform="matrix(1.1590846,-0.34467221,0.22789693,0.794981,0,0)"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:296.55969238px;line-height:125%;font-family:Impact;-inkscape-font-specification:Impact;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="text4596">
<path
d="m 220.91497,266.9035 -34.89789,105.85211 38.2284,128.58643 H 161.2555 L 136.63873,400.84769 V 501.34204 H 75.676021 V 266.9035 h 60.962709 v 91.08205 l 27.07845,-91.08205 z"
style="font-size:296.55969238px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path824"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

142
next-ui/src/auto-imports.d.ts vendored Normal file
View file

@ -0,0 +1,142 @@
/* eslint-disable */
/* 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']
}
// 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'
import('vue')
}
// for vue template auto import
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']>
}
}

View file

@ -0,0 +1,16 @@
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'),
onSuccess: () => {
queryCache.invalidateQueries({key: ['current-user']})
},
onError: (error) => {
console.log('logout error', error)
},
})
})

View file

@ -0,0 +1,15 @@
import {useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
import type {ActuatorInfo} from '@/types/Actuator'
export function useActuatorInfo() {
return useQuery({
key: () => ['actuator-info'],
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

@ -0,0 +1,14 @@
import {useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
export function useAppReleases() {
return useQuery({
key: () => ['app-releases'],
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,
})
}

View file

@ -0,0 +1,15 @@
import {useQuery} from '@pinia/colada'
import {komgaClient} from '@/api/komga-client'
export function useCurrentUser() {
return useQuery({
key: () => ['current-user'],
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,
})
}

30
next-ui/src/components.d.ts vendored Normal file
View file

@ -0,0 +1,30 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AppBar: typeof import('./components/app/bar/AppBar.vue')['default']
AppDrawer: typeof import('./components/app/drawer/AppDrawer.vue')['default']
AppDrawerFooter: typeof import('./components/app/drawer/AppDrawerFooter.vue')['default']
AppDrawerMenu: typeof import('./components/app/drawer/AppDrawerMenu.vue')['default']
AppDrawerMenuAccount: typeof import('./components/app/drawer/AppDrawerMenuAccount.vue')['default']
AppDrawerMenuHistory: typeof import('./components/app/drawer/AppDrawerMenuHistory.vue')['default']
AppDrawerMenuImport: typeof import('./components/app/drawer/AppDrawerMenuImport.vue')['default']
AppDrawerMenuLogout: typeof import('./components/app/drawer/AppDrawerMenuLogout.vue')['default']
AppDrawerMenuMedia: typeof import('./components/app/drawer/AppDrawerMenuMedia.vue')['default']
AppDrawerMenuServer: typeof import('./components/app/drawer/AppDrawerMenuServer.vue')['default']
AppFooter: typeof import('./components/AppFooter.vue')['default']
BuidVersion: typeof import('./components/BuidVersion.vue')['default']
BuildCommit: typeof import('./components/BuildCommit.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.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/app/bar/ThemeSelector.vue')['default']
}
}

View file

@ -0,0 +1,82 @@
<template>
<v-footer
height="40"
app
>
<a
v-for="item in items"
:key="item.title"
:href="item.href"
:title="item.title"
class="d-inline-block mx-2 social-link"
rel="noopener noreferrer"
target="_blank"
>
<v-icon
:icon="item.icon"
:size="item.icon === '$vuetify' ? 24 : 16"
/>
</a>
<div
class="text-caption text-disabled"
style="position: absolute; right: 16px;"
>
&copy; 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
<a
class="text-decoration-none on-surface"
href="https://vuetifyjs.com/about/licensing/"
rel="noopener noreferrer"
target="_blank"
>
MIT License
</a>
</div>
</v-footer>
</template>
<script setup lang="ts">
const items = [
{
title: 'Vuetify Documentation',
icon: `$vuetify`,
href: 'https://vuetifyjs.com/',
},
{
title: 'Vuetify Support',
icon: 'mdi-shield-star-outline',
href: 'https://support.vuetifyjs.com/',
},
{
title: 'Vuetify X',
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
href: 'https://x.com/vuetifyjs',
},
{
title: 'Vuetify GitHub',
icon: `mdi-github`,
href: 'https://github.com/vuetifyjs/vuetify',
},
{
title: 'Vuetify Discord',
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
href: 'https://community.vuetifyjs.com/',
},
{
title: 'Vuetify Reddit',
icon: `mdi-reddit`,
href: 'https://reddit.com/r/vuetifyjs',
},
]
</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
&:hover
color: rgba(25, 118, 210, 1)
</style>

View file

@ -0,0 +1,20 @@
<template>
<template v-if="buildVersion">
<v-btn
prepend-icon="mdi-tag-outline"
variant="text"
color="grey"
size="small"
class="text-caption"
to="/server/updates"
>
{{ buildVersion }}
</v-btn>
</template>
</template>
<script setup lang="ts">
import {useBuildVersion} from '@/composables/buid-version.ts'
const {buildVersion} = useBuildVersion()
</script>

View file

@ -0,0 +1,23 @@
<template>
<template v-if="commit">
<v-btn
prepend-icon="mdi-source-commit"
variant="text"
color="grey"
size="small"
class="text-caption"
:href="'https://github.com/gotson/komga/commits/' + commit"
target="_blank"
>
{{ commit }}
</v-btn>
</template>
</template>
<script setup lang="ts">
import {useActuatorInfo} from '@/colada/queries/actuator-info'
const {data} = useActuatorInfo()
const commit = computed(() => data.value?.git?.commit?.id)
</script>

View file

@ -0,0 +1,189 @@
<template>
<v-container class="fill-height">
<v-responsive
class="align-centerfill-height mx-auto"
max-width="900"
>
<v-img
class="mb-4"
height="150"
src="@/assets/logo.svg"
/>
<div class="text-center">
<div class="text-body-2">
Welcome "{{ currentUser?.email }}"
</div>
<VBtn @click="performLogout">
Logout
</VBtn>
</div>
<div class="py-4" />
<v-row>
<v-col cols="12">
<v-card
class="py-4"
color="surface-variant"
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
prepend-icon="mdi-rocket-launch-outline"
rounded="lg"
variant="outlined"
>
<template #image>
<v-img position="top right" />
</template>
<template #title>
<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/>
` }}
</v-kbd>
in
<v-kbd>pages/index.vue</v-kbd>
.
</div>
</template>
<v-overlay
opacity=".12"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/"
prepend-icon="mdi-text-box-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Learn about all things Vuetify in our documentation."
target="_blank"
title="Documentation"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
prepend-icon="mdi-star-circle-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Explore available framework Features."
target="_blank"
title="Features"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/components/all"
prepend-icon="mdi-widgets-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Discover components in the API Explorer."
target="_blank"
title="Components"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://discord.vuetifyjs.com"
prepend-icon="mdi-account-group-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Connect with Vuetify developers."
target="_blank"
title="Community"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
</v-row>
</v-responsive>
</v-container>
</template>
<script setup lang="ts">
import {useCurrentUser} from '@/colada/queries/current-user'
import {useLogout} from '@/colada/mutations/logout'
const {data: currentUser} = useCurrentUser()
const router = useRouter()
const {mutateAsync: logoutAsync} = useLogout()
async function performLogout() {
logoutAsync().then(() => router.push('/login'))
}
</script>
<script lang="ts">
</script>

View file

@ -0,0 +1,97 @@
<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="Email"
autofocus
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="password"
label="Password"
type="password"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-checkbox
v-model="rememberMe"
label="Remember me"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
text="Sign in"
@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()
async 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)
router.push({path: route.query.redirect.toString()})
else
router.push('/')
},
onError: (error) => {
//TODO: handle error
},
})
mutate()
}
</script>
<script lang="ts">
</script>
<style scoped>
</style>

View file

@ -0,0 +1,35 @@
# Components
Vue template files in this folder are automatically imported.
## 🚀 Usage
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
The following example assumes a component located at `src/components/MyComponent.vue`:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
//
</script>
```
When your template is rendered, the component's import will automatically be inlined, which renders to this:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
import MyComponent from '@/components/MyComponent.vue'
</script>
```

View file

@ -0,0 +1,29 @@
<template>
<v-app-bar elevation="2">
<template #prepend>
<v-app-bar-nav-icon @click="appStore.drawer = !appStore.drawer" />
</template>
<v-app-bar-title>
<RouterLink to="/">
<v-avatar>
<v-img src="@/assets/logo.svg" />
</v-avatar>
</RouterLink>
Komga
</v-app-bar-title>
<ThemeSelector />
</v-app-bar>
</template>
<script setup lang="ts">
import {useAppStore} from '@/stores/app'
const appStore = useAppStore()
</script>
<script lang="ts">
</script>
<style scoped>
</style>

View file

@ -0,0 +1,55 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn
v-bind="props"
:icon="themeIcon"
/>
</template>
<v-list>
<v-list-item
v-for="theme in themes"
:key="theme.value"
:prepend-icon="theme.icon"
:title="theme.title"
@click="appStore.theme = theme.value"
/>
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import {useAppStore} from '@/stores/app'
const appStore = useAppStore()
const themes= [
{
title: 'Light',
value: 'light',
icon: 'mdi-brightness-7'
},
{
title: 'Dark',
value: 'dark',
icon: 'mdi-brightness-3'
},
{
title: 'System',
value: 'system',
icon: 'mdi-brightness-auto'
},
]
const themeIcon = computed(
() => themes.find(x => x.value === appStore.theme)?.icon || 'mdi-brightness-auto'
)
</script>
<script lang="ts">
</script>
<style scoped>
</style>

View file

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

View file

@ -0,0 +1,18 @@
<template>
<template v-if="isAdmin">
<v-divider />
<div class="d-flex align-center text-caption text-medium-emphasis pa-2">
<div class="d-flex ms-auto">
<BuildCommit class="me-2" />
<BuidVersion />
</div>
</div>
</template>
</template>
<script setup lang="ts">
import {useCurrentUserRole} from '@/composables/current-user-role.ts'
import {UserRoles} from '@/types/UserRoles.ts'
const {hasRole: isAdmin} = useCurrentUserRole(UserRoles.ADMIN)
</script>

View file

@ -0,0 +1,17 @@
<template>
<v-list nav>
<AppDrawerMenuImport v-if="isAdmin" />
<AppDrawerMenuMedia v-if="isAdmin" />
<AppDrawerMenuHistory v-if="isAdmin" />
<AppDrawerMenuServer v-if="isAdmin" />
<AppDrawerMenuAccount />
<AppDrawerMenuLogout />
</v-list>
</template>
<script setup lang="ts">
import {useCurrentUserRole} from '@/composables/current-user-role.ts'
import {UserRoles} from '@/types/UserRoles.ts'
const {hasRole: isAdmin} = useCurrentUserRole(UserRoles.ADMIN)
</script>

View file

@ -0,0 +1,38 @@
<template>
<v-list-group value=" My Account">
<template #activator="{ props }">
<v-list-item
v-bind="props"
title="My Account"
prepend-icon="mdi-account"
/>
</template>
<v-list-item
to="/account/details"
title="Details"
/>
<v-list-item
to="/account/api-keys"
title="API Keys"
/>
<v-list-item
to="/account/ui"
title="User Interface"
/>
<v-list-item
to="/account/activity"
title="Activity"
/>
</v-list-group>
</template>
<script setup lang="ts">
</script>
<script lang="ts">
</script>
<style scoped>
</style>

View file

@ -0,0 +1,7 @@
<template>
<v-list-item
to="/history"
title="History"
prepend-icon="mdi-clock-time-four-outline"
/>
</template>

View file

@ -0,0 +1,21 @@
<template>
<v-list-group value="Import">
<template #activator="{ props }">
<v-list-item
v-bind="props"
title="Import"
prepend-icon="mdi-import"
/>
</template>
<v-list-item
to="/import/books"
title="Books"
/>
<v-list-item
to="/import/readlist"
title="Read List"
/>
</v-list-group>
</template>

View file

@ -0,0 +1,18 @@
<template>
<v-list-item
title="Logout"
prepend-icon="mdi-power"
@click="performLogout"
/>
</template>
<script setup lang="ts">
import {useLogout} from '@/colada/mutations/logout'
const router = useRouter()
const {mutateAsync: logoutAsync} = useLogout()
async function performLogout() {
logoutAsync().then(() => router.push('/login'))
}
</script>

View file

@ -0,0 +1,44 @@
<template>
<v-list-group value="Media">
<template #activator="{ props }">
<v-list-item
v-bind="props"
title="Media"
prepend-icon="mdi-book-cog"
/>
</template>
<v-list-item
to="/media/analysis"
title="Media Analysis"
/>
<v-list-item
to="/media/missing-posters"
title="Missing Posters"
/>
<v-list-item
to="/media/duplicate-files"
title="Duplicate Files"
/>
<v-list-group value="Duplicate Pages">
<template #activator="{ props }">
<v-list-item
v-bind="props"
title="Duplicate Pages"
/>
</template>
<v-list-item
to="/media/duplicate-pages/known"
title="Known"
/>
<v-list-item
to="/media/duplicate-pages/unknown"
title="Unknown"
/>
</v-list-group>
</v-list-group>
</template>
<script setup lang="ts">
</script>

View file

@ -0,0 +1,36 @@
<template>
<v-list-group value="Server">
<template #activator="{ props }">
<v-list-item
v-bind="props"
title="Server"
prepend-icon="mdi-cog"
/>
</template>
<v-list-item
to="/server/users"
title="Users"
/>
<v-list-item
to="/server/settings"
title="Settings"
/>
<v-list-item
to="/server/ui"
title="User Interface"
/>
<v-list-item
to="/server/metrics"
title="Metrics"
/>
<v-list-item
to="/server/announcements"
title="Announcements"
/>
<v-list-item
to="/server/updates"
title="Updates"
/>
</v-list-group>
</template>

View file

@ -0,0 +1,9 @@
import {useActuatorInfo} from '@/colada/queries/actuator-info'
export function useBuildVersion() {
const {data} = useActuatorInfo()
const buildVersion = computed(() => data.value?.build?.version)
return {buildVersion}
}

View file

@ -0,0 +1,10 @@
import {useCurrentUser} from '@/colada/queries/current-user.ts'
import {UserRoles} from '@/types/UserRoles.ts'
export function useCurrentUserRole(role: UserRoles) {
const {data} = useCurrentUser()
const hasRole = computed(() => data.value?.roles.includes(role))
return {hasRole}
}

View file

@ -0,0 +1,17 @@
import {useAppReleases} from '@/colada/queries/app-releases.ts'
import {useBuildVersion} from '@/composables/buid-version.ts'
export function useLatestVersion() {
const {data, } = useAppReleases()
const {buildVersion} = useBuildVersion()
const latestRelease = computed(() => data.value?.find(x => x.latest))
const isLatestVersion = computed(() => {
if(buildVersion.value && latestRelease.value)
return buildVersion.value == latestRelease.value?.version
else return undefined
})
return {isLatestVersion}
}

9401
next-ui/src/generated/openapi/komga.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
# Layouts
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts-next](https://github.com/loicduong/vite-plugin-vue-layouts-next) repository.

View file

@ -0,0 +1,20 @@
<template>
<AppBar />
<AppDrawer />
<v-main scrollable>
<v-container
fluid
class="pa-6"
>
<router-view />
</v-container>
</v-main>
<AppFooter />
</template>
<script lang="ts" setup>
</script>

View file

@ -0,0 +1,9 @@
<template>
<v-main>
<router-view />
</v-main>
</template>
<script lang="ts" setup>
//
</script>

20
next-ui/src/main.ts Normal file
View file

@ -0,0 +1,20 @@
/**
* main.ts
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import {registerPlugins} from '@/plugins'
// Components
import App from './App.vue'
// Composables
import {createApp} from 'vue'
const app = createApp(App)
registerPlugins(app)
app.mount('#app')

View file

@ -0,0 +1,5 @@
# Pages
Vue components created in this folder will automatically be converted to navigable routes.
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.

View file

@ -0,0 +1,7 @@
<template>
<h1>Authentication Activity</h1>
</template>
<script lang="ts" setup>
//
</script>

View file

@ -0,0 +1,7 @@
<template>
<h1>API Keys</h1>
</template>
<script lang="ts" setup>
//
</script>

View file

@ -0,0 +1,7 @@
<template>
<h1>Account Details</h1>
</template>
<script lang="ts" setup>
//
</script>

View file

@ -0,0 +1,7 @@
<template>
<h1>UI</h1>
</template>
<script lang="ts" setup>
//
</script>

View file

@ -0,0 +1,12 @@
<template>
<h1>History</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Import Books</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Import Read List</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,7 @@
<template>
<HelloWorld />
</template>
<script lang="ts" setup>
//
</script>

View file

@ -0,0 +1,13 @@
<template>
<LoginForm />
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
layout: single
noAuth: true
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Media Analysis</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Duplicate Files</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Known</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Unknown</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Missing Posters</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Announcements</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Metrics</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Settings</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>UI</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,123 @@
<template>
<v-alert
v-if="error"
type="error"
variant="tonal"
>
Error loading data
</v-alert>
<template v-if="data">
<v-row>
<v-col>
<div v-if="isLatestVersion == true">
<v-alert
type="success"
variant="tonal"
>
The latest version of Komga is already installed
</v-alert>
</div>
<div v-if="isLatestVersion == false">
<v-alert
type="warning"
variant="tonal"
>
Updates are available
</v-alert>
</div>
</v-col>
</v-row>
<div
v-for="(release, index) in data"
:key="index"
>
<v-row
justify="space-between"
align="center"
>
<v-col cols="auto">
<div>
<a
:href="release.url"
target="_blank"
class="text-h4 font-weight-medium link-underline me-2"
>{{
release.version
}}</a>
<v-chip
v-if="release.version == currentVersion"
class="mx-2 mt-n3"
size="small"
label
color="info"
>
Currently installed
</v-chip>
<v-chip
v-if="release.version == latest?.version"
class="mx-2 mt-n3"
size="small"
label
>
Latest
</v-chip>
</div>
<!-- TODO: i18n the date -->
<div class="mt-2 subtitle-1">
{{ release.releaseDate }}
</div>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<!-- eslint-disable vue/no-v-html -->
<div
class="releases"
v-html="marked(release.description)"
/>
<!-- eslint-enable vue/no-v-html -->
</v-col>
</v-row>
<v-divider
v-if="index != data.length - 1"
class="my-8"
/>
</div>
</template>
</template>
<script lang="ts" setup>
import {useAppReleases} from '@/colada/queries/app-releases.ts'
import {marked} from 'marked'
import {useBuildVersion} from '@/composables/buid-version.ts'
import {useLatestVersion} from '@/composables/latest-version.ts'
const {data, error} = useAppReleases()
const latest = computed(() => data.value?.find(x => x.latest))
const {buildVersion: currentVersion} = useBuildVersion()
const {isLatestVersion} = useLatestVersion()
</script>
<style lang="scss">
.releases p {
margin-bottom: 16px;
}
.releases ul {
padding-left: 24px;
}
.releases a {
color: var(--v-anchor-base);
}
</style>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,12 @@
<template>
<h1>Users</h1>
</template>
<script lang="ts" setup>
//
</script>
<route lang="yaml">
meta:
requiresRole: ADMIN
</route>

View file

@ -0,0 +1,41 @@
<template>
<v-container max-width="550px">
<v-row justify="center">
<v-col>
<v-img src="@/assets/logo.svg" />
</v-col>
</v-row>
</v-container>
</template>
<script lang="ts" setup>
import {useCurrentUser} from '@/colada/queries/current-user'
import {onMounted} from 'vue'
async function checkAuthenticated() {
const router = useRouter()
const route = useRoute()
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 (error.value) {
await router.push({name: '/login', query: {redirect: route.query.redirect}})
}
}
onMounted(() => checkAuthenticated())
// TODO: exchange header token for cookie
</script>
<route lang="yaml">
meta:
layout: single
noAuth: true
</route>

View file

@ -0,0 +1,3 @@
# Plugins
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.

View file

@ -0,0 +1,36 @@
/**
* plugins/index.ts
*
* Automatically included in `./src/main.ts`
*/
// Plugins
import vuetify from './vuetify'
import pinia from '../stores'
import router from '../router'
import {PiniaColada} from '@pinia/colada'
import { PiniaColadaAutoRefetch } from '@pinia/colada-plugin-auto-refetch'
// Types
import type {App} from 'vue'
// Navigation guards
import {useLoginGuard} from '@/router/login-guard'
import {useRoleGuard} from '@/router/role-guard.ts'
export function registerPlugins(app: App) {
app
.use(vuetify)
// .use(DataLoaderPlugin, {router})
.use(router)
.use(pinia)
.use(PiniaColada, {
plugins: [
PiniaColadaAutoRefetch()
]
})
// register navigation guards
useLoginGuard(router)
useRoleGuard(router)
}

View file

@ -0,0 +1,40 @@
/**
* plugins/vuetify.ts
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import {createVuetify} from 'vuetify'
import {md3} from 'vuetify/blueprints'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
defaultTheme: 'light',
themes: {
light: {
dark: false,
colors: {
primary: '#005ed3',
secondary: '#fec000',
accent: '#ff0335',
},
},
dark: {
dark: true,
colors: {
primary: '#78baec',
secondary: '#fec000',
accent: '#ff0335',
},
},
},
},
blueprint: md3,
})

View file

@ -0,0 +1,36 @@
/**
* router/index.ts
*
* Automatic routes for `./src/pages/*.vue`
*/
// Composables
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),
routes: setupLayouts(routes),
})
// Workaround for https://github.com/vitejs/vite/issues/11804
router.onError((err, to) => {
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
if (!localStorage.getItem('vuetify:dynamic-reload')) {
console.log('Reloading page to fix dynamic import error')
localStorage.setItem('vuetify:dynamic-reload', 'true')
location.assign(to.fullPath)
} else {
console.error('Dynamic import error, reloading page did not fix it', err)
}
} else {
console.error(err)
}
})
router.isReady().then(() => {
localStorage.removeItem('vuetify:dynamic-reload')
})
export default router

View file

@ -0,0 +1,18 @@
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
// redirect to the startup page if not authenticated
export function useLoginGuard(router: Router) {
router.beforeEach((to) => {
if (!to.meta.noAuth) {
const {data} = useCurrentUser()
const authenticated = data.value
if(!authenticated) {
const query = Object.assign({}, to.query, {redirect: to.fullPath})
return {name: '/startup', query: query}
}
}
})
}

View file

@ -0,0 +1,16 @@
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
// redirect to the home page in case of insufficient permissions
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: '/'}
}
}
})
}

View file

@ -0,0 +1,5 @@
# Store
Pinia stores are used to store reactive state and expose actions to mutate it.
Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.

11
next-ui/src/stores/app.ts Normal file
View file

@ -0,0 +1,11 @@
// Utilities
import {defineStore} from 'pinia'
import {useDisplay} from 'vuetify'
export const useAppStore = defineStore('app', {
state: () => ({
drawer: !useDisplay().mobile.value.valueOf(),
theme: 'system',
}),
persist: true,
})

View file

@ -0,0 +1,8 @@
// Utilities
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia

View file

@ -0,0 +1,7 @@
// Utilities
import {defineStore} from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
}),
})

View file

@ -0,0 +1,3 @@
# Styles
This directory is for configuring the styles of the application.

View file

@ -0,0 +1,16 @@
html {
overflow-y: auto !important;
}
.link-none {
text-decoration: none;
}
.link-underline {
text-decoration: none;
color: var(--v-anchor-base)
}
.link-underline:hover {
text-decoration: underline;
}

View file

@ -0,0 +1,11 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $reset: true
// );

43
next-ui/src/typed-router.d.ts vendored Normal file
View file

@ -0,0 +1,43 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/account/activity': RouteRecordInfo<'/account/activity', '/account/activity', Record<never, never>, Record<never, never>>,
'/account/api-keys': RouteRecordInfo<'/account/api-keys', '/account/api-keys', Record<never, never>, Record<never, never>>,
'/account/details': RouteRecordInfo<'/account/details', '/account/details', Record<never, never>, Record<never, never>>,
'/account/ui': RouteRecordInfo<'/account/ui', '/account/ui', Record<never, never>, Record<never, never>>,
'/history': RouteRecordInfo<'/history', '/history', Record<never, never>, Record<never, never>>,
'/import/books': RouteRecordInfo<'/import/books', '/import/books', Record<never, never>, Record<never, never>>,
'/import/readlist': RouteRecordInfo<'/import/readlist', '/import/readlist', Record<never, never>, Record<never, never>>,
'/login': RouteRecordInfo<'/login', '/login', Record<never, never>, Record<never, never>>,
'/media/analysis': RouteRecordInfo<'/media/analysis', '/media/analysis', Record<never, never>, Record<never, never>>,
'/media/duplicate-files': RouteRecordInfo<'/media/duplicate-files', '/media/duplicate-files', Record<never, never>, Record<never, never>>,
'/media/duplicate-pages/known': RouteRecordInfo<'/media/duplicate-pages/known', '/media/duplicate-pages/known', Record<never, never>, Record<never, never>>,
'/media/duplicate-pages/unknown': RouteRecordInfo<'/media/duplicate-pages/unknown', '/media/duplicate-pages/unknown', Record<never, never>, Record<never, never>>,
'/media/missing-posters': RouteRecordInfo<'/media/missing-posters', '/media/missing-posters', Record<never, never>, Record<never, never>>,
'/server/announcements': RouteRecordInfo<'/server/announcements', '/server/announcements', Record<never, never>, Record<never, never>>,
'/server/metrics': RouteRecordInfo<'/server/metrics', '/server/metrics', Record<never, never>, Record<never, never>>,
'/server/settings': RouteRecordInfo<'/server/settings', '/server/settings', Record<never, never>, Record<never, never>>,
'/server/ui': RouteRecordInfo<'/server/ui', '/server/ui', Record<never, never>, Record<never, never>>,
'/server/updates': RouteRecordInfo<'/server/updates', '/server/updates', Record<never, never>, Record<never, never>>,
'/server/users': RouteRecordInfo<'/server/users', '/server/users', Record<never, never>, Record<never, never>>,
'/startup': RouteRecordInfo<'/startup', '/startup', Record<never, never>, Record<never, never>>,
}
}

View file

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

15
next-ui/src/types/RouterMeta.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
// This can be directly added to any of your `.ts` files like `router.ts`
// 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'
// To ensure it is treated as a module, add at least one `export` statement
export {}
declare module 'vue-router' {
interface RouteMeta {
noAuth?: boolean
requiresRole?: UserRoles
}
}

View file

@ -0,0 +1,7 @@
export enum UserRoles {
ADMIN = 'ADMIN',
FILE_DOWNLOAD = 'FILE_DOWNLOAD',
PAGE_STREAMING = 'PAGE_STREAMING',
KOBO_SYNC = 'KOBO_SYNC',
KOREADER_SYNC = 'KOREADER_SYNC'
}

18
next-ui/tsconfig.app.json Normal file
View file

@ -0,0 +1,18 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true, // openapi-ts
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["vite-plugin-vue-layouts-next/client"]
}
}

11
next-ui/tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View file

@ -0,0 +1,18 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

84
next-ui/vite.config.mts Normal file
View file

@ -0,0 +1,84 @@
// Plugins
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import ViteFonts from "unplugin-fonts/vite"
import Layouts from 'vite-plugin-vue-layouts-next'
import Vue from '@vitejs/plugin-vue'
import VueRouter from 'unplugin-vue-router/vite'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter({
dts: 'src/typed-router.d.ts',
}),
Layouts(),
AutoImport({
imports: [
'vue',
{
'vue-router/auto': ['useRoute', 'useRouter'],
}
],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true,
},
vueTemplate: true,
}),
Components({
dts: 'src/components.d.ts',
}),
Vue({
template: { transformAssetUrls },
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
},
}),
ViteFonts({
fontsource: {
families: [
{
name: "Roboto",
weights: [100, 300, 400, 500, 700, 900],
styles: ["normal", "italic"],
},
],
},
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
css: {
preprocessorOptions: {
sass: {
api: 'modern-compiler',
},
},
},
})