mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 11:36:05 +00:00
feat: scan file for c2pa content (#7297)
* feat: scan file for c2pa content * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
co-authored by
Prospector
parent
1007b7b69e
commit
a921cfdb7a
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { ButtonLink, NewModal } from '@modrinth/ui'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { fileDeclaresAi } from '~/helpers/c2pa.ts'
|
||||
|
||||
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
|
||||
const isScanning = ref(false)
|
||||
|
||||
const fileUrl = ref<string | null>(null)
|
||||
const totalFiles = ref(0)
|
||||
const checkedFiles = ref(0)
|
||||
const aiFiles = ref<string[]>([])
|
||||
|
||||
async function scanFile(url: string) {
|
||||
if (isScanning.value) return
|
||||
|
||||
fileUrl.value = url
|
||||
isScanning.value = true
|
||||
totalFiles.value = 0
|
||||
checkedFiles.value = 0
|
||||
aiFiles.value = []
|
||||
|
||||
try {
|
||||
const { BlobReader, ZipReader } = await import('@zip.js/zip.js')
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch file: ${response.statusText}`)
|
||||
}
|
||||
const blob = await response.blob()
|
||||
|
||||
const reader = new ZipReader(new BlobReader(blob))
|
||||
const entries = await reader.getEntries()
|
||||
|
||||
totalFiles.value = entries.length
|
||||
|
||||
for (const entry of entries) {
|
||||
checkedFiles.value++
|
||||
|
||||
if (entry.directory) continue
|
||||
if (!entry.filename.endsWith('.png')) continue
|
||||
|
||||
const buffer = await entry.arrayBuffer()
|
||||
const isAiFile = await fileDeclaresAi(
|
||||
new File([buffer], entry.filename, { type: 'image/png' }),
|
||||
)
|
||||
|
||||
if (isAiFile) {
|
||||
aiFiles.value.push(entry.filename)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isScanning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function createSlicerUrl(path: string) {
|
||||
const downloadUrl = new URL(fileUrl.value!)
|
||||
const fileName = downloadUrl.pathname.split('/').pop()
|
||||
|
||||
return `https://slicer.run/?file=${encodeURIComponent(`${fileName}/${path}`)}&url=${encodeURIComponent(downloadUrl.toString())}`
|
||||
}
|
||||
|
||||
function openC2paModal(url: string) {
|
||||
modalRef.value?.show()
|
||||
void scanFile(url)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modalRef.value?.hide()
|
||||
}
|
||||
|
||||
defineExpose({ openC2paModal, hide })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal ref="modalRef" width="40vw" :disable-close="isScanning">
|
||||
<template #title>
|
||||
<span class="text-2xl font-semibold text-contrast">C2PA Scan Info</span>
|
||||
</template>
|
||||
|
||||
<div class="w-full">
|
||||
<div v-if="isScanning" class="flex items-center justify-center">
|
||||
<span class="rounded-xl bg-highlight-blue px-4 py-1"
|
||||
>Scanning {{ checkedFiles }}/{{ totalFiles }} files.</span
|
||||
>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="aiFiles.length === 0" class="flex items-center justify-center">
|
||||
<span class="rounded-xl bg-highlight-green px-4 py-1">No AI-generated files found.</span>
|
||||
</div>
|
||||
|
||||
<div v-if="aiFiles.length > 0" class="flex flex-col gap-1">
|
||||
<span class="ml-2 text-xl font-semibold text-contrast"
|
||||
>AI-generated files ({{ aiFiles.length }})</span
|
||||
>
|
||||
<div
|
||||
v-for="file in aiFiles"
|
||||
:key="file"
|
||||
class="flex flex-row flex-wrap items-center justify-between gap-2 rounded-2xl bg-surface-2 p-4 font-semibold text-secondary"
|
||||
>
|
||||
{{ file }}
|
||||
<ButtonLink :href="createSlicerUrl(file)" target="_blank">
|
||||
Open <ExternalIcon />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
@@ -12,6 +12,8 @@
|
||||
ref="create-project-version-modal"
|
||||
></CreateProjectVersionModal>
|
||||
|
||||
<ProjectC2paScanModal ref="project-c2pa-scan-modal"></ProjectC2paScanModal>
|
||||
|
||||
<ConfirmModal
|
||||
v-if="currentMember"
|
||||
ref="deleteVersionModal"
|
||||
@@ -156,6 +158,14 @@
|
||||
auth.user ? reportVersion(version.id) : navigateTo(getSignInRouteObj(route)),
|
||||
shown: !currentMember,
|
||||
},
|
||||
{ type: 'divider', shown: isStaff(auth.user) },
|
||||
{
|
||||
id: 'view-c2pa-info',
|
||||
label: 'View C2PA info',
|
||||
tone: 'orange',
|
||||
action: () => projectC2paScanModal.openC2paModal(createDownloadUrl(version)),
|
||||
shown: isStaff(auth.user),
|
||||
},
|
||||
{ type: 'divider', shown: currentMember || flags.developerMode },
|
||||
{
|
||||
id: 'copy-id',
|
||||
@@ -243,6 +253,10 @@
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Delete
|
||||
</template>
|
||||
<template #view-c2pa-info>
|
||||
<ScanEyeIcon aria-hidden="true" />
|
||||
View C2PA Info
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy ID
|
||||
@@ -279,6 +293,7 @@ import {
|
||||
LinkIcon,
|
||||
MoreVerticalIcon,
|
||||
ReportIcon,
|
||||
ScanEyeIcon,
|
||||
ShareIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
@@ -297,6 +312,7 @@ import { isStaff } from '@modrinth/utils'
|
||||
import { onMounted, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||
import ProjectC2paScanModal from '~/components/ui/moderation/ProjectC2paScanModal.vue'
|
||||
import { getSignInRouteObj } from '~/composables/auth.ts'
|
||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
|
||||
@@ -337,6 +353,7 @@ onMounted(() => {
|
||||
const deleteVersionModal = ref()
|
||||
const selectedVersion = ref(null)
|
||||
const createProjectVersionModal = useTemplateRef('create-project-version-modal')
|
||||
const projectC2paScanModal = useTemplateRef('project-c2pa-scan-modal')
|
||||
|
||||
const handleOpenCreateVersionModal = () => {
|
||||
if (!currentMember.value) return
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
export type AccountChoice = {
|
||||
id: string
|
||||
username: string
|
||||
avatarUrl?: string | null
|
||||
role?: Labrinth.Users.v2.Role | null
|
||||
}
|
||||
Reference in New Issue
Block a user