Skip to content

Commit

Permalink
feat(webui): add media management view for books with missing poster
Browse files Browse the repository at this point in the history
Refs: #1829
  • Loading branch information
gotson committed Jan 20, 2025
1 parent 70bcb8f commit 7d092c2
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 0 deletions.
3 changes: 3 additions & 0 deletions komga-webui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,9 @@
"tasks_total_time": "Tasks total time",
"title": "Metrics"
},
"missing_posters": {
"title": "Missing Posters"
},
"navigation": {
"home": "Home",
"libraries": "Libraries",
Expand Down
6 changes: 6 additions & 0 deletions komga-webui/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ const router = new Router({
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "media-analysis" */ './views/MediaAnalysis.vue'),
},
{
path: '/media-management/missing-posters',
name: 'missing-posters',
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "missing-posters" */ './views/MissingPosters.vue'),
},
{
path: '/media-management/duplicate-files',
name: 'duplicate-files',
Expand Down
19 changes: 19 additions & 0 deletions komga-webui/src/types/komga-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ export class SearchConditionAuthor implements SearchConditionBook, SearchConditi
}
}

export class SearchConditionPoster implements SearchConditionBook {
poster: SearchOperatorBoolean

constructor(op: SearchOperatorEquality) {
this.poster = op
}
}

export class SearchConditionTitleSort implements SearchConditionSeries {
titleSort: SearchOperatorString

Expand All @@ -187,6 +195,17 @@ export interface AuthorMatch {
role?: string
}

export interface PosterMatch {
type?: PosterMatchType,
selected?: boolean
}

export enum PosterMatchType {
GENERATED = 'GENERATED',
SIDECAR = 'SIDECAR',
USER_UPLOADED = 'USER_UPLOADED',
}

export interface SearchOperatorEquality {
}

Expand Down
1 change: 1 addition & 0 deletions komga-webui/src/views/MediaManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
{{ $t('media_analysis.media_analysis') }}
</v-badge>
</v-tab>
<v-tab :to="{name: 'missing-posters'}">{{ $t('missing_posters.title') }}</v-tab>
<v-tab :to="{name: 'duplicate-files'}">{{ $t('duplicates.title') }}</v-tab>
<v-tab :to="{name: 'duplicate-pages'}">{{ $t('duplicate_pages.title') }}</v-tab>
</v-tabs>
Expand Down
122 changes: 122 additions & 0 deletions komga-webui/src/views/MissingPosters.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<v-container fluid class="pa-6">
<v-data-table
:headers="headers"
:items="books"
:options.sync="options"
:server-items-length="totalBooks"
:loading="loading"
multi-sort
class="elevation-1"
:footer-props="{
itemsPerPageOptions: [20, 50, 100]
}"
>
<template v-slot:item.seriesTitle="{ item }">
<router-link
:to="{name: item.oneshot ? 'browse-oneshot' : 'browse-series', params: {bookId: item.id, seriesId: item.seriesId}}">
{{ item.seriesTitle }}
</router-link>
</template>

<template v-slot:item.metadata.title="{ item }">
<router-link
:to="{name: item.oneshot ? 'browse-oneshot' : 'browse-book', params: {bookId: item.id, seriesId: item.seriesId}}">
{{ item.metadata.title }}
</router-link>
</template>

<template v-slot:item.deleted="{ item }">
<v-chip
v-if="item.deleted"
label small color="error">
{{ $t('common.unavailable') }}
</v-chip>
</template>

<template v-slot:footer.prepend>
<v-btn icon @click="loadBooks">
<v-icon>mdi-refresh</v-icon>
</v-btn>
</template>
</v-data-table>
</v-container>
</template>

<script lang="ts">
import Vue from 'vue'
import {MediaStatus} from '@/types/enum-books'
import {BookDto} from '@/types/komga-books'
import {
BookSearch,
PosterMatch,
SearchConditionAllOfBook,
SearchConditionMediaStatus,
SearchConditionPoster,
SearchOperatorIs,
SearchOperatorIsNot,
} from '@/types/komga-search'
export default Vue.extend({
name: 'MissingPosters',
data: function () {
return {
books: [] as BookDto[],
totalBooks: 0,
loading: true,
options: {} as any,
}
},
watch: {
options: {
handler() {
this.loadBooks()
},
deep: true,
},
},
computed: {
headers(): object[] {
return [
{text: this.$i18n.tc('common.series', 1).toString(), value: 'seriesTitle', sortable: false},
{text: this.$i18n.t('common.book').toString(), value: 'metadata.title'},
{text: this.$i18n.t('media_analysis.media_type').toString(), value: 'media.mediaType'},
{text: this.$i18n.t('media_analysis.url').toString(), value: 'url'},
{text: '', value: 'deleted', groupable: false, sortable: false},
]
},
},
methods: {
async loadBooks() {
this.loading = true
const {sortBy, sortDesc, page, itemsPerPage} = this.options
const pageRequest = {
page: page - 1,
size: itemsPerPage,
sort: [],
} as PageRequest
for (let i = 0; i < sortBy.length; i++) {
pageRequest.sort!!.push(`${sortBy[i]},${sortDesc[i] ? 'desc' : 'asc'}`)
}
const booksPage = await this.$komgaBooks.getBooksList({
condition: new SearchConditionAllOfBook([
new SearchConditionMediaStatus(new SearchOperatorIs(MediaStatus.READY)),
new SearchConditionPoster(new SearchOperatorIsNot({selected: true} as PosterMatch)),
]),
} as BookSearch, pageRequest)
this.totalBooks = booksPage.totalElements
this.books = booksPage.content
this.loading = false
},
},
})
</script>

<style scoped>
</style>

0 comments on commit 7d092c2

Please sign in to comment.