mirror of
https://github.com/gotson/komga.git
synced 2025-12-21 07:56:57 +01:00
parent
b599b72c48
commit
30208a2340
9 changed files with 133 additions and 47 deletions
|
|
@ -5,8 +5,12 @@
|
|||
class="sticky-bar"
|
||||
:style="barStyle"
|
||||
>
|
||||
<!-- Action menu -->
|
||||
<library-actions-menu v-if="library"
|
||||
:library="library"/>
|
||||
|
||||
<v-toolbar-title>
|
||||
<span>{{ libraryName }}</span>
|
||||
<span>{{ library ? library.name : 'All libraries' }}</span>
|
||||
<span class="ml-4 badge-count"
|
||||
v-if="totalElements"
|
||||
>
|
||||
|
|
@ -16,6 +20,7 @@
|
|||
|
||||
<v-spacer/>
|
||||
|
||||
<!-- Sort menu -->
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{on}">
|
||||
<v-btn icon v-on="on">
|
||||
|
|
@ -41,19 +46,6 @@
|
|||
</v-list-item>
|
||||
</v-list>
|
||||
</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-container fluid class="px-6">
|
||||
|
|
@ -80,15 +72,16 @@
|
|||
|
||||
<script lang="ts">
|
||||
import CardSeries from '@/components/CardSeries.vue'
|
||||
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
|
||||
import { LoadState } from '@/types/common'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseLibraries',
|
||||
components: { CardSeries },
|
||||
components: { LibraryActionsMenu, CardSeries },
|
||||
data: () => {
|
||||
return {
|
||||
libraryName: '',
|
||||
library: undefined as LibraryDto | undefined,
|
||||
series: [] as SeriesDto[],
|
||||
pagesState: [] as LoadState[],
|
||||
pageSize: 20,
|
||||
|
|
@ -122,7 +115,7 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
async created () {
|
||||
this.libraryName = await this.getLibraryNameLazy(this.libraryId)
|
||||
this.library = await this.getLibraryLazy(this.libraryId)
|
||||
},
|
||||
mounted () {
|
||||
// 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) {
|
||||
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.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.pagesState[page.number] = LoadState.Loaded
|
||||
},
|
||||
getLibraryNameLazy (libraryId: any): string {
|
||||
getLibraryLazy (libraryId: any): LibraryDto | undefined {
|
||||
if (libraryId !== 0) {
|
||||
return (this.$store.getters.getLibraryById(libraryId)).name
|
||||
return this.$store.getters.getLibraryById(libraryId)
|
||||
} else {
|
||||
return 'All libraries'
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
analyze () {
|
||||
this.$komgaLibraries.analyzeLibrary(this.libraryId)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "../assets/css/badge.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 () {
|
||||
try {
|
||||
await this.$store.dispatch('deleteLibrary', this.library)
|
||||
this.$emit('deleted', true)
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,11 +57,23 @@ export default class KomgaLibrariesService {
|
|||
}
|
||||
}
|
||||
|
||||
async analyzeLibrary (libraryId: number) {
|
||||
async scanLibrary (library: LibraryDto) {
|
||||
try {
|
||||
await this.http.post(`${API_LIBRARIES}/${libraryId}/analyze`)
|
||||
await this.http.post(`${API_LIBRARIES}/${library.id}/scan`)
|
||||
} 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) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,9 +65,7 @@
|
|||
</v-tooltip>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action v-if="isAdmin">
|
||||
<v-btn icon @click.prevent="promptDeleteLibrary(l)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
<library-actions-menu :library="l"/>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
|
|
@ -103,27 +101,21 @@
|
|||
<v-content>
|
||||
<router-view/>
|
||||
</v-content>
|
||||
|
||||
<library-delete-dialog v-model="modalDeleteLibrary"
|
||||
:library="libraryToDelete">
|
||||
</library-delete-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import LibraryDeleteDialog from '@/components/LibraryDeleteDialog.vue'
|
||||
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
|
||||
import SearchBox from '@/components/SearchBox.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'home',
|
||||
components: { LibraryDeleteDialog, SearchBox },
|
||||
components: { LibraryActionsMenu, SearchBox },
|
||||
data: function () {
|
||||
return {
|
||||
drawerVisible: this.$vuetify.breakpoint.lgAndUp,
|
||||
modalAddLibrary: false,
|
||||
modalDeleteLibrary: false,
|
||||
libraryToDelete: {} as LibraryDto
|
||||
modalAddLibrary: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -138,10 +130,6 @@ export default Vue.extend({
|
|||
toggleDrawer () {
|
||||
this.drawerVisible = !this.drawerVisible
|
||||
},
|
||||
promptDeleteLibrary (library: LibraryDto) {
|
||||
this.libraryToDelete = library
|
||||
this.modalDeleteLibrary = true
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push({ name: 'login' })
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package org.gotson.komga.domain.service
|
|||
import mu.KotlinLogging
|
||||
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||
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.LibraryRepository
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
|
|
@ -21,7 +22,7 @@ class AsyncOrchestrator(
|
|||
) {
|
||||
|
||||
@Async("periodicScanTaskExecutor")
|
||||
fun scanAndAnalyze() {
|
||||
fun scanAndAnalyzeAllLibraries() {
|
||||
logger.info { "Starting periodic libraries scan" }
|
||||
val libraries = libraryRepository.findAll()
|
||||
|
||||
|
|
@ -37,6 +38,12 @@ class AsyncOrchestrator(
|
|||
}
|
||||
}
|
||||
|
||||
@Async("periodicScanTaskExecutor")
|
||||
fun scanAndAnalyzeOneLibrary(library: Library) {
|
||||
libraryScanner.scanRootFolder(library)
|
||||
libraryScanner.analyzeUnknownBooks()
|
||||
}
|
||||
|
||||
@Async("regenerateThumbnailsTaskExecutor")
|
||||
fun regenerateAllThumbnails() {
|
||||
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}" }
|
||||
try {
|
||||
asyncOrchestrator.scanAndAnalyze()
|
||||
asyncOrchestrator.scanAndAnalyzeAllLibraries()
|
||||
} catch (e: RejectedExecutionException) {
|
||||
logger.warn { "Another scan is already running, skipping" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class PeriodicScannerController(
|
|||
@Scheduled(cron = "#{@komgaProperties.librariesScanCron ?: '-'}")
|
||||
fun scanRootFolder() {
|
||||
try {
|
||||
asyncOrchestrator.scanAndAnalyze()
|
||||
asyncOrchestrator.scanAndAnalyzeAllLibraries()
|
||||
} catch (e: RejectedExecutionException) {
|
||||
logger.warn { "Another scan is already running, skipping" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,19 @@ class LibraryController(
|
|||
} ?: 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")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
|
|
|
|||
Loading…
Reference in a new issue