feat(webui): support kepubify path in Server Settings

This commit is contained in:
Gauthier Roebroeck 2024-09-19 15:53:33 +08:00
parent ca57ab35fd
commit d838c85786
7 changed files with 98 additions and 21 deletions

View file

@ -32,7 +32,7 @@
<div v-for="(d, index) in directoryListing.directories" :key="index">
<v-list-item
@click.prevent="select(d.path)"
@click.prevent="select(d)"
>
<v-list-item-icon>
<v-icon>{{ d.type === 'directory' ? 'mdi-folder' : 'mdi-file' }}</v-icon>
@ -47,6 +47,24 @@
<v-divider v-if="index !== directoryListing.directories.length-1"/>
</div>
<div v-for="(d, index) in directoryListing.files" :key="index">
<v-list-item
@click.prevent="select(d)"
>
<v-list-item-icon>
<v-icon>{{ d.type === 'directory' ? 'mdi-folder' : 'mdi-file' }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ d.name }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-divider v-if="index !== directoryListing.files.length-1"/>
</div>
</v-list>
</v-card-text>
@ -91,6 +109,10 @@ export default Vue.extend({
type: String,
required: false,
},
showFiles: {
type: Boolean,
default: false,
},
dialogTitle: {
type: String,
default: function (): string {
@ -119,17 +141,18 @@ export default Vue.extend({
dialogConfirm() {
this.$emit('input', false)
this.$emit('update:path', this.selectedPath)
this.$emit('confirm')
},
async getDirs(path?: string) {
try {
this.directoryListing = await this.$komgaFileSystem.getDirectoryListing(path)
this.directoryListing = await this.$komgaFileSystem.getDirectoryListing(path, this.showFiles)
} catch (e) {
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
},
select(path: string) {
this.selectedPath = path
this.getDirs(path)
select(path: PathDto) {
this.selectedPath = path.path
if(path.type == 'directory') this.getDirs(path.path)
},
},
})

View file

@ -938,6 +938,7 @@
"hint_kobo_port": "Set only in case of sync issues with covers and downloads",
"label_delete_empty_collections": "Delete empty collections after scan",
"label_delete_empty_readlists": "Delete empty read lists after scan",
"label_kepubify_path": "Path to kepubify",
"label_kobo_port": "Kobo Sync external port",
"label_kobo_proxy": "Proxy Kobo Sync requests to Kobo Store",
"label_rememberme_duration": "Remember me duration (in days)",

View file

@ -9,10 +9,11 @@ export default class KomgaFilesystemService {
this.http = http
}
async getDirectoryListing (path: String = ''): Promise<DirectoryListingDto> {
async getDirectoryListing (path: String = '', showFiles: Boolean = false): Promise<DirectoryListingDto> {
try {
return (await this.http.post(API_FILESYSTEM, {
path: path,
path: path || '',
showFiles: showFiles,
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve directory listing'

View file

@ -1,6 +1,7 @@
interface DirectoryListingDto {
parent?: string,
directories: PathDto[]
directories: PathDto[],
files: PathDto[],
}
interface PathDto {

View file

@ -8,6 +8,7 @@ export interface SettingsDto {
serverContextPath: SettingMultiSource<string>,
koboProxy: boolean,
koboPort?: number,
kepubifyPath: SettingMultiSource<string>,
}
export interface SettingMultiSource<T> {
@ -27,6 +28,7 @@ export interface SettingsUpdateDto {
serverContextPath?: string,
koboProxy?: boolean,
koboPort?: number,
kepubifyPath?: string,
}
export enum ThumbnailSizeDto {

View file

@ -128,6 +128,41 @@
max="65535"
class="mt-4"
/>
<file-browser-dialog
v-model="modalFileBrowserKepubify"
@confirm="$v.form.kepubifyPath.$touch()"
:path.sync="form.kepubifyPath"
:show-files="true"
/>
<v-text-field
v-model="form.kepubifyPath"
@input="$v.form.kepubifyPath.$touch()"
@blur="$v.form.kepubifyPath.$touch()"
:error-messages="serverContextPathErrors"
:placeholder="existingSettings.kepubifyPath?.configurationSource"
:persistent-placeholder="!!existingSettings.kepubifyPath?.configurationSource"
clearable
:label="$t('server_settings.label_kepubify_path')"
class="mt-4"
>
<template v-slot:append v-if="!!existingSettings.kepubifyPath?.configurationSource">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on">
mdi-information-outline
</v-icon>
</template>
{{ $t('server_settings.config_precedence') }}
</v-tooltip>
</template>
<template v-slot:append-outer>
<v-btn small @click="modalFileBrowserKepubify = true">{{ $t('dialog.edit_library.button_browse') }}</v-btn>
</template>
</v-text-field>
</v-col>
</v-row>
<v-row>
@ -164,12 +199,13 @@ import {SettingsDto, ThumbnailSizeDto} from '@/types/komga-settings'
import Vue from 'vue'
import {helpers, maxValue, minValue, required} from 'vuelidate/lib/validators'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
import FileBrowserDialog from '@/components/dialogs/FileBrowserDialog.vue'
const contextPath = helpers.regex('contextPath', /^\/[-a-zA-Z0-9_\/]*[a-zA-Z0-9]$/)
export default Vue.extend({
name: 'ServerSettings',
components: {ConfirmationDialog},
components: {FileBrowserDialog, ConfirmationDialog},
data: () => ({
form: {
deleteEmptyCollections: false,
@ -182,9 +218,11 @@ export default Vue.extend({
serverContextPath: '',
koboProxy: false,
koboPort: undefined,
kepubifyPath: undefined,
},
existingSettings: {} as SettingsDto,
dialogRegenerateThumbnails: false,
modalFileBrowserKepubify: false,
}),
validations: {
form: {
@ -212,6 +250,7 @@ export default Vue.extend({
minValue: minValue(1),
maxValue: maxValue(65535),
},
kepubifyPath: {},
},
},
mounted() {
@ -269,6 +308,7 @@ export default Vue.extend({
this.$_.merge(this.form, settings)
this.form.serverPort = settings.serverPort.databaseSource
this.form.serverContextPath = settings.serverContextPath.databaseSource
this.form.kepubifyPath = settings.kepubifyPath.databaseSource
this.$_.merge(this.existingSettings, settings)
this.$v.form.$reset()
},
@ -299,6 +339,8 @@ export default Vue.extend({
this.$_.merge(newSettings, {koboProxy: this.form.koboProxy})
if (this.$v.form?.koboPort?.$dirty)
this.$_.merge(newSettings, {koboPort: this.form.koboPort})
if (this.$v.form?.kepubifyPath?.$dirty)
this.$_.merge(newSettings, {kepubifyPath: this.form.kepubifyPath})
await this.$komgaSettings.updateSettings(newSettings)

View file

@ -12,6 +12,7 @@ import org.springframework.web.server.ResponseStatusException
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isDirectory
import kotlin.streams.asSequence
@RestController
@ -27,22 +28,26 @@ class FileSystemController {
if (request.path.isEmpty()) {
DirectoryListingDto(
directories = fs.rootDirectories.map { it.toDto() },
files = emptyList(),
)
} else {
val p = fs.getPath(request.path)
if (!p.isAbsolute) throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Path must be absolute")
val path = fs.getPath(request.path)
if (!path.isAbsolute) throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Path must be absolute")
val directory = if(path.isDirectory()) path else path.parent
try {
val (directories, files) = Files.list(directory).use { dirStream ->
dirStream.asSequence()
.apply { if(!request.showFiles) filter { Files.isDirectory(it) } }
.filter { !Files.isHidden(it) }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.toString() })
.map { it.toDto() }
.toList()
.partition { it.type == "directory" }
}
DirectoryListingDto(
parent = (p.parent ?: "").toString(),
directories =
Files.list(p).use { dirStream ->
dirStream.asSequence()
.filter { Files.isDirectory(it) }
.filter { !Files.isHidden(it) }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.toString() })
.map { it.toDto() }
.toList()
},
parent = (path.parent ?: "").toString(),
directories = directories,
files = files,
)
} catch (e: Exception) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Path does not exist")
@ -52,12 +57,14 @@ class FileSystemController {
data class DirectoryRequestDto(
val path: String = "",
val showFiles: Boolean = false,
)
@JsonInclude(JsonInclude.Include.NON_NULL)
data class DirectoryListingDto(
val parent: String? = null,
val directories: List<PathDto>,
val files: List<PathDto>,
)
data class PathDto(