mirror of
https://github.com/gotson/komga.git
synced 2025-12-19 23:12:47 +01:00
feat(webui): view duplicate pages
This commit is contained in:
parent
5777952c05
commit
79d265c852
10 changed files with 359 additions and 0 deletions
99
komga-webui/src/components/PageHashMatchesTable.vue
Normal file
99
komga-webui/src/components/PageHashMatchesTable.vue
Normal 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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
17
komga-webui/src/plugins/komga-pagehashes.plugin.ts
Normal file
17
komga-webui/src/plugins/komga-pagehashes.plugin.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
44
komga-webui/src/services/komga-pagehashes.service.ts
Normal file
44
komga-webui/src/services/komga-pagehashes.service.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
komga-webui/src/types/komga-pagehashes.ts
Normal file
13
komga-webui/src/types/komga-pagehashes.ts
Normal 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,
|
||||
}
|
||||
158
komga-webui/src/views/SettingsDuplicatePages.vue
Normal file
158
komga-webui/src/views/SettingsDuplicatePages.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue