mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
parent
b599b72c48
commit
30208a2340
9 changed files with 133 additions and 47 deletions
|
|
@ -5,8 +5,12 @@
|
||||||
class="sticky-bar"
|
class="sticky-bar"
|
||||||
:style="barStyle"
|
:style="barStyle"
|
||||||
>
|
>
|
||||||
|
<!-- Action menu -->
|
||||||
|
<library-actions-menu v-if="library"
|
||||||
|
:library="library"/>
|
||||||
|
|
||||||
<v-toolbar-title>
|
<v-toolbar-title>
|
||||||
<span>{{ libraryName }}</span>
|
<span>{{ library ? library.name : 'All libraries' }}</span>
|
||||||
<span class="ml-4 badge-count"
|
<span class="ml-4 badge-count"
|
||||||
v-if="totalElements"
|
v-if="totalElements"
|
||||||
>
|
>
|
||||||
|
|
@ -16,6 +20,7 @@
|
||||||
|
|
||||||
<v-spacer/>
|
<v-spacer/>
|
||||||
|
|
||||||
|
<!-- Sort menu -->
|
||||||
<v-menu offset-y>
|
<v-menu offset-y>
|
||||||
<template v-slot:activator="{on}">
|
<template v-slot:activator="{on}">
|
||||||
<v-btn icon v-on="on">
|
<v-btn icon v-on="on">
|
||||||
|
|
@ -41,19 +46,6 @@
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
|
||||||
<v-menu offset-y v-if="libraryId !== 0">
|
|
||||||
<template v-slot:activator="{ on }">
|
|
||||||
<v-btn icon v-on="on">
|
|
||||||
<v-icon>mdi-dots-vertical</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item @click="analyze()">
|
|
||||||
<v-list-item-title>Analyze</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-container fluid class="px-6">
|
<v-container fluid class="px-6">
|
||||||
|
|
@ -80,15 +72,16 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CardSeries from '@/components/CardSeries.vue'
|
import CardSeries from '@/components/CardSeries.vue'
|
||||||
|
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
|
||||||
import { LoadState } from '@/types/common'
|
import { LoadState } from '@/types/common'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'BrowseLibraries',
|
name: 'BrowseLibraries',
|
||||||
components: { CardSeries },
|
components: { LibraryActionsMenu, CardSeries },
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
libraryName: '',
|
library: undefined as LibraryDto | undefined,
|
||||||
series: [] as SeriesDto[],
|
series: [] as SeriesDto[],
|
||||||
pagesState: [] as LoadState[],
|
pagesState: [] as LoadState[],
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
|
@ -122,7 +115,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created () {
|
async created () {
|
||||||
this.libraryName = await this.getLibraryNameLazy(this.libraryId)
|
this.library = await this.getLibraryLazy(this.libraryId)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
// fill series skeletons if an index is provided, so scroll position can be restored
|
// fill series skeletons if an index is provided, so scroll position can be restored
|
||||||
|
|
@ -137,7 +130,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
beforeRouteUpdate (to, from, next) {
|
beforeRouteUpdate (to, from, next) {
|
||||||
if (to.params.libraryId !== from.params.libraryId) {
|
if (to.params.libraryId !== from.params.libraryId) {
|
||||||
this.libraryName = this.getLibraryNameLazy(Number(to.params.libraryId))
|
this.library = this.getLibraryLazy(Number(to.params.libraryId))
|
||||||
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
||||||
this.reloadData(Number(to.params.libraryId))
|
this.reloadData(Number(to.params.libraryId))
|
||||||
}
|
}
|
||||||
|
|
@ -249,20 +242,16 @@ export default Vue.extend({
|
||||||
this.series.splice(page.number * page.size, page.size, ...page.content)
|
this.series.splice(page.number * page.size, page.size, ...page.content)
|
||||||
this.pagesState[page.number] = LoadState.Loaded
|
this.pagesState[page.number] = LoadState.Loaded
|
||||||
},
|
},
|
||||||
getLibraryNameLazy (libraryId: any): string {
|
getLibraryLazy (libraryId: any): LibraryDto | undefined {
|
||||||
if (libraryId !== 0) {
|
if (libraryId !== 0) {
|
||||||
return (this.$store.getters.getLibraryById(libraryId)).name
|
return this.$store.getters.getLibraryById(libraryId)
|
||||||
} else {
|
} else {
|
||||||
return 'All libraries'
|
return undefined
|
||||||
}
|
}
|
||||||
},
|
|
||||||
analyze () {
|
|
||||||
this.$komgaLibraries.analyzeLibrary(this.libraryId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@import "../assets/css/badge.css";
|
@import "../assets/css/badge.css";
|
||||||
@import "../assets/css/sticky-bar.css";
|
@import "../assets/css/sticky-bar.css";
|
||||||
|
|
|
||||||
76
komga-webui/src/components/LibraryActionsMenu.vue
Normal file
76
komga-webui/src/components/LibraryActionsMenu.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-menu offset-y v-if="isAdmin">
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn icon v-on="on" @click.prevent="">
|
||||||
|
<v-icon>mdi-dots-vertical</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item @click="scan">
|
||||||
|
<v-list-item-title>Scan library files</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="analyze">
|
||||||
|
<v-list-item-title>Analyze</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="promptDeleteLibrary"
|
||||||
|
class="list-warning">
|
||||||
|
<v-list-item-title>Delete</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<library-delete-dialog v-model="modalDeleteLibrary"
|
||||||
|
:library="library"
|
||||||
|
@deleted="navigateHome"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import LibraryDeleteDialog from '@/components/LibraryDeleteDialog.vue'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'library-actions-menu',
|
||||||
|
components: { LibraryDeleteDialog },
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
modalDeleteLibrary: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
library: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isAdmin (): boolean {
|
||||||
|
return this.$store.getters.meAdmin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scan () {
|
||||||
|
this.$komgaLibraries.scanLibrary(this.library)
|
||||||
|
},
|
||||||
|
analyze () {
|
||||||
|
this.$komgaLibraries.analyzeLibrary(this.library)
|
||||||
|
},
|
||||||
|
promptDeleteLibrary () {
|
||||||
|
this.modalDeleteLibrary = true
|
||||||
|
},
|
||||||
|
navigateHome () {
|
||||||
|
this.$router.push({ name: 'home' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.list-warning:hover {
|
||||||
|
background: #F44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-warning:hover .v-list-item__title {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -99,6 +99,7 @@ export default Vue.extend({
|
||||||
async deleteLibrary () {
|
async deleteLibrary () {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('deleteLibrary', this.library)
|
await this.$store.dispatch('deleteLibrary', this.library)
|
||||||
|
this.$emit('deleted', true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.showSnack(e.message)
|
this.showSnack(e.message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,23 @@ export default class KomgaLibrariesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async analyzeLibrary (libraryId: number) {
|
async scanLibrary (library: LibraryDto) {
|
||||||
try {
|
try {
|
||||||
await this.http.post(`${API_LIBRARIES}/${libraryId}/analyze`)
|
await this.http.post(`${API_LIBRARIES}/${library.id}/scan`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let msg = `An error occurred while trying to analyze library`
|
let msg = `An error occurred while trying to scan library '${library.name}'`
|
||||||
|
if (e.response.data.message) {
|
||||||
|
msg += `: ${e.response.data.message}`
|
||||||
|
}
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyzeLibrary (library: LibraryDto) {
|
||||||
|
try {
|
||||||
|
await this.http.post(`${API_LIBRARIES}/${library.id}/analyze`)
|
||||||
|
} catch (e) {
|
||||||
|
let msg = `An error occurred while trying to analyze library '${library.name}'`
|
||||||
if (e.response.data.message) {
|
if (e.response.data.message) {
|
||||||
msg += `: ${e.response.data.message}`
|
msg += `: ${e.response.data.message}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,7 @@
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
<v-list-item-action v-if="isAdmin">
|
<v-list-item-action v-if="isAdmin">
|
||||||
<v-btn icon @click.prevent="promptDeleteLibrary(l)">
|
<library-actions-menu :library="l"/>
|
||||||
<v-icon>mdi-delete</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
|
|
@ -103,27 +101,21 @@
|
||||||
<v-content>
|
<v-content>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</v-content>
|
</v-content>
|
||||||
|
|
||||||
<library-delete-dialog v-model="modalDeleteLibrary"
|
|
||||||
:library="libraryToDelete">
|
|
||||||
</library-delete-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LibraryDeleteDialog from '@/components/LibraryDeleteDialog.vue'
|
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
|
||||||
import SearchBox from '@/components/SearchBox.vue'
|
import SearchBox from '@/components/SearchBox.vue'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'home',
|
name: 'home',
|
||||||
components: { LibraryDeleteDialog, SearchBox },
|
components: { LibraryActionsMenu, SearchBox },
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
drawerVisible: this.$vuetify.breakpoint.lgAndUp,
|
drawerVisible: this.$vuetify.breakpoint.lgAndUp,
|
||||||
modalAddLibrary: false,
|
modalAddLibrary: false
|
||||||
modalDeleteLibrary: false,
|
|
||||||
libraryToDelete: {} as LibraryDto
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -138,10 +130,6 @@ export default Vue.extend({
|
||||||
toggleDrawer () {
|
toggleDrawer () {
|
||||||
this.drawerVisible = !this.drawerVisible
|
this.drawerVisible = !this.drawerVisible
|
||||||
},
|
},
|
||||||
promptDeleteLibrary (library: LibraryDto) {
|
|
||||||
this.libraryToDelete = library
|
|
||||||
this.modalDeleteLibrary = true
|
|
||||||
},
|
|
||||||
logout () {
|
logout () {
|
||||||
this.$store.dispatch('logout')
|
this.$store.dispatch('logout')
|
||||||
this.$router.push({ name: 'login' })
|
this.$router.push({ name: 'login' })
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package org.gotson.komga.domain.service
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||||
import org.gotson.komga.domain.model.Book
|
import org.gotson.komga.domain.model.Book
|
||||||
|
import org.gotson.komga.domain.model.Library
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||||
import org.springframework.scheduling.annotation.Async
|
import org.springframework.scheduling.annotation.Async
|
||||||
|
|
@ -21,7 +22,7 @@ class AsyncOrchestrator(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Async("periodicScanTaskExecutor")
|
@Async("periodicScanTaskExecutor")
|
||||||
fun scanAndAnalyze() {
|
fun scanAndAnalyzeAllLibraries() {
|
||||||
logger.info { "Starting periodic libraries scan" }
|
logger.info { "Starting periodic libraries scan" }
|
||||||
val libraries = libraryRepository.findAll()
|
val libraries = libraryRepository.findAll()
|
||||||
|
|
||||||
|
|
@ -37,6 +38,12 @@ class AsyncOrchestrator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Async("periodicScanTaskExecutor")
|
||||||
|
fun scanAndAnalyzeOneLibrary(library: Library) {
|
||||||
|
libraryScanner.scanRootFolder(library)
|
||||||
|
libraryScanner.analyzeUnknownBooks()
|
||||||
|
}
|
||||||
|
|
||||||
@Async("regenerateThumbnailsTaskExecutor")
|
@Async("regenerateThumbnailsTaskExecutor")
|
||||||
fun regenerateAllThumbnails() {
|
fun regenerateAllThumbnails() {
|
||||||
logger.info { "Regenerate thumbnail for all books" }
|
logger.info { "Regenerate thumbnail for all books" }
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class LibraryLifecycle(
|
||||||
|
|
||||||
logger.info { "Trying to launch a scan for the newly added library: ${library.name}" }
|
logger.info { "Trying to launch a scan for the newly added library: ${library.name}" }
|
||||||
try {
|
try {
|
||||||
asyncOrchestrator.scanAndAnalyze()
|
asyncOrchestrator.scanAndAnalyzeAllLibraries()
|
||||||
} catch (e: RejectedExecutionException) {
|
} catch (e: RejectedExecutionException) {
|
||||||
logger.warn { "Another scan is already running, skipping" }
|
logger.warn { "Another scan is already running, skipping" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class PeriodicScannerController(
|
||||||
@Scheduled(cron = "#{@komgaProperties.librariesScanCron ?: '-'}")
|
@Scheduled(cron = "#{@komgaProperties.librariesScanCron ?: '-'}")
|
||||||
fun scanRootFolder() {
|
fun scanRootFolder() {
|
||||||
try {
|
try {
|
||||||
asyncOrchestrator.scanAndAnalyze()
|
asyncOrchestrator.scanAndAnalyzeAllLibraries()
|
||||||
} catch (e: RejectedExecutionException) {
|
} catch (e: RejectedExecutionException) {
|
||||||
logger.warn { "Another scan is already running, skipping" }
|
logger.warn { "Another scan is already running, skipping" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,19 @@ class LibraryController(
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("{libraryId}/scan")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
fun scan(@PathVariable libraryId: Long) {
|
||||||
|
libraryRepository.findByIdOrNull(libraryId)?.let { library ->
|
||||||
|
try {
|
||||||
|
asyncOrchestrator.scanAndAnalyzeOneLibrary(library)
|
||||||
|
} catch (e: RejectedExecutionException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Another scan task is already running")
|
||||||
|
}
|
||||||
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("{libraryId}/analyze")
|
@PostMapping("{libraryId}/analyze")
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue