feat(webui): view duplicate pages

This commit is contained in:
Gauthier Roebroeck 2022-01-26 18:19:06 +08:00
parent 5777952c05
commit 79d265c852
10 changed files with 359 additions and 0 deletions

View file

@ -0,0 +1,99 @@
<template>
<v-data-table
:headers="headers"
:items="elements"
:options.sync="options"
:server-items-length="totalElements"
:loading="loading"
class="elevation-1"
:footer-props="{
itemsPerPageOptions: [10, 20, 50]
}"
>
<template v-slot:item.url="{ item }">
<router-link :to="{name:'browse-book', params: {bookId: item.bookId}}">{{ item.url }}</router-link>
</template>
<template v-slot:item.bookId="{ item }">
<v-img
contain
height="200"
:src="bookPageThumbnailUrl(item.bookId, item.pageNumber)"
/>
</template>
<template v-slot:footer.prepend>
<v-btn icon @click="loadData">
<v-icon>mdi-refresh</v-icon>
</v-btn>
</template>
</v-data-table>
</template>
<script lang="ts">
import Vue from 'vue'
import {bookPageThumbnailUrl} from '@/functions/urls'
import {PageHashMatchDto} from '@/types/komga-pagehashes'
export default Vue.extend({
name: 'PageHashMatchesTable',
props: {
hash: {
type: String,
},
},
data() {
return {
elements: [] as PageHashMatchDto[],
totalElements: 0,
loading: true,
options: {} as any,
bookPageThumbnailUrl,
}
},
watch: {
options: {
handler() {
this.loadData(this.hash)
},
deep: true,
},
hash(val) {
this.options.page = 1
this.loadData(val)
},
},
computed: {
headers(): object[] {
return [
{text: this.$t('common.url').toString(), value: 'url'},
{text: this.$t('common.page_number').toString(), value: 'pageNumber'},
{text: this.$t('common.page').toString(), value: 'bookId'},
]
},
},
methods: {
async loadData(hash: string) {
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 elementsPage = await this.$komgaPageHashes.getUnknownPageHashMatches(hash, pageRequest)
this.totalElements = elementsPage.totalElements
this.elements = elementsPage.content
this.loading = false
},
},
})
</script>

View file

@ -71,3 +71,11 @@ export function readListThumbnailUrlByThumbnailId(readListId: string, thumbnailI
export function transientBookPageUrl(transientBookId: string, page: number): string {
return `${urls.originNoSlash}/api/v1/transient-books/${transientBookId}/pages/${page}`
}
export function pageHashUnknownThumbnailUrl(hash: string, resize?: number): string {
let url = `${urls.originNoSlash}/api/v1/page-hashes/unknown/${hash}/thumbnail`
if(resize) {
url += `?resize=${resize}`
}
return url
}

View file

@ -202,6 +202,8 @@
"n_selected": "{count} selected",
"nothing_to_show": "Nothing to show",
"outdated": "Outdated",
"page": "Page",
"page_number": "Page number",
"pages": "pages",
"pages_n": "No pages | 1 page | {count} pages",
"password": "Password",
@ -216,6 +218,7 @@
"tags": "Tags",
"unavailable": "Unavailable",
"unlock_all": "Unlock all",
"url": "URL",
"use_filter_panel_to_change_filter": "Use the filter panel to change the active filter",
"year": "year"
},
@ -524,6 +527,14 @@
"title_comparison": "Book Comparison"
}
},
"duplicate_pages": {
"action_delete_auto": "Auto delete",
"action_delete_manual": "Manual delete",
"action_ignore": "Ignore",
"matches_n": "No matches | 1 match | {count} matches",
"title": "Duplicate pages",
"unknown_size": "Unknown size"
},
"duplicates": {
"file_hash": "File hash",
"size": "Size",

View file

@ -21,6 +21,7 @@ import komgaSse from './plugins/komga-sse.plugin'
import komgaTasks from './plugins/komga-tasks.plugin'
import komgaOauth2 from './plugins/komga-oauth2.plugin'
import komgaLogin from './plugins/komga-login.plugin'
import komgaPageHashes from './plugins/komga-pagehashes.plugin'
import vuetify from './plugins/vuetify'
import logger from './plugins/logger.plugin'
import './public-path'
@ -51,6 +52,7 @@ Vue.use(actuator, {http: Vue.prototype.$http})
Vue.use(komgaTasks, {http: Vue.prototype.$http})
Vue.use(komgaOauth2, {http: Vue.prototype.$http})
Vue.use(komgaLogin, {http: Vue.prototype.$http})
Vue.use(komgaPageHashes, {http: Vue.prototype.$http})
Vue.config.productionTip = false

View file

@ -0,0 +1,17 @@
import {AxiosInstance} from 'axios'
import _Vue from 'vue'
import KomgaPageHashesService from '@/services/komga-pagehashes.service'
export default {
install(
Vue: typeof _Vue,
{http}: { http: AxiosInstance }) {
Vue.prototype.$komgaPageHashes = new KomgaPageHashesService(http)
},
}
declare module 'vue/types/vue' {
interface Vue {
$komgaPageHashes: KomgaPageHashesService;
}
}

View file

@ -94,6 +94,12 @@ const router = new Router({
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "settings-duplicates" */ './views/SettingsDuplicates.vue'),
},
{
path: '/settings/duplicate-pages',
name: 'settings-duplicate-pages',
beforeEnter: adminGuard,
component: () => import(/* webpackChunkName: "settings-duplicate-pages" */ './views/SettingsDuplicatePages.vue'),
},
{
path: '/settings/server',
name: 'settings-server',

View file

@ -0,0 +1,44 @@
import {AxiosInstance} from 'axios'
import {PageHashMatchDto, PageHashUnknownDto} from '@/types/komga-pagehashes'
const qs = require('qs')
const API_PAGE_HASH = '/api/v1/page-hashes'
export default class KomgaPageHashesService {
private http: AxiosInstance
constructor(http: AxiosInstance) {
this.http = http
}
async getUnknownHashes(pageRequest?: PageRequest): Promise<Page<PageHashUnknownDto>> {
try {
return (await this.http.get(`${API_PAGE_HASH}/unknown`, {
params: pageRequest,
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve unknown page hashes'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async getUnknownPageHashMatches(hash: string, pageRequest?: PageRequest): Promise<Page<PageHashMatchDto>> {
try {
return (await this.http.get(`${API_PAGE_HASH}/unknown/${hash}`, {
params: pageRequest,
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = `An error occurred while trying to retrieve matches for page hash: ${hash}`
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
}

View file

@ -0,0 +1,13 @@
export interface PageHashUnknownDto {
hash: string,
mediaType: string,
sizeBytes?: number,
size?: string,
matchCount: number,
}
export interface PageHashMatchDto{
bookId: string,
url: string,
pageNumber: number,
}

View file

@ -0,0 +1,158 @@
<template>
<v-container fluid class="pa-6">
<v-pagination
v-if="totalPages > 1"
v-model="page"
:total-visible="paginationVisible"
:length="totalPages"
class="mb-2"
/>
<v-row>
<v-card
v-for="(element, i) in elements"
:key="i"
class="ma-2"
>
<v-container>
<v-row>
<v-col>
<v-img
width="200"
contain
@click="showDialogImage(element.hash)"
:src="pageHashUnknownThumbnailUrl(element.hash, 500)"
style="cursor: zoom-in"
/>
</v-col>
<v-col>
<v-card-text>
<div>{{ element.mediaType }}</div>
<div>{{ element.size || $t('duplicate_pages.unknown_size') }}</div>
<v-btn
@click="showDialogMatches(element.hash)"
outlined
rounded
class="mt-2"
>
{{ $tc('duplicate_pages.matches_n', element.matchCount) }}
</v-btn>
</v-card-text>
</v-col>
</v-row>
<v-card-actions>
<v-btn text disabled>{{ $t('duplicate_pages.action_ignore') }}</v-btn>
<v-btn text disabled>{{ $t('duplicate_pages.action_delete_manual') }}</v-btn>
<v-btn text disabled>{{ $t('duplicate_pages.action_delete_auto') }}</v-btn>
</v-card-actions>
</v-container>
</v-card>
</v-row>
<v-dialog v-model="dialogImage">
<v-card>
<v-card-text>
<v-img
@click="dialogImage = false"
contain
:src="pageHashUnknownThumbnailUrl(dialogImageHash)"
style="cursor: zoom-out;"
/>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog
v-model="dialogMatches"
scrollable
>
<v-card>
<v-card-text>
<page-hash-matches-table
:hash="dialogMatchesHash"
class="my-2"
/>
</v-card-text>
<v-card-actions>
<v-btn @click="dialogMatches = false" text>{{ $t('common.close') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script lang="ts">
import Vue from 'vue'
import {PageHashUnknownDto} from '@/types/komga-pagehashes'
import {pageHashUnknownThumbnailUrl} from '@/functions/urls'
import PageHashMatchesTable from '@/components/PageHashMatchesTable.vue'
export default Vue.extend({
name: 'SettingsDuplicatePages',
components: {PageHashMatchesTable},
data: function () {
return {
elements: [] as PageHashUnknownDto[],
totalElements: 0,
page: 1,
totalPages: 1,
dialogImage: false,
dialogMatches: false,
dialogImageHash: '',
dialogMatchesHash: '',
pageHashUnknownThumbnailUrl,
}
},
async mounted() {
await this.loadData(this.page)
},
watch: {
page(val) {
this.loadData(val)
},
},
computed: {
paginationVisible(): number {
switch (this.$vuetify.breakpoint.name) {
case 'xs':
return 5
case 'sm':
case 'md':
return 10
case 'lg':
case 'xl':
default:
return 15
}
},
},
methods: {
async loadData(page: number) {
const pageRequest = {
page: page - 1,
sort: ['matchCount,desc'],
} as PageRequest
const itemsPage = await this.$komgaPageHashes.getUnknownHashes(pageRequest)
this.totalElements = itemsPage.totalElements
this.totalPages = itemsPage.totalPages
this.elements = itemsPage.content
},
showDialogImage(hash: string) {
this.dialogImageHash = hash
this.dialogImage = true
},
showDialogMatches(hash: string) {
this.dialogMatchesHash = hash
this.dialogMatches = true
},
},
})
</script>
<style scoped>
</style>

View file

@ -11,6 +11,7 @@
</v-badge>
</v-tab>
<v-tab :to="{name: 'settings-duplicates'}">{{ $t('duplicates.title') }}</v-tab>
<v-tab :to="{name: 'settings-duplicate-pages'}">{{ $t('duplicate_pages.title') }}</v-tab>
<v-tab :to="{name: 'settings-users'}">{{ $t('users.users') }}</v-tab>
<v-tab :to="{name: 'settings-server'}">{{ $t('server.tab_title') }}</v-tab>
</v-tabs>