mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
621 lines
20 KiB
Vue
621 lines
20 KiB
Vue
<template>
|
||
<NewModal
|
||
ref="modal"
|
||
:header="
|
||
reportMode
|
||
? formatMessage(messages.reportSharedInstance)
|
||
: formatMessage(messages.installToPlay)
|
||
"
|
||
:closable="!submitLoading"
|
||
:on-hide="handleHide"
|
||
:max-width="reportMode ? '816px' : '544px'"
|
||
:width="reportMode ? '816px' : '544px'"
|
||
:no-padding="reportMode"
|
||
:scrollable="reportMode"
|
||
>
|
||
<div v-if="preview" class="flex w-full flex-col gap-6" :class="{ 'p-6 pb-2': reportMode }">
|
||
<Admonition v-if="reportMode" type="info">
|
||
<div class="flex flex-col gap-2">
|
||
<p class="m-0">
|
||
<IntlFormatted :message-id="messages.reportDescription">
|
||
<template #rules-link="{ children }">
|
||
<AutoLink class="text-link hover:underline" to="https://modrinth.com/legal/rules">
|
||
<component :is="() => children" />
|
||
</AutoLink>
|
||
</template>
|
||
<template #terms-link="{ children }">
|
||
<AutoLink class="text-link hover:underline" to="https://modrinth.com/legal/terms">
|
||
<component :is="() => children" />
|
||
</AutoLink>
|
||
</template>
|
||
</IntlFormatted>
|
||
</p>
|
||
<ul class="m-0 list-disc pl-5">
|
||
<li>
|
||
<IntlFormatted :message-id="messages.supportAndBugReports">
|
||
<template #support-link="{ children }">
|
||
<AutoLink class="text-link hover:underline" to="https://support.modrinth.com">
|
||
<component :is="() => children" />
|
||
</AutoLink>
|
||
</template>
|
||
<template #github-link="{ children }">
|
||
<AutoLink
|
||
class="text-link hover:underline"
|
||
to="https://github.com/modrinth/code/issues"
|
||
>
|
||
<component :is="() => children" />
|
||
</AutoLink>
|
||
</template>
|
||
</IntlFormatted>
|
||
</li>
|
||
<li>
|
||
<IntlFormatted :message-id="messages.legalClaims">
|
||
<template #copyright-link="{ children }">
|
||
<AutoLink
|
||
class="text-link hover:underline"
|
||
to="https://modrinth.com/legal/copyright"
|
||
>
|
||
<component :is="() => children" />
|
||
</AutoLink>
|
||
</template>
|
||
</IntlFormatted>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</Admonition>
|
||
<p v-else class="m-0 text-primary">
|
||
<IntlFormatted
|
||
v-if="creator"
|
||
:message-id="messages.inviteWarningWithCreator"
|
||
:values="{ username: creator.username }"
|
||
>
|
||
<template #creator="{ children }">
|
||
<AutoLink :to="creatorProfileLink" class="font-medium text-contrast hover:underline">
|
||
<Avatar
|
||
:src="creator.avatarUrl"
|
||
:alt="creator.username"
|
||
:tint-by="creator.username"
|
||
size="24px"
|
||
circle
|
||
no-shadow
|
||
class="mr-1 inline-block align-middle"
|
||
/>
|
||
<span><component :is="() => children" /></span>
|
||
</AutoLink>
|
||
</template>
|
||
</IntlFormatted>
|
||
<template v-else>
|
||
{{ formatMessage(messages.inviteWarning) }}
|
||
</template>
|
||
</p>
|
||
<SharedInstanceInstallSummary
|
||
:preview="preview"
|
||
:heading="reportMode ? formatMessage(messages.contentYouAreReporting) : undefined"
|
||
@view-contents="openViewContents"
|
||
/>
|
||
<Transition
|
||
enter-active-class="overflow-hidden transition-all duration-200 ease-out"
|
||
enter-from-class="max-h-0 opacity-0"
|
||
enter-to-class="max-h-[400px] opacity-100"
|
||
leave-active-class="overflow-hidden transition-all duration-200 ease-in"
|
||
leave-from-class="max-h-[400px] opacity-100"
|
||
leave-to-class="max-h-0 opacity-0"
|
||
>
|
||
<div v-if="reportMode" class="flex flex-col gap-6">
|
||
<div class="flex flex-col gap-2.5">
|
||
<span class="font-semibold text-contrast">
|
||
{{ formatMessage(messages.reportReason) }}
|
||
</span>
|
||
<Combobox v-model="reportReason" :options="reportReasonOptions" />
|
||
</div>
|
||
<div class="flex flex-col gap-2.5">
|
||
<span class="font-semibold text-contrast">
|
||
{{ formatMessage(messages.additionalContext) }}
|
||
</span>
|
||
<MarkdownEditor
|
||
v-model="additionalContext"
|
||
:placeholder="formatMessage(messages.additionalContextPlaceholder)"
|
||
:on-image-upload="onImageUpload"
|
||
:min-height="120"
|
||
:max-height="240"
|
||
/>
|
||
</div>
|
||
<div v-if="reportOnly || blockTargetUserId" class="flex flex-col gap-2">
|
||
<Checkbox
|
||
v-if="reportOnly"
|
||
v-model="deleteInstance"
|
||
:label="formatMessage(messages.deleteInstance)"
|
||
/>
|
||
<Checkbox
|
||
v-if="blockTargetUserId"
|
||
v-model="blockUser"
|
||
:label="formatMessage(messages.blockUser)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
<Admonition
|
||
v-if="!reportMode && hasExternalFiles"
|
||
type="warning"
|
||
:header="formatMessage(messages.unknownFilesWarning)"
|
||
>
|
||
{{ formatMessage(messages.unknownFilesDescription) }}
|
||
</Admonition>
|
||
|
||
<div v-if="!reportMode && hasExternalFiles" class="relative w-full">
|
||
<div
|
||
ref="externalFileTable"
|
||
class="max-h-[242px] overflow-y-auto rounded-2xl"
|
||
@scroll="checkTableScrollState"
|
||
>
|
||
<Table
|
||
:columns="externalFileColumns"
|
||
:data="externalFileRows"
|
||
row-key="id"
|
||
virtualized
|
||
:virtual-row-height="48"
|
||
class="shadow-sm"
|
||
>
|
||
<template #cell-name="{ value }">
|
||
<span class="block truncate" :title="String(value)">{{ value }}</span>
|
||
</template>
|
||
</Table>
|
||
</div>
|
||
<Transition
|
||
enter-active-class="transition-all duration-200 ease-out"
|
||
enter-from-class="opacity-0 max-h-0"
|
||
enter-to-class="opacity-100 max-h-2"
|
||
leave-active-class="transition-all duration-200 ease-in"
|
||
leave-from-class="opacity-100 max-h-2"
|
||
leave-to-class="opacity-0 max-h-0"
|
||
>
|
||
<div
|
||
v-if="showTableTopFade"
|
||
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-2 bg-gradient-to-b from-bg-raised to-transparent"
|
||
/>
|
||
</Transition>
|
||
<Transition
|
||
enter-active-class="transition-all duration-200 ease-out"
|
||
enter-from-class="opacity-0 max-h-0"
|
||
enter-to-class="opacity-100 max-h-2"
|
||
leave-active-class="transition-all duration-200 ease-in"
|
||
leave-from-class="opacity-100 max-h-2"
|
||
leave-to-class="opacity-0 max-h-0"
|
||
>
|
||
<div
|
||
v-if="showTableBottomFade"
|
||
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-2 bg-gradient-to-t from-bg-raised to-transparent"
|
||
/>
|
||
</Transition>
|
||
</div>
|
||
|
||
<p v-if="!reportMode && hasExternalFiles" class="m-0 text-primary">
|
||
{{ formatMessage(messages.reviewedFiles) }}
|
||
</p>
|
||
<div v-if="!reportMode" class="flex w-full items-center justify-between gap-2">
|
||
<Button type="quiet" color="red" @click="reportMode = true">
|
||
<ReportIcon />{{ formatMessage(commonMessages.reportButton) }}
|
||
</Button>
|
||
<div class="flex items-center gap-2">
|
||
<template v-if="hasExternalFiles">
|
||
<Button type="quiet" color="orange" @click="accept">
|
||
{{ formatMessage(messages.installAnyway) }}
|
||
</Button>
|
||
<Button type="colored" color="brand" @click="handleCancel">
|
||
<BanIcon />{{ formatMessage(messages.dontInstall) }}
|
||
</Button>
|
||
</template>
|
||
<template v-else>
|
||
<Button type="outlined" class="!border" @click="handleCancel">
|
||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||
</Button>
|
||
<Button type="colored" color="brand" @click="accept">
|
||
<DownloadIcon />{{ formatMessage(messages.installButton) }}
|
||
</Button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<template v-if="reportMode" #actions>
|
||
<div class="flex justify-end gap-2">
|
||
<Button type="outlined" class="!border" :disabled="submitLoading" @click="handleCancel">
|
||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||
</Button>
|
||
<Button type="colored" color="brand" :disabled="!canSubmitReport" @click="submitReport">
|
||
<SpinnerIcon v-if="submitLoading" class="animate-spin" />
|
||
<SendIcon v-else />
|
||
{{ formatMessage(commonMessages.reportButton) }}
|
||
</Button>
|
||
</div>
|
||
</template>
|
||
</NewModal>
|
||
<ModpackContentModal
|
||
ref="contentModal"
|
||
:header="formatMessage(messages.sharedInstanceContent)"
|
||
:modpack-name="preview?.name ?? ''"
|
||
:modpack-icon-url="preview?.iconUrl ?? undefined"
|
||
/>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Labrinth } from '@modrinth/api-client'
|
||
import { BanIcon, DownloadIcon, ReportIcon, SendIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||
import { Button } from '@modrinth/ui'
|
||
import {
|
||
Admonition,
|
||
AutoLink,
|
||
Avatar,
|
||
blockedUsersQueryKey,
|
||
Checkbox,
|
||
Combobox,
|
||
type ComboboxOption,
|
||
commonMessages,
|
||
defineMessages,
|
||
injectAuth,
|
||
injectModrinthClient,
|
||
injectNotificationManager,
|
||
IntlFormatted,
|
||
MarkdownEditor,
|
||
ModpackContentModal,
|
||
NewModal,
|
||
Table,
|
||
type TableColumn,
|
||
useScrollIndicator,
|
||
useVIntl,
|
||
} from '@modrinth/ui'
|
||
import { useQueryClient } from '@tanstack/vue-query'
|
||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||
import { computed, nextTick, ref } from 'vue'
|
||
|
||
import { config } from '@/config'
|
||
import { toError } from '@/helpers/errors'
|
||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||
import { create_report } from '@/helpers/reports'
|
||
import { block_user } from '@/helpers/users'
|
||
|
||
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
|
||
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
|
||
|
||
type ExternalFileColumn = 'name'
|
||
type ExternalFileRow = {
|
||
id: string
|
||
name: string
|
||
}
|
||
type SharedInstanceCreator = {
|
||
id: string | null
|
||
username: string
|
||
avatarUrl: string | null
|
||
}
|
||
|
||
const modal = ref<InstanceType<typeof NewModal>>()
|
||
const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
|
||
const externalFileTable = ref<HTMLElement | null>(null)
|
||
const preview = ref<SharedInstanceInstallPreview | null>(null)
|
||
const creator = ref<SharedInstanceCreator | null>(null)
|
||
const install = ref<() => void | Promise<void>>(() => {})
|
||
const reportMode = ref(false)
|
||
const reportOnly = ref(false)
|
||
type ReportReason = 'malicious' | 'inappropriate' | 'spam'
|
||
const reportReason = ref<ReportReason>('malicious')
|
||
const additionalContext = ref('')
|
||
const deleteInstance = ref(true)
|
||
const blockUser = ref(true)
|
||
const blockTargetUserId = ref<string | null>(null)
|
||
const submitLoading = ref(false)
|
||
const uploadedImageIDs = ref<string[]>([])
|
||
const emit = defineEmits<{
|
||
reported: [deleteInstance: boolean]
|
||
}>()
|
||
const { formatMessage } = useVIntl()
|
||
const auth = injectAuth()
|
||
const client = injectModrinthClient()
|
||
const queryClient = useQueryClient()
|
||
const { addNotification, handleError } = injectNotificationManager()
|
||
const { load } = useSharedInstancePreviewContent()
|
||
const {
|
||
showTopFade: showTableTopFade,
|
||
showBottomFade: showTableBottomFade,
|
||
checkScrollState: checkTableScrollState,
|
||
forceCheck: forceCheckTableScroll,
|
||
} = useScrollIndicator(externalFileTable)
|
||
const hasExternalFiles = computed(() => Boolean(preview.value?.externalFiles.length))
|
||
const externalFileRows = computed<ExternalFileRow[]>(() =>
|
||
(preview.value?.externalFiles ?? [])
|
||
.map((file, index) => ({
|
||
id: `${index}-${file.fileType}-${file.fileName}`,
|
||
name: file.fileName,
|
||
}))
|
||
.sort((left, right) => left.name.localeCompare(right.name)),
|
||
)
|
||
const reportReasonOptions = computed<ComboboxOption<ReportReason>[]>(() => [
|
||
{ value: 'malicious', label: formatMessage(messages.maliciousReason) },
|
||
{ value: 'inappropriate', label: formatMessage(messages.inappropriateReason) },
|
||
{ value: 'spam', label: formatMessage(messages.spamReason) },
|
||
])
|
||
const canSubmitReport = computed(
|
||
() => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value,
|
||
)
|
||
const creatorProfileLink = computed(() => {
|
||
const username = creator.value?.username
|
||
return username
|
||
? () => openUrl(`${config.siteUrl}/user/${encodeURIComponent(username)}`)
|
||
: undefined
|
||
})
|
||
|
||
async function accept() {
|
||
hide()
|
||
try {
|
||
await install.value()
|
||
} catch (error) {
|
||
console.error('Failed to install shared instance:', error)
|
||
}
|
||
}
|
||
async function openViewContents() {
|
||
if (!preview.value) return
|
||
contentModal.value?.showLoading()
|
||
try {
|
||
contentModal.value?.show(await load(preview.value))
|
||
} catch (error) {
|
||
console.error('Failed to load shared instance contents:', error)
|
||
contentModal.value?.show([])
|
||
}
|
||
}
|
||
async function submitReport() {
|
||
const reportPreview = preview.value
|
||
const body = additionalContext.value.trim()
|
||
if (!reportPreview || !body || submitLoading.value) return
|
||
|
||
submitLoading.value = true
|
||
try {
|
||
const uploadedImages = uploadedImageIDs.value.slice(-10)
|
||
const blockTarget = blockUser.value ? blockTargetUserId.value : null
|
||
const [reportResult, blockResult] = await Promise.allSettled([
|
||
create_report({
|
||
report_type: reportReason.value,
|
||
item_type: 'shared-instance',
|
||
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
|
||
body,
|
||
uploaded_images: uploadedImages,
|
||
}),
|
||
blockTarget ? block_user(blockTarget) : Promise.resolve(),
|
||
])
|
||
|
||
if (blockTarget) {
|
||
if (blockResult.status === 'fulfilled') {
|
||
blockUser.value = false
|
||
const authUserId = auth.user.value?.id
|
||
if (authUserId) {
|
||
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
|
||
blockedUsersQueryKey(authUserId),
|
||
(blockedUsers = []) =>
|
||
blockedUsers.includes(blockTarget) ? blockedUsers : [...blockedUsers, blockTarget],
|
||
)
|
||
}
|
||
addNotification({
|
||
type: 'success',
|
||
title: formatMessage(messages.userBlocked),
|
||
})
|
||
} else {
|
||
handleError(toError(blockResult.reason))
|
||
}
|
||
}
|
||
|
||
if (reportResult.status === 'rejected') throw reportResult.reason
|
||
|
||
const shouldDeleteInstance = reportOnly.value && deleteInstance.value
|
||
hide()
|
||
addNotification({
|
||
type: 'success',
|
||
title: formatMessage(messages.reportSubmitted),
|
||
})
|
||
emit('reported', shouldDeleteInstance)
|
||
} catch (error) {
|
||
handleError(toError(error))
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
async function onImageUpload(file: File) {
|
||
const imageExtensionByType: Partial<Record<string, Labrinth.Images.v3.ImageExtension>> = {
|
||
'image/gif': 'gif',
|
||
'image/jpeg': 'jpeg',
|
||
'image/png': 'png',
|
||
'image/webp': 'webp',
|
||
}
|
||
const extension = imageExtensionByType[file.type]
|
||
if (!extension) {
|
||
throw new Error(formatMessage(messages.invalidImageType))
|
||
}
|
||
if (file.size > 1024 * 1024) {
|
||
throw new Error(formatMessage(messages.imageTooLarge))
|
||
}
|
||
|
||
const image = await client.labrinth.images_v3.uploadImage(file, extension, {
|
||
context: 'report',
|
||
}).promise
|
||
uploadedImageIDs.value.push(image.id)
|
||
return image.url
|
||
}
|
||
function handleCancel() {
|
||
if (reportMode.value && !reportOnly.value) {
|
||
reportMode.value = false
|
||
void nextTick(() => forceCheckTableScroll())
|
||
return
|
||
}
|
||
hide()
|
||
}
|
||
function handleHide() {
|
||
resetReportState()
|
||
creator.value = null
|
||
}
|
||
function resetReportState() {
|
||
reportMode.value = false
|
||
reportOnly.value = false
|
||
reportReason.value = 'malicious'
|
||
additionalContext.value = ''
|
||
deleteInstance.value = true
|
||
blockUser.value = true
|
||
blockTargetUserId.value = null
|
||
submitLoading.value = false
|
||
uploadedImageIDs.value = []
|
||
}
|
||
function show(
|
||
previewValue: SharedInstanceInstallPreview,
|
||
installValue: () => void | Promise<void>,
|
||
creatorValue?: SharedInstanceCreator,
|
||
event?: MouseEvent,
|
||
) {
|
||
resetReportState()
|
||
creator.value = creatorValue ?? null
|
||
blockTargetUserId.value = creatorValue?.id ?? null
|
||
install.value = installValue
|
||
showPreview(previewValue, event)
|
||
}
|
||
function showReport(
|
||
previewValue: SharedInstanceInstallPreview,
|
||
blockTargetUserIdValue?: string | null,
|
||
event?: MouseEvent,
|
||
) {
|
||
resetReportState()
|
||
creator.value = null
|
||
blockTargetUserId.value = blockTargetUserIdValue ?? null
|
||
reportMode.value = true
|
||
reportOnly.value = true
|
||
install.value = () => {}
|
||
showPreview(previewValue, event)
|
||
}
|
||
function showPreview(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
|
||
preview.value = previewValue
|
||
modal.value?.show(event)
|
||
void nextTick(() => forceCheckTableScroll())
|
||
}
|
||
function hide() {
|
||
modal.value?.hide()
|
||
}
|
||
|
||
const messages = defineMessages({
|
||
installToPlay: { id: 'app.modal.install-to-play.header', defaultMessage: 'Install to play' },
|
||
reportSharedInstance: {
|
||
id: 'app.modal.install-to-play.report-shared-instance-header',
|
||
defaultMessage: 'Report shared instance',
|
||
},
|
||
reportSubmitted: {
|
||
id: 'app.modal.install-to-play.report-submitted',
|
||
defaultMessage: 'Report submitted',
|
||
},
|
||
sharedInstanceContent: {
|
||
id: 'app.modal.install-to-play.shared-instance-content',
|
||
defaultMessage: 'Shared instance content',
|
||
},
|
||
inviteWarning: {
|
||
id: 'app.modal.install-to-play.invite-warning',
|
||
defaultMessage:
|
||
'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.',
|
||
},
|
||
inviteWarningWithCreator: {
|
||
id: 'app.modal.install-to-play.invite-warning-with-creator',
|
||
defaultMessage:
|
||
'This invite was created by <creator>{username}</creator>, not Modrinth. Only accept invites from people you trust.',
|
||
},
|
||
reportDescription: {
|
||
id: 'app.modal.install-to-play.report-description',
|
||
defaultMessage:
|
||
'Use this form to report instances that may violate our <rules-link>Rules</rules-link> or <terms-link>Terms of Use</terms-link>.',
|
||
},
|
||
supportAndBugReports: {
|
||
id: 'app.modal.install-to-play.report-support-and-bugs',
|
||
defaultMessage:
|
||
'For support requests, contact our <support-link>support team</support-link>. For bug reports, open a <github-link>GitHub issue</github-link>.',
|
||
},
|
||
legalClaims: {
|
||
id: 'app.modal.install-to-play.report-legal-claims',
|
||
defaultMessage:
|
||
'For DMCA notices or other legal claims, see our <copyright-link>Copyright Policy</copyright-link>.',
|
||
},
|
||
contentYouAreReporting: {
|
||
id: 'app.modal.install-to-play.content-you-are-reporting',
|
||
defaultMessage: 'Instance you’re reporting',
|
||
},
|
||
reportReason: {
|
||
id: 'app.modal.install-to-play.report-reason',
|
||
defaultMessage: 'Which rule does this instance violate?',
|
||
},
|
||
maliciousReason: {
|
||
id: 'app.modal.install-to-play.report-reason.malicious',
|
||
defaultMessage: 'Malicious',
|
||
},
|
||
inappropriateReason: {
|
||
id: 'app.modal.install-to-play.report-reason.inappropriate',
|
||
defaultMessage: 'Inappropriate',
|
||
},
|
||
spamReason: {
|
||
id: 'app.modal.install-to-play.report-reason.spam',
|
||
defaultMessage: 'Spam',
|
||
},
|
||
additionalContext: {
|
||
id: 'app.modal.install-to-play.additional-context',
|
||
defaultMessage: 'Additional context',
|
||
},
|
||
additionalContextPlaceholder: {
|
||
id: 'app.modal.install-to-play.additional-context-placeholder',
|
||
defaultMessage: 'Include links and images if possible and relevant',
|
||
},
|
||
invalidImageType: {
|
||
id: 'app.modal.install-to-play.report-image-invalid-type',
|
||
defaultMessage: 'File is not an accepted image type',
|
||
},
|
||
imageTooLarge: {
|
||
id: 'app.modal.install-to-play.report-image-too-large',
|
||
defaultMessage: 'File exceeds the 1 MiB size limit',
|
||
},
|
||
deleteInstance: {
|
||
id: 'app.modal.install-to-play.delete-instance',
|
||
defaultMessage: 'Delete instance',
|
||
},
|
||
blockUser: {
|
||
id: 'app.modal.install-to-play.block-user',
|
||
defaultMessage: 'Block user',
|
||
},
|
||
userBlocked: {
|
||
id: 'app.modal.install-to-play.user-blocked',
|
||
defaultMessage: 'User blocked',
|
||
},
|
||
unknownFilesWarning: {
|
||
id: 'app.modal.install-to-play.unknown-files-warning',
|
||
defaultMessage: 'Unknown files warning',
|
||
},
|
||
unknownFilesDescription: {
|
||
id: 'app.modal.install-to-play.shared-instance-unknown-files-description',
|
||
defaultMessage:
|
||
'This shared instance contains files that aren’t published on Modrinth. We strongly recommend only installing files from sources you trust.',
|
||
},
|
||
unrecognizedFiles: {
|
||
id: 'app.modal.install-to-play.unrecognized-files',
|
||
defaultMessage: 'Unrecognized files',
|
||
},
|
||
reviewedFiles: {
|
||
id: 'app.modal.install-to-play.reviewed-files',
|
||
defaultMessage: "Files that aren't published to Modrinth aren't reviewed.",
|
||
},
|
||
installAnyway: {
|
||
id: 'app.modal.install-to-play.install-anyway',
|
||
defaultMessage: 'Install anyway',
|
||
},
|
||
dontInstall: {
|
||
id: 'app.modal.install-to-play.external-files-dont-install',
|
||
defaultMessage: "Don't install",
|
||
},
|
||
installButton: { id: 'app.modal.install-to-play.install-button', defaultMessage: 'Install' },
|
||
})
|
||
|
||
const externalFileColumns = computed<TableColumn<ExternalFileColumn>[]>(() => [
|
||
{
|
||
key: 'name',
|
||
label: formatMessage(messages.unrecognizedFiles),
|
||
cellClass: '!h-12',
|
||
},
|
||
])
|
||
|
||
defineExpose({ show, showReport, hide })
|
||
</script>
|