mirror of
https://github.com/gotson/komga.git
synced 2026-04-28 18:02:37 +02:00
feat(webui): support kepubify path in Server Settings
This commit is contained in:
parent
ca57ab35fd
commit
d838c85786
7 changed files with 98 additions and 21 deletions
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
interface DirectoryListingDto {
|
||||
parent?: string,
|
||||
directories: PathDto[]
|
||||
directories: PathDto[],
|
||||
files: PathDto[],
|
||||
}
|
||||
|
||||
interface PathDto {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue