mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
implement functionality with staging api
This commit is contained in:
@@ -54,7 +54,7 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.items.length > 0 && this.neverEmpty) {
|
||||
if (this.items.length > 0 && this.neverEmpty && !this.modelValue) {
|
||||
this.selected = this.items[0]
|
||||
}
|
||||
},
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { MegaphoneIcon } from '@modrinth/assets'
|
||||
import {
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
normalizeChildren,
|
||||
SettingsFormGroup,
|
||||
SettingsToggleCard,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import type { NoteDisclosure } from './types'
|
||||
|
||||
const model = defineModel<NoteDisclosure>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'project.settings.disclosures.advertising.title',
|
||||
defaultMessage: 'Contains advertisements',
|
||||
},
|
||||
description1: {
|
||||
id: 'project.settings.disclosures.advertising.description.1',
|
||||
defaultMessage: `You must enable this if your project contains advertisements, sponsorships, or promotions of other works.`,
|
||||
},
|
||||
description2: {
|
||||
id: 'project.settings.disclosures.advertising.description.2',
|
||||
defaultMessage: `If the promotion has no direct monetary value <emphasis>and</emphasis> it is for something that the average person would consider <italic>relevant</italic> and <italic>unobtrusive</italic> (such as a link to your Modrinth profile in the corner of the settings page for your own mod), we would not consider that an advertisement.`,
|
||||
},
|
||||
noteDescription: {
|
||||
id: 'project.settings.disclosures.advertising.note-description',
|
||||
defaultMessage:
|
||||
'Please explain how your project utilizes advertising so that users can know what to expect.',
|
||||
},
|
||||
notePlaceholder: {
|
||||
id: 'project.settings.disclosures.advertising.note-placeholder',
|
||||
defaultMessage: 'e.g. Adds the Modrinth SMP server to your server list automatically.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsToggleCard
|
||||
v-model="model.enabled"
|
||||
:disabled="disabled"
|
||||
:icon="MegaphoneIcon"
|
||||
:title="formatMessage(messages.title)"
|
||||
>
|
||||
<p>{{ formatMessage(messages.description1) }}</p>
|
||||
<p>
|
||||
<IntlFormatted :message-id="messages.description2">
|
||||
<template #italic="{ children }">
|
||||
<span class="italic">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
<template #emphasis="{ children }">
|
||||
<span class="font-bold italic">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
<template #expanded>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.noteDescription)"
|
||||
title-for="advertising-disclosure-note"
|
||||
>
|
||||
<StyledInput
|
||||
id="advertising-disclosure-note"
|
||||
v-model="model.note"
|
||||
multiline
|
||||
:rows="3"
|
||||
class="max-w-[40rem]"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.notePlaceholder)"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
</template>
|
||||
</SettingsToggleCard>
|
||||
</template>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import { SparklesIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
normalizeChildren,
|
||||
SettingsFormGroup,
|
||||
SettingsToggleCard,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import type { AiDisclosure, AiUsage } from './types'
|
||||
|
||||
const model = defineModel<AiDisclosure>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const AI_USES: AiUsage[] = ['code', 'assets', 'text', 'functionality']
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'project.settings.disclosures.ai.title',
|
||||
defaultMessage: 'Contains AI-generated content',
|
||||
},
|
||||
description: {
|
||||
id: 'project.settings.disclosures.ai.description',
|
||||
defaultMessage: `You must enable this if this project contains a substantial amount of AI-generated code, any
|
||||
assets that are substantially AI-generated, the project's functionality relies on the use of
|
||||
generative AI, or if any element of your project's page such as description or publishing
|
||||
relies on generative AI.`,
|
||||
},
|
||||
contentRules: {
|
||||
id: 'project.settings.disclosures.ai.content-rules',
|
||||
defaultMessage:
|
||||
"Please refer to Section 6 of Modrinth's <rules>Content Rules</rules> for more information.",
|
||||
},
|
||||
typesDescription: {
|
||||
id: 'project.settings.disclosures.ai.types-description',
|
||||
defaultMessage: 'Select what this project uses generative AI for.',
|
||||
},
|
||||
typeCode: {
|
||||
id: 'project.settings.disclosures.ai.types-code',
|
||||
defaultMessage: 'Code',
|
||||
},
|
||||
typeAssets: {
|
||||
id: 'project.settings.disclosures.ai.types-assets',
|
||||
defaultMessage: 'Assets',
|
||||
},
|
||||
typeText: {
|
||||
id: 'project.settings.disclosures.ai.types-text',
|
||||
defaultMessage: 'Text',
|
||||
},
|
||||
typeFunctionality: {
|
||||
id: 'project.settings.disclosures.ai.types-functionality',
|
||||
defaultMessage: 'Functionality',
|
||||
},
|
||||
noteDescription: {
|
||||
id: 'project.settings.disclosures.ai.note-description',
|
||||
defaultMessage:
|
||||
'You may optionally provide a note to explain how you use generative AI in this project.',
|
||||
},
|
||||
notePlaceholder: {
|
||||
id: 'project.settings.disclosures.ai.note-placeholder',
|
||||
defaultMessage: 'e.g. The Chinese and Arabic translations are AI-generated.',
|
||||
},
|
||||
})
|
||||
|
||||
const useLabels = {
|
||||
code: messages.typeCode,
|
||||
assets: messages.typeAssets,
|
||||
text: messages.typeText,
|
||||
functionality: messages.typeFunctionality,
|
||||
} as const
|
||||
|
||||
function hasUse(use: AiUsage): boolean {
|
||||
return model.value.uses.includes(use)
|
||||
}
|
||||
|
||||
function setUse(use: AiUsage, enabled: boolean) {
|
||||
if (enabled) {
|
||||
if (!model.value.uses.includes(use)) {
|
||||
model.value.uses = [...model.value.uses, use]
|
||||
}
|
||||
return
|
||||
}
|
||||
model.value.uses = model.value.uses.filter((entry) => entry !== use)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsToggleCard
|
||||
v-model="model.enabled"
|
||||
:disabled="disabled"
|
||||
:icon="SparklesIcon"
|
||||
:title="formatMessage(messages.title)"
|
||||
:description="formatMessage(messages.description)"
|
||||
>
|
||||
<p class="text-secondary">
|
||||
<IntlFormatted :message-id="messages.contentRules">
|
||||
<template #rules="{ children }">
|
||||
<nuxt-link
|
||||
to="/legal/rules#generative-ai"
|
||||
target="_blank"
|
||||
class="smart-clickable:allow-pointer-events underline hover:text-primary"
|
||||
>
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
<template #expanded>
|
||||
<SettingsFormGroup :title="formatMessage(messages.typesDescription)">
|
||||
<div class="grid gap-2 sm:grid-cols-4">
|
||||
<Checkbox
|
||||
v-for="use in AI_USES"
|
||||
:key="use"
|
||||
:model-value="hasUse(use)"
|
||||
:disabled="disabled"
|
||||
@update:model-value="(enabled) => setUse(use, enabled)"
|
||||
>
|
||||
{{ formatMessage(useLabels[use]) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</SettingsFormGroup>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.noteDescription)"
|
||||
title-for="ai-disclosure-note"
|
||||
>
|
||||
<StyledInput
|
||||
id="ai-disclosure-note"
|
||||
v-model="model.note"
|
||||
multiline
|
||||
:rows="3"
|
||||
class="max-w-[40rem]"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.notePlaceholder)"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
</template>
|
||||
</SettingsToggleCard>
|
||||
</template>
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import { GitForkIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
SettingsFormGroup,
|
||||
SettingsToggleCard,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import type { DerivativeDisclosure, DerivativeSource } from './types'
|
||||
|
||||
const model = defineModel<DerivativeDisclosure>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'project.settings.disclosures.derivative.title',
|
||||
defaultMessage: 'Contains derivative content',
|
||||
},
|
||||
description: {
|
||||
id: 'project.settings.disclosures.derivative.description',
|
||||
defaultMessage:
|
||||
'You must enable this if your project is derivative of another project, such as being a fork or containing a substantial amount of someone else’s work.',
|
||||
},
|
||||
nameLabel: {
|
||||
id: 'project.settings.disclosures.derivative.name-label',
|
||||
defaultMessage: 'Name of original work',
|
||||
},
|
||||
namePlaceholder: {
|
||||
id: 'project.settings.disclosures.derivative.name-placeholder',
|
||||
defaultMessage: 'Example project',
|
||||
},
|
||||
linkLabel: {
|
||||
id: 'project.settings.disclosures.derivative.link-label',
|
||||
defaultMessage: 'Link to original work',
|
||||
},
|
||||
linkPlaceholder: {
|
||||
id: 'project.settings.disclosures.derivative.link-placeholder',
|
||||
defaultMessage: 'https://example.com',
|
||||
},
|
||||
noteLabel: {
|
||||
id: 'project.settings.disclosures.derivative.note-label',
|
||||
defaultMessage: 'Please explain how your project is based on the original work',
|
||||
},
|
||||
})
|
||||
|
||||
function emptyDerivativeSource(): DerivativeSource {
|
||||
return { label: '' }
|
||||
}
|
||||
|
||||
function updateSource(index: number, patch: Partial<DerivativeSource>) {
|
||||
model.value.sources = model.value.sources.map((source, i) =>
|
||||
i === index ? { ...source, ...patch } : source,
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => model.value.enabled,
|
||||
(enabled) => {
|
||||
if (enabled && model.value.sources.length === 0) {
|
||||
model.value.sources = [emptyDerivativeSource()]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function addSource() {
|
||||
model.value.sources = [...model.value.sources, emptyDerivativeSource()]
|
||||
}
|
||||
|
||||
function removeSource(index: number) {
|
||||
if (model.value.sources.length <= 1) return
|
||||
model.value.sources = model.value.sources.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
function setOptionalField(
|
||||
index: number,
|
||||
field: 'link' | 'note',
|
||||
value: string | number | undefined,
|
||||
) {
|
||||
updateSource(index, {
|
||||
[field]: typeof value === 'string' && value ? value : null,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsToggleCard
|
||||
v-model="model.enabled"
|
||||
:disabled="disabled"
|
||||
:icon="GitForkIcon"
|
||||
:title="formatMessage(messages.title)"
|
||||
:description="formatMessage(messages.description)"
|
||||
>
|
||||
<template #expanded>
|
||||
<div
|
||||
v-for="(source, index) in model.sources"
|
||||
:key="index"
|
||||
class="relative flex flex-col gap-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4"
|
||||
>
|
||||
<div class="absolute right-3 top-3">
|
||||
<ButtonStyled v-if="model.sources.length > 1" color="red" color-fill="text">
|
||||
<button type="button" :disabled="disabled" @click="removeSource(index)">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(commonMessages.removeButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.nameLabel)"
|
||||
:title-for="`derivative-name-${index}`"
|
||||
class="max-w-[40rem]"
|
||||
>
|
||||
<StyledInput
|
||||
:id="`derivative-name-${index}`"
|
||||
:model-value="source.label"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.namePlaceholder)"
|
||||
@update:model-value="
|
||||
(value) => updateSource(index, { label: typeof value === 'string' ? value : '' })
|
||||
"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.linkLabel)"
|
||||
:title-for="`derivative-link-${index}`"
|
||||
class="max-w-[40rem]"
|
||||
>
|
||||
<StyledInput
|
||||
:id="`derivative-link-${index}`"
|
||||
:model-value="source.link ?? undefined"
|
||||
type="url"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.linkPlaceholder)"
|
||||
@update:model-value="(value) => setOptionalField(index, 'link', value)"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.noteLabel)"
|
||||
:title-for="`derivative-note-${index}`"
|
||||
class="max-w-[40rem]"
|
||||
>
|
||||
<StyledInput
|
||||
:id="`derivative-note-${index}`"
|
||||
:model-value="source.note ?? undefined"
|
||||
multiline
|
||||
:rows="3"
|
||||
:disabled="disabled"
|
||||
@update:model-value="(value) => setOptionalField(index, 'note', value)"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button type="button" class="w-fit" :disabled="disabled" @click="addSource">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(commonMessages.addAnotherButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</SettingsToggleCard>
|
||||
</template>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleDollarSignIcon, ListPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
SettingsFormGroup,
|
||||
SettingsToggleCard,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import type { PaidFeaturesDisclosure } from './types'
|
||||
|
||||
const model = defineModel<PaidFeaturesDisclosure>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'project.settings.disclosures.paid-features.title',
|
||||
defaultMessage: 'Contains paid features',
|
||||
},
|
||||
description: {
|
||||
id: 'project.settings.disclosures.paid-features.description',
|
||||
defaultMessage:
|
||||
'You must enable this if your project contains features that can be obtained by spending real-world money.',
|
||||
},
|
||||
featuresDescription: {
|
||||
id: 'project.settings.disclosures.paid-features.features-description',
|
||||
defaultMessage: 'What kinds of paid features does it add?',
|
||||
},
|
||||
featurePlaceholder: {
|
||||
id: 'project.settings.disclosures.paid-features.feature-placeholder',
|
||||
defaultMessage: 'e.g. Cosmetics available as Patreon reward',
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => model.value.enabled,
|
||||
(enabled) => {
|
||||
if (enabled && model.value.features.length === 0) {
|
||||
model.value.features = ['']
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function addFeature() {
|
||||
model.value.features = [...model.value.features, '']
|
||||
}
|
||||
|
||||
function removeFeature(index: number) {
|
||||
if (model.value.features.length <= 1) return
|
||||
model.value.features = model.value.features.filter((_, i) => i !== index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsToggleCard
|
||||
v-model="model.enabled"
|
||||
:disabled="disabled"
|
||||
:icon="CircleDollarSignIcon"
|
||||
:title="formatMessage(messages.title)"
|
||||
:description="formatMessage(messages.description)"
|
||||
>
|
||||
<template #expanded>
|
||||
<SettingsFormGroup :title="formatMessage(messages.featuresDescription)">
|
||||
<div v-for="(_, index) in model.features" :key="index" class="flex items-center gap-2">
|
||||
<StyledInput
|
||||
v-model="model.features[index]"
|
||||
class="min-w-0 flex-1"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.featurePlaceholder)"
|
||||
/>
|
||||
<ButtonStyled v-if="model.features.length > 1" circular>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
:aria-label="formatMessage(commonMessages.removeButton)"
|
||||
@click="removeFeature(index)"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</SettingsFormGroup>
|
||||
<ButtonStyled>
|
||||
<button type="button" class="w-fit" :disabled="disabled" @click="addFeature">
|
||||
<ListPlusIcon />
|
||||
{{ formatMessage(commonMessages.addAnotherButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</SettingsToggleCard>
|
||||
</template>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { EyeIcon } from '@modrinth/assets'
|
||||
import {
|
||||
defineMessages,
|
||||
SettingsFormGroup,
|
||||
SettingsToggleCard,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import type { NoteDisclosure } from './types'
|
||||
|
||||
const model = defineModel<NoteDisclosure>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'project.settings.disclosures.photosensitivity.title',
|
||||
defaultMessage: 'Photosensitivity warning',
|
||||
},
|
||||
description: {
|
||||
id: 'project.settings.disclosures.photosensitivity.description',
|
||||
defaultMessage:
|
||||
'Enable this if your project contains anything that you think may be dangerous to certain people who are sensitive to flashing lights or patterns.',
|
||||
},
|
||||
noteLabel: {
|
||||
id: 'project.settings.disclosures.photosensitivity.note-label',
|
||||
defaultMessage: 'Please briefly describe why your project has a photosensitivity warning.',
|
||||
},
|
||||
notePlaceholder: {
|
||||
id: 'project.settings.disclosures.photosensitivity.note-placeholder',
|
||||
defaultMessage:
|
||||
'e.g. It adds a flashlight item that has a strobe mode. It can be disabled in Accessibility settings in-game.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsToggleCard
|
||||
v-model="model.enabled"
|
||||
:disabled="disabled"
|
||||
:icon="EyeIcon"
|
||||
:title="formatMessage(messages.title)"
|
||||
:description="formatMessage(messages.description)"
|
||||
>
|
||||
<template #expanded>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.noteLabel)"
|
||||
title-for="photosensitivity-disclosure-note"
|
||||
>
|
||||
<StyledInput
|
||||
id="photosensitivity-disclosure-note"
|
||||
v-model="model.note"
|
||||
multiline
|
||||
:rows="3"
|
||||
class="max-w-[40rem]"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.notePlaceholder)"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
</template>
|
||||
</SettingsToggleCard>
|
||||
</template>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { CircuitBoardIcon } from '@modrinth/assets'
|
||||
import {
|
||||
defineMessages,
|
||||
SettingsFormGroup,
|
||||
SettingsToggleCard,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import type { NoteDisclosure } from './types'
|
||||
|
||||
const model = defineModel<NoteDisclosure>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'project.settings.disclosures.system-interactions.title',
|
||||
defaultMessage: 'External system interactions',
|
||||
},
|
||||
description: {
|
||||
id: 'project.settings.disclosures.system-interactions.description',
|
||||
defaultMessage:
|
||||
"You must enable this if your project reads or edits things on the user's system outside of the game.",
|
||||
},
|
||||
noteLabel: {
|
||||
id: 'project.settings.disclosures.system-interactions.note-label',
|
||||
defaultMessage:
|
||||
'Please describe the external system interactions in the mod so users can know what to expect.',
|
||||
},
|
||||
notePlaceholder: {
|
||||
id: 'project.settings.disclosures.system-interactions.note-placeholder',
|
||||
defaultMessage: 'e.g. It adds a file to the desktop called wake_up.txt',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsToggleCard
|
||||
v-model="model.enabled"
|
||||
:disabled="disabled"
|
||||
:icon="CircuitBoardIcon"
|
||||
:title="formatMessage(messages.title)"
|
||||
:description="formatMessage(messages.description)"
|
||||
>
|
||||
<template #expanded>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.noteLabel)"
|
||||
title-for="system-interactions-disclosure-note"
|
||||
>
|
||||
<StyledInput
|
||||
id="system-interactions-disclosure-note"
|
||||
v-model="model.note"
|
||||
multiline
|
||||
:rows="3"
|
||||
class="max-w-[40rem]"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.notePlaceholder)"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
</template>
|
||||
</SettingsToggleCard>
|
||||
</template>
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ListPlusIcon, RadioTowerIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Chips,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
SettingsFormGroup,
|
||||
SettingsToggleCard,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import type { TelemetryConsent, TelemetryDisclosure } from './types'
|
||||
|
||||
const model = defineModel<TelemetryDisclosure>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const CONSENT_MODELS: TelemetryConsent[] = ['opt_in', 'opt_out', 'always_active']
|
||||
|
||||
const consentMessages = defineMessages({
|
||||
opt_in: {
|
||||
id: 'project.settings.disclosures.telemetry.consent-opt-in',
|
||||
defaultMessage: 'Opt-in',
|
||||
},
|
||||
opt_out: {
|
||||
id: 'project.settings.disclosures.telemetry.consent-opt-out',
|
||||
defaultMessage: 'Opt-out',
|
||||
},
|
||||
always_active: {
|
||||
id: 'project.settings.disclosures.telemetry.consent-always-active',
|
||||
defaultMessage: 'Always active',
|
||||
},
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'project.settings.disclosures.telemetry.title',
|
||||
defaultMessage: 'Contains telemetry',
|
||||
},
|
||||
description: {
|
||||
id: 'project.settings.disclosures.telemetry.description',
|
||||
defaultMessage:
|
||||
'You must enable this if your project sends usage data back to yourself or a third party.',
|
||||
},
|
||||
consentDescription: {
|
||||
id: 'project.settings.disclosures.telemetry.consent-description',
|
||||
defaultMessage: 'What is the consent model of your telemetry?',
|
||||
},
|
||||
dataLabel: {
|
||||
id: 'project.settings.disclosures.telemetry.data-label',
|
||||
defaultMessage: 'What data is being collected?',
|
||||
},
|
||||
dataDescription: {
|
||||
id: 'project.settings.disclosures.telemetry.data-description',
|
||||
defaultMessage:
|
||||
'You can either add a privacy policy, or a list of types of data that are collected. Remember to mention if it is anonymous or contains personally identifiable information (PII).',
|
||||
},
|
||||
dataPlaceholder: {
|
||||
id: 'project.settings.disclosures.telemetry.data-placeholder',
|
||||
defaultMessage:
|
||||
'e.g. Anonymous launch analytics to track Minecraft version and mod loader usage.',
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => model.value.enabled,
|
||||
(enabled) => {
|
||||
if (enabled && model.value.entries.length === 0) {
|
||||
model.value.entries = ['']
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function addEntry() {
|
||||
model.value.entries = [...model.value.entries, '']
|
||||
}
|
||||
|
||||
function removeEntry(index: number) {
|
||||
if (model.value.entries.length <= 1) return
|
||||
model.value.entries = model.value.entries.filter((_, i) => i !== index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsToggleCard
|
||||
v-model="model.enabled"
|
||||
:disabled="disabled"
|
||||
:icon="RadioTowerIcon"
|
||||
:title="formatMessage(messages.title)"
|
||||
:description="formatMessage(messages.description)"
|
||||
>
|
||||
<template #expanded>
|
||||
<SettingsFormGroup :title="formatMessage(messages.consentDescription)">
|
||||
<Chips
|
||||
v-model="model.consent"
|
||||
:items="CONSENT_MODELS"
|
||||
:capitalize="false"
|
||||
:format-label="(item: TelemetryConsent) => formatMessage(consentMessages[item])"
|
||||
/>
|
||||
</SettingsFormGroup>
|
||||
<SettingsFormGroup
|
||||
:title="formatMessage(messages.dataLabel)"
|
||||
:description="formatMessage(messages.dataDescription)"
|
||||
>
|
||||
<div v-for="(_, index) in model.entries" :key="index" class="flex items-center gap-2">
|
||||
<StyledInput
|
||||
v-model="model.entries[index]"
|
||||
class="min-w-0 flex-1"
|
||||
:disabled="disabled"
|
||||
:placeholder="formatMessage(messages.dataPlaceholder)"
|
||||
/>
|
||||
<ButtonStyled v-if="model.entries.length > 1" circular>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
:aria-label="formatMessage(commonMessages.removeButton)"
|
||||
@click="removeEntry(index)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</SettingsFormGroup>
|
||||
<ButtonStyled>
|
||||
<button type="button" class="w-fit" :disabled="disabled" @click="addEntry">
|
||||
<ListPlusIcon />
|
||||
{{ formatMessage(commonMessages.addAnotherButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</SettingsToggleCard>
|
||||
</template>
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
import type {
|
||||
DisclosureFormState,
|
||||
DisclosureOf,
|
||||
DisclosureType,
|
||||
NoteDisclosure,
|
||||
ProjectDisclosure,
|
||||
ProjectDisclosureData,
|
||||
} from './types'
|
||||
|
||||
function findDisclosure<T extends DisclosureType>(
|
||||
disclosures: ProjectDisclosureData[],
|
||||
type: T,
|
||||
): DisclosureOf<T> | undefined {
|
||||
return disclosures.find((disclosure): disclosure is DisclosureOf<T> => disclosure.type === type)
|
||||
}
|
||||
|
||||
type NoteDisclosureType = 'advertisements' | 'epilepsy_triggers' | 'system_interactions'
|
||||
|
||||
function createNoteModel(
|
||||
disclosures: ProjectDisclosureData[],
|
||||
type: NoteDisclosureType,
|
||||
): NoteDisclosure {
|
||||
const disclosure = findDisclosure(disclosures, type)
|
||||
return {
|
||||
enabled: !!disclosure,
|
||||
note: disclosure?.note ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
function nonemptyOrPlaceholder(values: string[]): string[] {
|
||||
return values.length > 0 ? [...values] : ['']
|
||||
}
|
||||
|
||||
export function disclosuresToForm(disclosures: ProjectDisclosureData[]): DisclosureFormState {
|
||||
const ai = findDisclosure(disclosures, 'ai_content')
|
||||
const paidFeatures = findDisclosure(disclosures, 'paid_features')
|
||||
const telemetry = findDisclosure(disclosures, 'telemetry')
|
||||
const derivative = findDisclosure(disclosures, 'derivative_work')
|
||||
|
||||
return {
|
||||
ai: {
|
||||
enabled: !!ai,
|
||||
uses: ai ? [...(ai.uses ?? [])] : [],
|
||||
note: ai?.note ?? '',
|
||||
},
|
||||
advertising: createNoteModel(disclosures, 'advertisements'),
|
||||
paidFeatures: {
|
||||
enabled: !!paidFeatures,
|
||||
features: paidFeatures ? nonemptyOrPlaceholder(paidFeatures.features) : [],
|
||||
},
|
||||
telemetry: {
|
||||
enabled: !!telemetry,
|
||||
consent: telemetry?.consent ?? 'opt_in',
|
||||
entries: telemetry ? nonemptyOrPlaceholder(telemetry.data_collected) : [],
|
||||
},
|
||||
derivative: {
|
||||
enabled: !!derivative,
|
||||
sources: derivative ? derivative.sources.map((source) => ({ ...source })) : [],
|
||||
},
|
||||
photosensitivity: createNoteModel(disclosures, 'epilepsy_triggers'),
|
||||
systemInteractions: createNoteModel(disclosures, 'system_interactions'),
|
||||
}
|
||||
}
|
||||
|
||||
export function formToDisclosures(form: DisclosureFormState): ProjectDisclosure[] {
|
||||
const set: ProjectDisclosure[] = []
|
||||
|
||||
if (form.ai.enabled) {
|
||||
set.push({
|
||||
type: 'ai_content',
|
||||
uses: [...form.ai.uses],
|
||||
note: form.ai.note.trim() || null,
|
||||
})
|
||||
}
|
||||
|
||||
if (form.advertising.enabled) {
|
||||
set.push({ type: 'advertisements', note: form.advertising.note.trim() || null })
|
||||
}
|
||||
|
||||
if (form.paidFeatures.enabled) {
|
||||
set.push({
|
||||
type: 'paid_features',
|
||||
features: form.paidFeatures.features.map((feature) => feature.trim()).filter(Boolean),
|
||||
})
|
||||
}
|
||||
|
||||
if (form.telemetry.enabled) {
|
||||
set.push({
|
||||
type: 'telemetry',
|
||||
consent: form.telemetry.consent,
|
||||
data_collected: form.telemetry.entries.map((entry) => entry.trim()).filter(Boolean),
|
||||
})
|
||||
}
|
||||
|
||||
if (form.derivative.enabled) {
|
||||
set.push({
|
||||
type: 'derivative_work',
|
||||
sources: form.derivative.sources.map((source) => ({
|
||||
label: source.label.trim(),
|
||||
link: source.link?.trim() || null,
|
||||
note: source.note?.trim() || null,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
if (form.photosensitivity.enabled) {
|
||||
set.push({ type: 'epilepsy_triggers', note: form.photosensitivity.note.trim() || null })
|
||||
}
|
||||
|
||||
if (form.systemInteractions.enabled) {
|
||||
set.push({ type: 'system_interactions', note: form.systemInteractions.note.trim() || null })
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
export function toModifyRequest(
|
||||
form: DisclosureFormState,
|
||||
previous: DisclosureFormState,
|
||||
): Labrinth.Projects.v3.ModifyProjectDisclosures {
|
||||
const set = formToDisclosures(form)
|
||||
const nextTypes = new Set(set.map((disclosure) => disclosure.type))
|
||||
return {
|
||||
set,
|
||||
remove: formToDisclosures(previous)
|
||||
.map((disclosure) => disclosure.type)
|
||||
.filter((type) => !nextTypes.has(type)),
|
||||
}
|
||||
}
|
||||
|
||||
export function toCachedDisclosures(set: ProjectDisclosure[]): ProjectDisclosureData[] {
|
||||
const now = new Date().toISOString()
|
||||
return set.map((disclosure) => ({
|
||||
...disclosure,
|
||||
set_by_moderator: false,
|
||||
updated_at: now,
|
||||
updated_by: null,
|
||||
}))
|
||||
}
|
||||
|
||||
export type DisclosureFormIssue =
|
||||
| 'advertising-note'
|
||||
| 'paid-features-empty'
|
||||
| 'telemetry-empty'
|
||||
| 'derivative-empty'
|
||||
| 'derivative-source-label'
|
||||
| 'photosensitivity-note'
|
||||
| 'system-interactions-note'
|
||||
|
||||
export function getDisclosureFormIssues(form: DisclosureFormState): DisclosureFormIssue[] {
|
||||
const issues: DisclosureFormIssue[] = []
|
||||
const missingNote = (model: NoteDisclosure) => model.enabled && !model.note.trim()
|
||||
|
||||
if (missingNote(form.advertising)) {
|
||||
issues.push('advertising-note')
|
||||
}
|
||||
if (form.paidFeatures.enabled && !form.paidFeatures.features.some((feature) => feature.trim())) {
|
||||
issues.push('paid-features-empty')
|
||||
}
|
||||
if (form.telemetry.enabled && !form.telemetry.entries.some((entry) => entry.trim())) {
|
||||
issues.push('telemetry-empty')
|
||||
}
|
||||
if (form.derivative.enabled) {
|
||||
if (form.derivative.sources.length === 0) {
|
||||
issues.push('derivative-empty')
|
||||
} else if (form.derivative.sources.some((source) => !source.label?.trim())) {
|
||||
issues.push('derivative-source-label')
|
||||
}
|
||||
}
|
||||
if (missingNote(form.photosensitivity)) {
|
||||
issues.push('photosensitivity-note')
|
||||
}
|
||||
if (missingNote(form.systemInteractions)) {
|
||||
issues.push('system-interactions-note')
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as AdvertisingDisclosureCard } from './AdvertisingDisclosureCard.vue'
|
||||
export { default as AiDisclosureCard } from './AiDisclosureCard.vue'
|
||||
export { default as DerivativeDisclosureCard } from './DerivativeDisclosureCard.vue'
|
||||
export * from './form'
|
||||
export { default as PaidFeaturesDisclosureCard } from './PaidFeaturesDisclosureCard.vue'
|
||||
export { default as PhotosensitivityDisclosureCard } from './PhotosensitivityDisclosureCard.vue'
|
||||
export { default as SystemInteractionsDisclosureCard } from './SystemInteractionsDisclosureCard.vue'
|
||||
export { default as TelemetryDisclosureCard } from './TelemetryDisclosureCard.vue'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
export type TelemetryConsent = Labrinth.Projects.v3.TelemetryConsent
|
||||
export type AiUsage = Labrinth.Projects.v3.AiUsage
|
||||
export type DerivativeSource = Labrinth.Projects.v3.DerivativeSource
|
||||
export type ProjectDisclosure = Labrinth.Projects.v3.ProjectDisclosure
|
||||
export type ProjectDisclosureData = Labrinth.Projects.v3.ProjectDisclosureData
|
||||
export type DisclosureType = Labrinth.Projects.v3.ProjectDisclosureType
|
||||
export type DisclosureOf<T extends DisclosureType> = Labrinth.Projects.v3.ProjectDisclosureOf<T>
|
||||
|
||||
export type NoteDisclosure = {
|
||||
enabled: boolean
|
||||
note: string
|
||||
}
|
||||
|
||||
export type AiDisclosure = {
|
||||
enabled: boolean
|
||||
uses: AiUsage[]
|
||||
note: string
|
||||
}
|
||||
|
||||
export type PaidFeaturesDisclosure = {
|
||||
enabled: boolean
|
||||
features: string[]
|
||||
}
|
||||
|
||||
export type TelemetryDisclosure = {
|
||||
enabled: boolean
|
||||
consent: TelemetryConsent
|
||||
entries: string[]
|
||||
}
|
||||
|
||||
export type DerivativeDisclosure = {
|
||||
enabled: boolean
|
||||
sources: DerivativeSource[]
|
||||
}
|
||||
|
||||
export type DisclosureFormState = {
|
||||
ai: AiDisclosure
|
||||
advertising: NoteDisclosure
|
||||
paidFeatures: PaidFeaturesDisclosure
|
||||
telemetry: TelemetryDisclosure
|
||||
derivative: DerivativeDisclosure
|
||||
photosensitivity: NoteDisclosure
|
||||
systemInteractions: NoteDisclosure
|
||||
}
|
||||
Reference in New Issue
Block a user