mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
566 lines
14 KiB
Vue
566 lines
14 KiB
Vue
<template>
|
|
<div>
|
|
<h2 v-if="modPackData" class="m-0 mb-2 text-lg font-extrabold">
|
|
Modpack permissions ({{ Math.min(modPackData.length, currentIndex + 1) }} /
|
|
{{ modPackData.length }})
|
|
</h2>
|
|
|
|
<div v-if="!modPackData">Loading data...</div>
|
|
|
|
<div v-else-if="modPackData.length === 0">
|
|
<p>All permissions already obtained.</p>
|
|
</div>
|
|
|
|
<div v-else-if="!modPackData[currentIndex]">
|
|
<p>All permission checks complete!</p>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<div v-if="modPackData[currentIndex].type === 'unknown'">
|
|
<p>What is the approval type of {{ modPackData[currentIndex].file_name }}?</p>
|
|
<div class="input-group">
|
|
<Button
|
|
v-for="(option, index) in fileApprovalTypes"
|
|
:key="index"
|
|
:type="
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') &&
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') !== 'standard'
|
|
? 'colored'
|
|
: 'base'
|
|
"
|
|
:color="
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') &&
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') !== 'standard'
|
|
? (modPackData[currentIndex].status === option.id ? 'brand' : 'standard') ===
|
|
'medal-promo'
|
|
? 'medal_promotion'
|
|
: modPackData[currentIndex].status === option.id
|
|
? 'brand'
|
|
: 'standard'
|
|
: undefined
|
|
"
|
|
@click="setStatus(currentIndex, option.id)"
|
|
>
|
|
{{ option.name }}
|
|
</Button>
|
|
</div>
|
|
<div v-if="modPackData[currentIndex].status !== 'unidentified'" class="flex flex-col gap-1">
|
|
<label for="proof">
|
|
<span class="label__title">Proof</span>
|
|
</label>
|
|
<StyledInput
|
|
id="proof"
|
|
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).proof"
|
|
autocomplete="off"
|
|
placeholder="Enter proof of status..."
|
|
@update:model-value="persistAll()"
|
|
/>
|
|
<label for="link">
|
|
<span class="label__title">Link</span>
|
|
</label>
|
|
<StyledInput
|
|
id="link"
|
|
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).url"
|
|
autocomplete="off"
|
|
placeholder="Enter link of project..."
|
|
@update:model-value="persistAll()"
|
|
/>
|
|
<label for="title">
|
|
<span class="label__title">Title</span>
|
|
</label>
|
|
<StyledInput
|
|
id="title"
|
|
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).title"
|
|
autocomplete="off"
|
|
placeholder="Enter title of project..."
|
|
@update:model-value="persistAll()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="modPackData[currentIndex].type === 'flame'">
|
|
<p>
|
|
What is the approval type of {{ modPackData[currentIndex].title }} (<a
|
|
:href="modPackData[currentIndex].url"
|
|
target="_blank"
|
|
class="text-link"
|
|
>{{ modPackData[currentIndex].url }}</a
|
|
>)?
|
|
</p>
|
|
<div class="input-group">
|
|
<Button
|
|
v-for="(option, index) in fileApprovalTypes"
|
|
:key="index"
|
|
:type="
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') &&
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') !== 'standard'
|
|
? 'colored'
|
|
: 'base'
|
|
"
|
|
:color="
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') &&
|
|
(modPackData[currentIndex].status === option.id ? 'brand' : 'standard') !== 'standard'
|
|
? (modPackData[currentIndex].status === option.id ? 'brand' : 'standard') ===
|
|
'medal-promo'
|
|
? 'medal_promotion'
|
|
: modPackData[currentIndex].status === option.id
|
|
? 'brand'
|
|
: 'standard'
|
|
: undefined
|
|
"
|
|
@click="setStatus(currentIndex, option.id)"
|
|
>
|
|
{{ option.name }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="
|
|
['unidentified', 'no', 'with-attribution'].includes(
|
|
modPackData[currentIndex].status || '',
|
|
)
|
|
"
|
|
>
|
|
<p v-if="modPackData[currentIndex].status === 'unidentified'">
|
|
Does this project provide identification and permission for
|
|
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
|
>?
|
|
</p>
|
|
<p v-else-if="modPackData[currentIndex].status === 'with-attribution'">
|
|
Does this project provide attribution for
|
|
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
|
>?
|
|
</p>
|
|
<p v-else>
|
|
Does this project provide proof of permission for
|
|
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
|
>?
|
|
</p>
|
|
<div class="input-group">
|
|
<Button
|
|
v-for="(option, index) in filePermissionTypes"
|
|
:key="index"
|
|
:type="
|
|
(modPackData[currentIndex].approved === option.id ? 'brand' : 'standard') &&
|
|
(modPackData[currentIndex].approved === option.id ? 'brand' : 'standard') !==
|
|
'standard'
|
|
? 'colored'
|
|
: 'base'
|
|
"
|
|
:color="
|
|
(modPackData[currentIndex].approved === option.id ? 'brand' : 'standard') &&
|
|
(modPackData[currentIndex].approved === option.id ? 'brand' : 'standard') !==
|
|
'standard'
|
|
? (modPackData[currentIndex].approved === option.id ? 'brand' : 'standard') ===
|
|
'medal-promo'
|
|
? 'medal_promotion'
|
|
: modPackData[currentIndex].approved === option.id
|
|
? 'brand'
|
|
: 'standard'
|
|
: undefined
|
|
"
|
|
@click="setApproval(currentIndex, option.id)"
|
|
>
|
|
{{ option.name }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 flex gap-2">
|
|
<Button :disabled="currentIndex <= 0" @click="goToPrevious">
|
|
<LeftArrowIcon aria-hidden="true" />
|
|
Previous
|
|
</Button>
|
|
<Button
|
|
v-if="modPackData && currentIndex < modPackData.length"
|
|
type="colored"
|
|
color="blue"
|
|
:disabled="!canGoNext"
|
|
@click="goToNext"
|
|
>
|
|
<RightArrowIcon aria-hidden="true" />
|
|
{{ currentIndex + 1 >= modPackData.length ? 'Complete' : 'Next' }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
|
|
import { Button, StyledInput } from '@modrinth/ui'
|
|
import type {
|
|
ModerationFlameModpackItem,
|
|
ModerationJudgements,
|
|
ModerationModpackItem,
|
|
ModerationModpackPermissionApprovalType,
|
|
ModerationModpackResponse,
|
|
ModerationPermissionType,
|
|
ModerationUnknownModpackItem,
|
|
} from '@modrinth/utils'
|
|
import { useLocalStorage, useSessionStorage } from '@vueuse/core'
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
|
|
const props = defineProps<{
|
|
projectId: string
|
|
projectUpdated: string
|
|
modelValue?: ModerationJudgements
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
complete: []
|
|
'update:modelValue': [judgements: ModerationJudgements]
|
|
}>()
|
|
|
|
const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
|
`modpack-permissions-${props.projectId}`,
|
|
null,
|
|
{
|
|
serializer: {
|
|
read: (v: any) => (v ? JSON.parse(v) : null),
|
|
write: (v: any) => JSON.stringify(v),
|
|
},
|
|
},
|
|
)
|
|
|
|
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0)
|
|
|
|
const modPackData = useSessionStorage<ModerationModpackItem[] | null>(
|
|
`modpack-permissions-data-${props.projectId}`,
|
|
null,
|
|
{
|
|
serializer: {
|
|
read: (v: any) => (v ? JSON.parse(v) : null),
|
|
write: (v: any) => JSON.stringify(v),
|
|
},
|
|
},
|
|
)
|
|
const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
|
`modpack-permissions-permanent-no-${props.projectId}`,
|
|
[],
|
|
{
|
|
serializer: {
|
|
read: (v: any) => (v ? JSON.parse(v) : []),
|
|
write: (v: any) => JSON.stringify(v),
|
|
},
|
|
},
|
|
)
|
|
const cachedProjectUpdated = useSessionStorage<string | null>(
|
|
`modpack-permissions-updated-${props.projectId}`,
|
|
null,
|
|
)
|
|
const currentIndex = ref(0)
|
|
|
|
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
|
{
|
|
id: 'yes',
|
|
name: 'Yes',
|
|
},
|
|
{
|
|
id: 'with-attribution-and-source',
|
|
name: 'With attribution and source',
|
|
},
|
|
{
|
|
id: 'with-attribution',
|
|
name: 'With attribution',
|
|
},
|
|
{
|
|
id: 'no',
|
|
name: 'No',
|
|
},
|
|
{
|
|
id: 'permanent-no',
|
|
name: 'Permanent no',
|
|
},
|
|
{
|
|
id: 'unidentified',
|
|
name: 'Unidentified',
|
|
},
|
|
]
|
|
|
|
const filePermissionTypes: ModerationPermissionType[] = [
|
|
{ id: 'yes', name: 'Yes' },
|
|
{ id: 'no', name: 'No' },
|
|
]
|
|
|
|
function persistAll() {
|
|
persistedModPackData.value = modPackData.value
|
|
persistedIndex.value = currentIndex.value
|
|
}
|
|
|
|
watch(
|
|
modPackData,
|
|
(newValue) => {
|
|
persistedModPackData.value = newValue
|
|
},
|
|
{ deep: true },
|
|
)
|
|
|
|
watch(currentIndex, (newValue) => {
|
|
persistedIndex.value = newValue
|
|
})
|
|
|
|
function loadPersistedData(): void {
|
|
if (persistedModPackData.value) {
|
|
modPackData.value = persistedModPackData.value
|
|
}
|
|
currentIndex.value = persistedIndex.value
|
|
}
|
|
|
|
function clearPersistedData(): void {
|
|
persistedModPackData.value = null
|
|
persistedIndex.value = 0
|
|
}
|
|
|
|
async function fetchModPackData(): Promise<void> {
|
|
try {
|
|
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
|
internal: true,
|
|
})) as ModerationModpackResponse
|
|
|
|
const permanentNoItems: ModerationModpackItem[] = Object.entries(data.identified || {})
|
|
.filter(([_, file]) => file.status === 'permanent-no')
|
|
.map(
|
|
([sha1, file]): ModerationModpackItem => ({
|
|
sha1,
|
|
file_name: file.file_name,
|
|
type: 'identified',
|
|
status: file.status,
|
|
approved: null,
|
|
}),
|
|
)
|
|
.sort((a, b) => a.file_name.localeCompare(b.file_name))
|
|
|
|
permanentNoFiles.value = permanentNoItems
|
|
|
|
const sortedData: ModerationModpackItem[] = [
|
|
...Object.entries(data.identified || {})
|
|
.filter(
|
|
([_, file]) =>
|
|
file.status !== 'yes' &&
|
|
file.status !== 'with-attribution-and-source' &&
|
|
file.status !== 'permanent-no',
|
|
)
|
|
.map(
|
|
([sha1, file]): ModerationModpackItem => ({
|
|
sha1,
|
|
file_name: file.file_name,
|
|
type: 'identified',
|
|
status: file.status,
|
|
approved: null,
|
|
...(file.status === 'unidentified' && {
|
|
proof: '',
|
|
url: '',
|
|
title: '',
|
|
}),
|
|
}),
|
|
)
|
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
|
...Object.entries(data.unknown_files || {})
|
|
.map(
|
|
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
|
sha1,
|
|
file_name: fileName,
|
|
type: 'unknown',
|
|
status: null,
|
|
approved: null,
|
|
proof: '',
|
|
url: '',
|
|
title: '',
|
|
}),
|
|
)
|
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
|
...Object.entries(data.flame_files || {})
|
|
.map(
|
|
([sha1, info]): ModerationFlameModpackItem => ({
|
|
sha1,
|
|
file_name: info.file_name,
|
|
type: 'flame',
|
|
status: null,
|
|
approved: null,
|
|
id: info.id,
|
|
title: info.title || info.file_name,
|
|
url: info.url || `https://www.curseforge.com/minecraft/mc-mods/${info.id}`,
|
|
}),
|
|
)
|
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
|
]
|
|
|
|
if (modPackData.value) {
|
|
const existingMap = new Map(modPackData.value.map((item) => [item.sha1, item]))
|
|
|
|
sortedData.forEach((item) => {
|
|
const existing = existingMap.get(item.sha1)
|
|
if (existing) {
|
|
Object.assign(item, {
|
|
status: existing.status,
|
|
approved: existing.approved,
|
|
...(item.type === 'unknown' && {
|
|
proof: (existing as ModerationUnknownModpackItem).proof || '',
|
|
url: (existing as ModerationUnknownModpackItem).url || '',
|
|
title: (existing as ModerationUnknownModpackItem).title || '',
|
|
}),
|
|
...(item.type === 'flame' && {
|
|
url: (existing as ModerationFlameModpackItem).url || item.url,
|
|
title: (existing as ModerationFlameModpackItem).title || item.title,
|
|
}),
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
modPackData.value = sortedData
|
|
cachedProjectUpdated.value = props.projectUpdated
|
|
persistAll()
|
|
} catch (error) {
|
|
console.error('Failed to fetch modpack data:', error)
|
|
modPackData.value = []
|
|
permanentNoFiles.value = []
|
|
cachedProjectUpdated.value = props.projectUpdated
|
|
persistAll()
|
|
}
|
|
}
|
|
|
|
function goToPrevious(): void {
|
|
if (currentIndex.value > 0) {
|
|
currentIndex.value--
|
|
persistAll()
|
|
}
|
|
}
|
|
|
|
watch(
|
|
modPackData,
|
|
(newValue) => {
|
|
persistedModPackData.value = newValue
|
|
},
|
|
{ deep: true },
|
|
)
|
|
|
|
function goToNext(): void {
|
|
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
|
currentIndex.value++
|
|
|
|
if (currentIndex.value >= modPackData.value.length) {
|
|
const judgements = getJudgements()
|
|
emit('update:modelValue', judgements)
|
|
emit('complete')
|
|
clearPersistedData()
|
|
} else {
|
|
persistAll()
|
|
}
|
|
}
|
|
}
|
|
|
|
function setStatus(index: number, status: ModerationModpackPermissionApprovalType['id']): void {
|
|
if (modPackData.value && modPackData.value[index]) {
|
|
modPackData.value[index].status = status
|
|
modPackData.value[index].approved = null
|
|
persistAll()
|
|
emit('update:modelValue', getJudgements())
|
|
}
|
|
}
|
|
|
|
function setApproval(index: number, approved: ModerationPermissionType['id']): void {
|
|
if (modPackData.value && modPackData.value[index]) {
|
|
modPackData.value[index].approved = approved
|
|
persistAll()
|
|
emit('update:modelValue', getJudgements())
|
|
}
|
|
}
|
|
|
|
const canGoNext = computed(() => {
|
|
if (!modPackData.value || !modPackData.value[currentIndex.value]) return false
|
|
const current = modPackData.value[currentIndex.value]
|
|
return current.status !== null
|
|
})
|
|
|
|
function getJudgements(): ModerationJudgements {
|
|
if (!modPackData.value) return {}
|
|
|
|
const judgements: ModerationJudgements = {}
|
|
|
|
modPackData.value.forEach((item) => {
|
|
if (item.type === 'flame') {
|
|
judgements[item.sha1] = {
|
|
type: 'flame',
|
|
id: item.id,
|
|
status: item.status,
|
|
link: item.url,
|
|
title: item.title,
|
|
file_name: item.file_name,
|
|
}
|
|
} else if (item.type === 'unknown') {
|
|
judgements[item.sha1] = {
|
|
type: 'unknown',
|
|
status: item.status,
|
|
proof: item.proof,
|
|
link: item.url,
|
|
title: item.title,
|
|
file_name: item.file_name,
|
|
}
|
|
}
|
|
})
|
|
|
|
return judgements
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadPersistedData()
|
|
|
|
const isStale = cachedProjectUpdated.value !== props.projectUpdated
|
|
|
|
if (!modPackData.value || isStale) {
|
|
fetchModPackData()
|
|
}
|
|
})
|
|
|
|
watch(
|
|
modPackData,
|
|
(newValue) => {
|
|
if (newValue && newValue.length === 0) {
|
|
emit('complete')
|
|
clearPersistedData()
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(
|
|
() => props.projectId,
|
|
() => {
|
|
clearPersistedData()
|
|
cachedProjectUpdated.value = null
|
|
loadPersistedData()
|
|
if (!modPackData.value) {
|
|
fetchModPackData()
|
|
}
|
|
},
|
|
)
|
|
|
|
function getModpackFiles(): {
|
|
interactive: ModerationModpackItem[]
|
|
permanentNo: ModerationModpackItem[]
|
|
} {
|
|
return {
|
|
interactive: modPackData.value || [],
|
|
permanentNo: permanentNoFiles.value,
|
|
}
|
|
}
|
|
|
|
defineExpose({ getModpackFiles })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.input-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.modpack-buttons {
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|