mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02: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 {
|
export function transientBookPageUrl(transientBookId: string, page: number): string {
|
||||||
return `${urls.originNoSlash}/api/v1/transient-books/${transientBookId}/pages/${page}`
|
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",
|
"n_selected": "{count} selected",
|
||||||
"nothing_to_show": "Nothing to show",
|
"nothing_to_show": "Nothing to show",
|
||||||
"outdated": "Outdated",
|
"outdated": "Outdated",
|
||||||
|
"page": "Page",
|
||||||
|
"page_number": "Page number",
|
||||||
"pages": "pages",
|
"pages": "pages",
|
||||||
"pages_n": "No pages | 1 page | {count} pages",
|
"pages_n": "No pages | 1 page | {count} pages",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
|
@ -216,6 +218,7 @@
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"unavailable": "Unavailable",
|
"unavailable": "Unavailable",
|
||||||
"unlock_all": "Unlock all",
|
"unlock_all": "Unlock all",
|
||||||
|
"url": "URL",
|
||||||
"use_filter_panel_to_change_filter": "Use the filter panel to change the active filter",
|
"use_filter_panel_to_change_filter": "Use the filter panel to change the active filter",
|
||||||
"year": "year"
|
"year": "year"
|
||||||
},
|
},
|
||||||
|
|
@ -524,6 +527,14 @@
|
||||||
"title_comparison": "Book Comparison"
|
"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": {
|
"duplicates": {
|
||||||
"file_hash": "File hash",
|
"file_hash": "File hash",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import komgaSse from './plugins/komga-sse.plugin'
|
||||||
import komgaTasks from './plugins/komga-tasks.plugin'
|
import komgaTasks from './plugins/komga-tasks.plugin'
|
||||||
import komgaOauth2 from './plugins/komga-oauth2.plugin'
|
import komgaOauth2 from './plugins/komga-oauth2.plugin'
|
||||||
import komgaLogin from './plugins/komga-login.plugin'
|
import komgaLogin from './plugins/komga-login.plugin'
|
||||||
|
import komgaPageHashes from './plugins/komga-pagehashes.plugin'
|
||||||
import vuetify from './plugins/vuetify'
|
import vuetify from './plugins/vuetify'
|
||||||
import logger from './plugins/logger.plugin'
|
import logger from './plugins/logger.plugin'
|
||||||
import './public-path'
|
import './public-path'
|
||||||
|
|
@ -51,6 +52,7 @@ Vue.use(actuator, {http: Vue.prototype.$http})
|
||||||
Vue.use(komgaTasks, {http: Vue.prototype.$http})
|
Vue.use(komgaTasks, {http: Vue.prototype.$http})
|
||||||
Vue.use(komgaOauth2, {http: Vue.prototype.$http})
|
Vue.use(komgaOauth2, {http: Vue.prototype.$http})
|
||||||
Vue.use(komgaLogin, {http: Vue.prototype.$http})
|
Vue.use(komgaLogin, {http: Vue.prototype.$http})
|
||||||
|
Vue.use(komgaPageHashes, {http: Vue.prototype.$http})
|
||||||
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
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,
|
beforeEnter: adminGuard,
|
||||||
component: () => import(/* webpackChunkName: "settings-duplicates" */ './views/SettingsDuplicates.vue'),
|
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',
|
path: '/settings/server',
|
||||||
name: '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-badge>
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab :to="{name: 'settings-duplicates'}">{{ $t('duplicates.title') }}</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-users'}">{{ $t('users.users') }}</v-tab>
|
||||||
<v-tab :to="{name: 'settings-server'}">{{ $t('server.tab_title') }}</v-tab>
|
<v-tab :to="{name: 'settings-server'}">{{ $t('server.tab_title') }}</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue