mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 16:42:24 +01:00
unknown page hashes
This commit is contained in:
parent
78428f137a
commit
376bce3324
7 changed files with 540 additions and 2 deletions
|
|
@ -19,3 +19,8 @@ export function pageHashKnownThumbnailUrl(hash?: string): string | undefined {
|
|||
if (hash) return `${API_BASE_URL}/api/v1/page-hashes/${hash}/thumbnail`
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function pageHashUnknownThumbnailUrl(hash?: string): string | undefined {
|
||||
if (hash) return `${API_BASE_URL}/api/v1/page-hashes/unknown/${hash}/thumbnail`
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import { defineQueryOptions } from '@pinia/colada'
|
|||
import { komgaClient } from '@/api/komga-client'
|
||||
import type { PageHashAction } from '@/types/PageHashAction'
|
||||
|
||||
export const QUERY_KEYS_PAGE_HASHES = {
|
||||
unknown: ['page-hashes-unknown'] as const,
|
||||
}
|
||||
|
||||
export const pageHashesKnownQuery = defineQueryOptions(
|
||||
({
|
||||
actions,
|
||||
|
|
@ -33,6 +37,26 @@ export const pageHashesKnownQuery = defineQueryOptions(
|
|||
}),
|
||||
)
|
||||
|
||||
export const pageHashesUnknownQuery = defineQueryOptions(
|
||||
({ page, size, sort }: { page?: number; size?: number; sort?: string[] }) => ({
|
||||
key: [QUERY_KEYS_PAGE_HASHES.unknown, { page: page, size: size, sort: sort }],
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v1/page-hashes/unknown', {
|
||||
params: {
|
||||
query: {
|
||||
page: page,
|
||||
size: size,
|
||||
sort: sort,
|
||||
},
|
||||
},
|
||||
})
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
placeholderData: (previousData: any) => previousData, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
)
|
||||
|
||||
export const pageHashMatchesQuery = defineQueryOptions(
|
||||
({
|
||||
pageHash,
|
||||
|
|
|
|||
1
next-ui/src/components.d.ts
vendored
1
next-ui/src/components.d.ts
vendored
|
|
@ -54,6 +54,7 @@ declare module 'vue' {
|
|||
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
|
||||
PageHashKnownTable: typeof import('./components/pageHash/KnownTable.vue')['default']
|
||||
PageHashMatchTable: typeof import('./components/pageHash/MatchTable.vue')['default']
|
||||
PageHashUnknownTable: typeof import('./components/pageHash/UnknownTable.vue')['default']
|
||||
ReleaseCard: typeof import('./components/release/Card.vue')['default']
|
||||
RemoteFileList: typeof import('./components/RemoteFileList.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
|
|
|||
61
next-ui/src/components/pageHash/UnknownTable.stories.ts
Normal file
61
next-ui/src/components/pageHash/UnknownTable.stories.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import UnknownTable from './UnknownTable.vue'
|
||||
import { delay, http } from 'msw'
|
||||
import { response401Unauthorized } from '@/mocks/api/handlers'
|
||||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import { mockPage } from '@/mocks/api/pageable'
|
||||
import { PageRequest } from '@/types/PageRequest'
|
||||
import DialogSimpleInstance from '@/components/dialog/DialogSimpleInstance.vue'
|
||||
import SnackQueue from '@/components/SnackQueue.vue'
|
||||
|
||||
const meta = {
|
||||
component: UnknownTable,
|
||||
subcomponents: { SnackQueue },
|
||||
render: (args: object) => ({
|
||||
components: { UnknownTable, DialogSimpleInstance, SnackQueue },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<UnknownTable v-bind="args" /><DialogSimpleInstance/><SnackQueue/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof UnknownTable>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const NoData: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.get('/api/v1/page-hashes/unknown', ({ response }) =>
|
||||
response(200).json(mockPage([], new PageRequest())),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [http.all('*/api/*', async () => await delay(2_000))],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [http.all('*/api/v1/page-hashes/unknown', response401Unauthorized)],
|
||||
},
|
||||
},
|
||||
}
|
||||
393
next-ui/src/components/pageHash/UnknownTable.vue
Normal file
393
next-ui/src/components/pageHash/UnknownTable.vue
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
<template>
|
||||
<v-data-table-server
|
||||
v-model="selectedHashes"
|
||||
v-model:sort-by="sortBy"
|
||||
:loading="isLoading"
|
||||
:items="data?.content"
|
||||
:items-length="data?.totalElements || 0"
|
||||
:items-per-page-options="[
|
||||
{ value: 10, title: '10' },
|
||||
{ value: 25, title: '25' },
|
||||
{ value: 50, title: '50' },
|
||||
{ value: 100, title: '100' },
|
||||
]"
|
||||
:headers="headers"
|
||||
fixed-header
|
||||
fixed-footer
|
||||
select-strategy="page"
|
||||
show-select
|
||||
return-object
|
||||
item-value="hash"
|
||||
multi-sort
|
||||
mobile-breakpoint="md"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #top>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title
|
||||
v-if="display.smAndUp.value || (display.xs.value && selectedHashes.length == 0)"
|
||||
>
|
||||
<v-icon
|
||||
color="medium-emphasis"
|
||||
icon="i-mdi:content-copy"
|
||||
size="x-small"
|
||||
start
|
||||
/>
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Unknown Duplicate Page Table global header',
|
||||
defaultMessage: 'Unknown Duplicates',
|
||||
id: 'XuqK4C',
|
||||
})
|
||||
}}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer v-if="display.smAndUp.value || (display.xs.value && selectedHashes.length > 0)" />
|
||||
<v-btn
|
||||
v-if="selectedHashes.length > 0"
|
||||
variant="elevated"
|
||||
class="mx-2"
|
||||
append-icon="i-mdi:menu-down"
|
||||
>
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Unknown Duplicate Page: selection action button',
|
||||
defaultMessage: 'Mark as',
|
||||
id: 'lFTdQ+',
|
||||
})
|
||||
}}
|
||||
|
||||
<v-menu activator="parent">
|
||||
<v-list
|
||||
density="compact"
|
||||
slim
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in actionOptions"
|
||||
:key="index"
|
||||
:title="item.title"
|
||||
:prepend-icon="item.icon"
|
||||
@click="updateHashActions(selectedHashes, item.value)"
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<EmptyStateNetworkError v-if="error" />
|
||||
<template v-else>
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'Unknown Duplicate Page Table: shown when table has no data',
|
||||
defaultMessage: 'No data found',
|
||||
id: 'hPo41m',
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #[`item.hash`]="{ value }">
|
||||
<div>
|
||||
<v-img
|
||||
width="200"
|
||||
height="200"
|
||||
contain
|
||||
style="cursor: zoom-in"
|
||||
:src="pageHashUnknownThumbnailUrl(value)"
|
||||
lazy-src="@/assets/cover.svg"
|
||||
class="my-1"
|
||||
:alt="
|
||||
$formatMessage({
|
||||
description: 'Unknown Duplicate Page Table: alt description for thumbnail',
|
||||
defaultMessage: 'Duplicate page',
|
||||
id: 'IXhDH6',
|
||||
})
|
||||
"
|
||||
@mouseenter="dialogSimple.activator = $event.currentTarget"
|
||||
@click="showDialogImage(value)"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height">
|
||||
<v-progress-circular
|
||||
color="grey"
|
||||
indeterminate
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-img>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #[`item.action`]="{ item, value }">
|
||||
<v-btn-toggle
|
||||
:model-value="value"
|
||||
variant="outlined"
|
||||
divided
|
||||
rounded="lg"
|
||||
:disabled="value"
|
||||
@update:model-value="(v) => updateHashAction(item, v)"
|
||||
>
|
||||
<v-btn
|
||||
v-tooltip:bottom="
|
||||
intl.formatMessage(pageHashActionMessages[PageHashActionEnum.DELETE_AUTO])
|
||||
"
|
||||
size="small"
|
||||
icon="i-mdi:robot"
|
||||
:value="PageHashActionEnum.DELETE_AUTO"
|
||||
:loading="value === PageHashActionEnum.DELETE_AUTO"
|
||||
color="success"
|
||||
/>
|
||||
<v-btn
|
||||
v-tooltip:bottom="
|
||||
intl.formatMessage(pageHashActionMessages[PageHashActionEnum.DELETE_MANUAL])
|
||||
"
|
||||
size="small"
|
||||
icon="i-mdi:hand-back-right"
|
||||
:value="PageHashActionEnum.DELETE_MANUAL"
|
||||
:loading="value === PageHashActionEnum.DELETE_MANUAL"
|
||||
color="warning"
|
||||
/>
|
||||
<v-btn
|
||||
v-tooltip:bottom="intl.formatMessage(pageHashActionMessages[PageHashActionEnum.IGNORE])"
|
||||
size="small"
|
||||
icon="i-mdi:cancel"
|
||||
:value="PageHashActionEnum.IGNORE"
|
||||
:loading="value === PageHashActionEnum.IGNORE"
|
||||
color=""
|
||||
/>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
|
||||
<template #[`item.size`]="{ value }">
|
||||
{{ getFileSize(value) }}
|
||||
</template>
|
||||
|
||||
<template #[`item.deleteSize`]="{ value }">
|
||||
{{ getFileSize(value) }}
|
||||
</template>
|
||||
|
||||
<template #[`item.matchCount`]="{ item, value }">
|
||||
<v-btn-group
|
||||
rounded="lg"
|
||||
divided
|
||||
variant="outlined"
|
||||
>
|
||||
<v-btn
|
||||
:text="value"
|
||||
append-icon="i-mdi:image-multiple-outline"
|
||||
color=""
|
||||
size="small"
|
||||
:disabled="value == 0"
|
||||
@mouseenter="dialogSimple.activator = $event.currentTarget"
|
||||
@click="showDialogMatches(item.hash)"
|
||||
/>
|
||||
</v-btn-group>
|
||||
</template>
|
||||
|
||||
<template #[`item.created`]="{ value }">
|
||||
<div>{{ $formatDate(value, { dateStyle: 'short' }) }}</div>
|
||||
<div>{{ $formatDate(value, { timeStyle: 'short' }) }}</div>
|
||||
</template>
|
||||
|
||||
<template #[`item.lastModified`]="{ value }">
|
||||
<div>{{ $formatDate(value, { dateStyle: 'short' }) }}</div>
|
||||
<div>{{ $formatDate(value, { timeStyle: 'short' }) }}</div>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { PageRequest, type SortItem } from '@/types/PageRequest'
|
||||
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
|
||||
import { pageHashesUnknownQuery, QUERY_KEYS_PAGE_HASHES } from '@/colada/page-hashes'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { getFileSize } from '@/utils/utils'
|
||||
import { pageHashUnknownThumbnailUrl } from '@/api/images'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { VImg } from 'vuetify/components'
|
||||
import MatchTable from '@/components/pageHash/MatchTable.vue'
|
||||
import { type ErrorCause, komgaClient } from '@/api/komga-client'
|
||||
import {
|
||||
type PageHashAction,
|
||||
PageHashActionEnum,
|
||||
pageHashActionMessages,
|
||||
} from '@/types/PageHashAction'
|
||||
import { useMessagesStore } from '@/stores/messages'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
|
||||
const intl = useIntl()
|
||||
const display = useDisplay()
|
||||
const messagesStore = useMessagesStore()
|
||||
const { simple: dialogSimple } = storeToRefs(useDialogsStore())
|
||||
|
||||
const selectedHashes = ref<components['schemas']['PageHashUnknownDto'][]>([])
|
||||
const sortBy = ref<SortItem[]>([{ key: 'deleteSize', order: 'desc' }])
|
||||
|
||||
//region headers
|
||||
const headers = [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Unknown Duplicate Page Table header: thumbnail',
|
||||
defaultMessage: 'Thumbnail',
|
||||
id: 'oyeyK/',
|
||||
}),
|
||||
key: 'hash',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Unknown Duplicate Page Table header: action',
|
||||
defaultMessage: 'Action',
|
||||
id: 'gxZjIe',
|
||||
}),
|
||||
key: 'action',
|
||||
value: (item: components['schemas']['PageHashUnknownDto']) => {
|
||||
return getPageHashAction(item)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Unknown Duplicate Page Table header: match count',
|
||||
defaultMessage: 'Matches',
|
||||
id: 'hdoWGT',
|
||||
}),
|
||||
key: 'matchCount',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Unknown Duplicate Page Table header: size',
|
||||
defaultMessage: 'Size',
|
||||
id: 'zRDVnR',
|
||||
}),
|
||||
key: 'size',
|
||||
},
|
||||
] as const
|
||||
//endregion
|
||||
|
||||
const pageRequest = ref<PageRequest>(new PageRequest())
|
||||
|
||||
const { data, isLoading, error } = useQuery(pageHashesUnknownQuery, () => ({
|
||||
...pageRequest.value,
|
||||
}))
|
||||
|
||||
function updateOptions({
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
}: {
|
||||
page: number
|
||||
itemsPerPage: number
|
||||
sortBy: SortItem[]
|
||||
}) {
|
||||
pageRequest.value = PageRequest.FromVuetify(page - 1, itemsPerPage, sortBy)
|
||||
}
|
||||
|
||||
function showDialogImage(hash: string) {
|
||||
dialogSimple.value.dialogProps = {
|
||||
fullscreen: display.xs.value,
|
||||
scrollable: true,
|
||||
}
|
||||
dialogSimple.value.slot = {
|
||||
component: markRaw(VImg),
|
||||
props: {
|
||||
src: pageHashUnknownThumbnailUrl(hash),
|
||||
contain: true,
|
||||
style: 'cursor: zoom-out;',
|
||||
},
|
||||
handlers: {
|
||||
click: () => {
|
||||
dialogSimple.value.dialogProps.shown = false
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function showDialogMatches(hash: string) {
|
||||
dialogSimple.value.dialogProps = {
|
||||
fullscreen: display.xs.value,
|
||||
scrollable: true,
|
||||
}
|
||||
dialogSimple.value.slot = {
|
||||
component: markRaw(MatchTable),
|
||||
props: {
|
||||
modelValue: hash,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//region Update action
|
||||
const updateRequests = ref<Record<string, PageHashAction>>({})
|
||||
|
||||
function getPageHashAction(
|
||||
pageHash: components['schemas']['PageHashUnknownDto'],
|
||||
): PageHashAction | undefined {
|
||||
return updateRequests.value[pageHash.hash]
|
||||
}
|
||||
|
||||
async function updateHashAction(
|
||||
pageHash: components['schemas']['PageHashUnknownDto'],
|
||||
newAction: PageHashAction,
|
||||
invalidateCache: boolean = true,
|
||||
) {
|
||||
updateRequests.value[pageHash.hash] = newAction
|
||||
|
||||
return useMutation({
|
||||
mutation: () =>
|
||||
komgaClient.PUT('/api/v1/page-hashes', {
|
||||
body: {
|
||||
...pageHash,
|
||||
action: newAction,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mutateAsync()
|
||||
.then(() => {
|
||||
if (selectedHashes.value.includes(pageHash))
|
||||
selectedHashes.value.splice(selectedHashes.value.indexOf(pageHash), 1)
|
||||
if (invalidateCache)
|
||||
void useQueryCache().invalidateQueries({ key: [QUERY_KEYS_PAGE_HASHES.unknown] })
|
||||
})
|
||||
.catch((error) =>
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message || intl.formatMessage(commonMessages.networkError),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function updateHashActions(
|
||||
pageHashes: components['schemas']['PageHashUnknownDto'][],
|
||||
newAction: PageHashAction,
|
||||
) {
|
||||
const updates = pageHashes.map((it) => updateHashAction(it, newAction, false))
|
||||
await Promise.allSettled(updates)
|
||||
|
||||
void useQueryCache().invalidateQueries({ key: [QUERY_KEYS_PAGE_HASHES.unknown] })
|
||||
}
|
||||
|
||||
const actionOptions = [
|
||||
{
|
||||
title: intl.formatMessage(pageHashActionMessages[PageHashActionEnum.DELETE_AUTO]),
|
||||
value: PageHashActionEnum.DELETE_AUTO,
|
||||
icon: 'i-mdi:robot',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage(pageHashActionMessages[PageHashActionEnum.DELETE_MANUAL]),
|
||||
value: PageHashActionEnum.DELETE_MANUAL,
|
||||
icon: 'i-mdi:hand-back-right',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage(pageHashActionMessages[PageHashActionEnum.IGNORE]),
|
||||
value: PageHashActionEnum.IGNORE,
|
||||
icon: 'i-mdi:cancel',
|
||||
},
|
||||
]
|
||||
//endregion
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -21,6 +21,18 @@ export function mockPageHashesKnown(count: number): components['schemas']['PageH
|
|||
})
|
||||
}
|
||||
|
||||
export function mockPageHashesUnknown(
|
||||
count: number,
|
||||
): components['schemas']['PageHashUnknownDto'][] {
|
||||
return [...Array(count).keys()].map((index) => {
|
||||
return {
|
||||
hash: `UNKN${index}`,
|
||||
size: 1234 * (index + 1),
|
||||
matchCount: index * 2,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function mockPageHashMatches(count: number): components['schemas']['PageHashMatchDto'][] {
|
||||
return [...Array(count).keys()].map((index) => {
|
||||
return {
|
||||
|
|
@ -34,6 +46,8 @@ export function mockPageHashMatches(count: number): components['schemas']['PageH
|
|||
})
|
||||
}
|
||||
|
||||
const knownHashes: string[] = []
|
||||
|
||||
export const pageHashesHandlers = [
|
||||
httpTyped.get('/api/v1/page-hashes', ({ query, response }) => {
|
||||
let data = mockPageHashesKnown(50)
|
||||
|
|
@ -46,6 +60,15 @@ export const pageHashesHandlers = [
|
|||
),
|
||||
)
|
||||
}),
|
||||
httpTyped.get('/api/v1/page-hashes/unknown', ({ query, response }) => {
|
||||
const data = mockPageHashesUnknown(50).filter((it) => !knownHashes.includes(it.hash))
|
||||
return response(200).json(
|
||||
mockPage(
|
||||
data,
|
||||
new PageRequest(Number(query.get('page')), Number(query.get('size')), query.getAll('sort')),
|
||||
),
|
||||
)
|
||||
}),
|
||||
httpTyped.get('/api/v1/page-hashes/{pageHash}', ({ params, query, response }) => {
|
||||
const hash = params.pageHash
|
||||
const data = mockPageHashMatches(Number(hash.substring(4)) * 2)
|
||||
|
|
@ -56,7 +79,10 @@ export const pageHashesHandlers = [
|
|||
),
|
||||
)
|
||||
}),
|
||||
httpTyped.put('/api/v1/page-hashes', ({ response }) => {
|
||||
httpTyped.put('/api/v1/page-hashes', async ({ request, response }) => {
|
||||
const body = await request.json()
|
||||
knownHashes.push(body.hash)
|
||||
|
||||
return response(202).empty()
|
||||
}),
|
||||
httpTyped.post('/api/v1/page-hashes/{pageHash}/delete-all', ({ response }) => {
|
||||
|
|
@ -82,4 +108,27 @@ export const pageHashesHandlers = [
|
|||
}),
|
||||
)
|
||||
}),
|
||||
httpTyped.get(
|
||||
'/api/v1/page-hashes/unknown/{pageHash}/thumbnail',
|
||||
async ({ params, response }) => {
|
||||
const hash = params.pageHash
|
||||
|
||||
// use landscape image for some images
|
||||
const landscape = Number(hash.slice(-1)) % 2 === 0
|
||||
|
||||
// Get an ArrayBuffer from reading the file from disk or fetching it.
|
||||
const buffer = await fetch(landscape ? mockThumbnailLandscapeUrl : mockThumbnailUrl).then(
|
||||
(response) => response.arrayBuffer(),
|
||||
)
|
||||
|
||||
return response.untyped(
|
||||
HttpResponse.arrayBuffer(buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/jpg',
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
<template>
|
||||
<h1>Unknown</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="pa-0 pa-sm-4 h-100 h-sm-auto"
|
||||
>
|
||||
<PageHashUnknownTable />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
|||
Loading…
Reference in a new issue