diff --git a/apps/frontend/src/assets/styles/global.scss b/apps/frontend/src/assets/styles/global.scss index 8ad63c2da1..462183f1f2 100644 --- a/apps/frontend/src/assets/styles/global.scss +++ b/apps/frontend/src/assets/styles/global.scss @@ -40,6 +40,8 @@ html { interpolate-size: allow-keywords; scrollbar-gutter: stable; + + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); } .light-mode { diff --git a/apps/frontend/src/components/ui/Chips.vue b/apps/frontend/src/components/ui/Chips.vue index 17905aa13c..002416be67 100644 --- a/apps/frontend/src/components/ui/Chips.vue +++ b/apps/frontend/src/components/ui/Chips.vue @@ -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] } }, diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/AdvertisingDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/AdvertisingDisclosureCard.vue new file mode 100644 index 0000000000..70ee057c8c --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/AdvertisingDisclosureCard.vue @@ -0,0 +1,87 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/AiDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/AiDisclosureCard.vue new file mode 100644 index 0000000000..bda96e4f0b --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/AiDisclosureCard.vue @@ -0,0 +1,147 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/DerivativeDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/DerivativeDisclosureCard.vue new file mode 100644 index 0000000000..f348252e71 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/DerivativeDisclosureCard.vue @@ -0,0 +1,169 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/PaidFeaturesDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/PaidFeaturesDisclosureCard.vue new file mode 100644 index 0000000000..070ff3b6de --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/PaidFeaturesDisclosureCard.vue @@ -0,0 +1,100 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/PhotosensitivityDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/PhotosensitivityDisclosureCard.vue new file mode 100644 index 0000000000..c70edfb402 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/PhotosensitivityDisclosureCard.vue @@ -0,0 +1,68 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/SystemInteractionsDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/SystemInteractionsDisclosureCard.vue new file mode 100644 index 0000000000..060e0eec97 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/SystemInteractionsDisclosureCard.vue @@ -0,0 +1,68 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/TelemetryDisclosureCard.vue b/apps/frontend/src/components/ui/project-settings/disclosures/TelemetryDisclosureCard.vue new file mode 100644 index 0000000000..0647fb058e --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/TelemetryDisclosureCard.vue @@ -0,0 +1,139 @@ + + + diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/form.ts b/apps/frontend/src/components/ui/project-settings/disclosures/form.ts new file mode 100644 index 0000000000..89bb99c91a --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/form.ts @@ -0,0 +1,180 @@ +import type { Labrinth } from '@modrinth/api-client' + +import type { + DisclosureFormState, + DisclosureOf, + DisclosureType, + NoteDisclosure, + ProjectDisclosure, + ProjectDisclosureData, +} from './types' + +function findDisclosure( + disclosures: ProjectDisclosureData[], + type: T, +): DisclosureOf | undefined { + return disclosures.find((disclosure): disclosure is DisclosureOf => 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 +} diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/index.ts b/apps/frontend/src/components/ui/project-settings/disclosures/index.ts new file mode 100644 index 0000000000..cb172e45b7 --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/index.ts @@ -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' diff --git a/apps/frontend/src/components/ui/project-settings/disclosures/types.ts b/apps/frontend/src/components/ui/project-settings/disclosures/types.ts new file mode 100644 index 0000000000..21619bb2ae --- /dev/null +++ b/apps/frontend/src/components/ui/project-settings/disclosures/types.ts @@ -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 = Labrinth.Projects.v3.ProjectDisclosureOf + +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 +} diff --git a/apps/frontend/src/composables/project-settings-head.ts b/apps/frontend/src/composables/project-settings-head.ts new file mode 100644 index 0000000000..346e83a764 --- /dev/null +++ b/apps/frontend/src/composables/project-settings-head.ts @@ -0,0 +1,26 @@ +import { + defineMessages, + injectProjectPageContext, + type MessageDescriptor, + useVIntl, +} from '@modrinth/ui' + +const messages = defineMessages({ + headTitle: { + id: 'project.settings.head-title', + defaultMessage: '⚙️{section} - {project}', + }, +}) + +export function useProjectSettingsHeadTitle(section: MessageDescriptor) { + const { formatMessage } = useVIntl() + const { projectV2: project } = injectProjectPageContext() + + useHead({ + title: () => + `${formatMessage(messages.headTitle, { + section: formatMessage(section), + project: project.value.title, + })}`, + }) +} diff --git a/apps/frontend/src/locales/cs-CZ/index.json b/apps/frontend/src/locales/cs-CZ/index.json index 4fbc4ff613..5d8324035d 100644 --- a/apps/frontend/src/locales/cs-CZ/index.json +++ b/apps/frontend/src/locales/cs-CZ/index.json @@ -2378,12 +2378,6 @@ "project.settings.permissions.sort.status": { "message": "Stav" }, - "project.settings.title": { - "message": "Nastavení" - }, - "project.settings.visit-dashboard": { - "message": "Přejít na přehled projektů" - }, "project.versions.title": { "message": "Verze" }, diff --git a/apps/frontend/src/locales/da-DK/index.json b/apps/frontend/src/locales/da-DK/index.json index 81e4a243e6..2c23b92d3b 100644 --- a/apps/frontend/src/locales/da-DK/index.json +++ b/apps/frontend/src/locales/da-DK/index.json @@ -2486,12 +2486,6 @@ "project.settings.tags.upload-version-first": { "message": "Upload venligst en version først for at kunne vælge tags!" }, - "project.settings.title": { - "message": "Indstillinger" - }, - "project.settings.visit-dashboard": { - "message": "Gå til projekt kontrolpanel" - }, "project.status.archived.message": { "message": "{title} er blevet arkiveret. {title} vil ikke modtage flere opdateringer medmindre forfatteren beslutter sig for at fjerne projektet fra arkivet." }, diff --git a/apps/frontend/src/locales/de-CH/index.json b/apps/frontend/src/locales/de-CH/index.json index 751f57167c..ec05f2c24b 100644 --- a/apps/frontend/src/locales/de-CH/index.json +++ b/apps/frontend/src/locales/de-CH/index.json @@ -3941,12 +3941,6 @@ "project.settings.tags.upload-version-first": { "message": "Bitte lade zuerst eine Version hoch, um Tags auswählen zu können!" }, - "project.settings.title": { - "message": "Einstellungen" - }, - "project.settings.visit-dashboard": { - "message": "Projekt-Dashboard besuchen" - }, "project.status.archived.message": { "message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheided sich, das Projekt zu Dearchivieren." }, diff --git a/apps/frontend/src/locales/de-DE/index.json b/apps/frontend/src/locales/de-DE/index.json index 1c5a72c63b..0ca0307b91 100644 --- a/apps/frontend/src/locales/de-DE/index.json +++ b/apps/frontend/src/locales/de-DE/index.json @@ -3941,12 +3941,6 @@ "project.settings.tags.upload-version-first": { "message": "Bitte lade zuerst eine Version hoch, um Tags auswählen zu können!" }, - "project.settings.title": { - "message": "Einstellungen" - }, - "project.settings.visit-dashboard": { - "message": "Projekt-Dashboard besuchen" - }, "project.status.archived.message": { "message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheidet sich das Projekt zu dearchivieren." }, diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json index aac072810e..e91fed890e 100644 --- a/apps/frontend/src/locales/en-US/index.json +++ b/apps/frontend/src/locales/en-US/index.json @@ -3458,6 +3458,12 @@ "project-type.shader.singular": { "message": "Shader" }, + "project.actions.back-to-all-projects": { + "message": "Back to all projects" + }, + "project.actions.back-to-project-page": { + "message": "Back to project page" + }, "project.actions.create-server": { "message": "Create a server" }, @@ -3470,6 +3476,9 @@ "project.actions.edit-project": { "message": "Edit project" }, + "project.actions.project-page": { + "message": "Project page" + }, "project.actions.rescan-modpack": { "message": "Rescan modpack" }, @@ -3746,6 +3755,174 @@ "project.notification.updated.title": { "message": "Project updated" }, + "project.settings.back-to-all-projects": { + "message": "Back to all projects" + }, + "project.settings.back-to-project-page": { + "message": "Back to project page" + }, + "project.settings.disclosures.advertising.description.1": { + "message": "You must enable this if your project contains advertisements, sponsorships, or promotions of other works." + }, + "project.settings.disclosures.advertising.description.2": { + "message": "If the promotion has no direct monetary value and it is for something that the average person would consider relevant and unobtrusive (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." + }, + "project.settings.disclosures.advertising.note-description": { + "message": "Please explain how your project utilizes advertising so that users can know what to expect." + }, + "project.settings.disclosures.advertising.note-placeholder": { + "message": "e.g. Adds the Modrinth SMP server to your server list automatically." + }, + "project.settings.disclosures.advertising.title": { + "message": "Contains advertisements" + }, + "project.settings.disclosures.ai.content-rules": { + "message": "Please refer to Section 6 of Modrinth's Content Rules for more information." + }, + "project.settings.disclosures.ai.description": { + "message": "You must enable this if this project contains a substantial amount of AI-generated code, any\n\t\t\t\tassets that are substantially AI-generated, the project's functionality relies on the use of\n\t\t\t\tgenerative AI, or if any element of your project's page such as description or publishing\n\t\t\t\trelies on generative AI." + }, + "project.settings.disclosures.ai.note-description": { + "message": "You may optionally provide a note to explain how you use generative AI in this project." + }, + "project.settings.disclosures.ai.note-placeholder": { + "message": "e.g. The Chinese and Arabic translations are AI-generated." + }, + "project.settings.disclosures.ai.title": { + "message": "Contains AI-generated content" + }, + "project.settings.disclosures.ai.types-assets": { + "message": "Assets" + }, + "project.settings.disclosures.ai.types-code": { + "message": "Code" + }, + "project.settings.disclosures.ai.types-description": { + "message": "Select what this project uses generative AI for." + }, + "project.settings.disclosures.ai.types-functionality": { + "message": "Functionality" + }, + "project.settings.disclosures.ai.types-text": { + "message": "Text" + }, + "project.settings.disclosures.content-disclosures": { + "message": "Content disclosures" + }, + "project.settings.disclosures.derivative.description": { + "message": "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." + }, + "project.settings.disclosures.derivative.link-label": { + "message": "Link to original work" + }, + "project.settings.disclosures.derivative.link-placeholder": { + "message": "https://example.com" + }, + "project.settings.disclosures.derivative.name-label": { + "message": "Name of original work" + }, + "project.settings.disclosures.derivative.name-placeholder": { + "message": "Example project" + }, + "project.settings.disclosures.derivative.note-label": { + "message": "Please explain how your project is based on the original work" + }, + "project.settings.disclosures.derivative.title": { + "message": "Contains derivative content" + }, + "project.settings.disclosures.description": { + "message": "You must add any applicable content disclosures to your project in compliance with Modrinth's Content Rules." + }, + "project.settings.disclosures.paid-features.description": { + "message": "You must enable this if your project contains features that can be obtained by spending real-world money." + }, + "project.settings.disclosures.paid-features.feature-placeholder": { + "message": "e.g. Cosmetics available as Patreon reward" + }, + "project.settings.disclosures.paid-features.features-description": { + "message": "What kinds of paid features does it add?" + }, + "project.settings.disclosures.paid-features.title": { + "message": "Contains paid features" + }, + "project.settings.disclosures.photosensitivity.description": { + "message": "Enable this if your project contains anything that you think may be dangerous to certain people who are sensitive to flashing lights or patterns." + }, + "project.settings.disclosures.photosensitivity.note-label": { + "message": "Please briefly describe why your project has a photosensitivity warning." + }, + "project.settings.disclosures.photosensitivity.note-placeholder": { + "message": "e.g. It adds a flashlight item that has a strobe mode. It can be disabled in Accessibility settings in-game." + }, + "project.settings.disclosures.photosensitivity.title": { + "message": "Photosensitivity warning" + }, + "project.settings.disclosures.save-blocked.advertising-note": { + "message": "Advertising disclosure requires explanation." + }, + "project.settings.disclosures.save-blocked.derivative-empty": { + "message": "Derivative works disclosure must list at least one source work." + }, + "project.settings.disclosures.save-blocked.derivative-source-label": { + "message": "Derivative work sources must have a name." + }, + "project.settings.disclosures.save-blocked.no-permission": { + "message": "You don't have permission to edit this project's disclosures." + }, + "project.settings.disclosures.save-blocked.paid-features-empty": { + "message": "Paid features disclosure must list at least one paid feature." + }, + "project.settings.disclosures.save-blocked.photosensitivity-note": { + "message": "Photosensitivity warning disclosure must include a description." + }, + "project.settings.disclosures.save-blocked.system-interactions-note": { + "message": "External system interactions disclosure must include a description." + }, + "project.settings.disclosures.save-blocked.telemetry-empty": { + "message": "Telemetry disclosure must list at least one type of data collected." + }, + "project.settings.disclosures.system-interactions.description": { + "message": "You must enable this if your project reads or edits things on the user's system outside of the game." + }, + "project.settings.disclosures.system-interactions.note-label": { + "message": "Please describe the external system interactions in the mod so users can know what to expect." + }, + "project.settings.disclosures.system-interactions.note-placeholder": { + "message": "e.g. It adds a file to the desktop called wake_up.txt" + }, + "project.settings.disclosures.system-interactions.title": { + "message": "External system interactions" + }, + "project.settings.disclosures.telemetry.consent-always-active": { + "message": "Always active" + }, + "project.settings.disclosures.telemetry.consent-description": { + "message": "What is the consent model of your telemetry?" + }, + "project.settings.disclosures.telemetry.consent-opt-in": { + "message": "Opt-in" + }, + "project.settings.disclosures.telemetry.consent-opt-out": { + "message": "Opt-out" + }, + "project.settings.disclosures.telemetry.data-description": { + "message": "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)." + }, + "project.settings.disclosures.telemetry.data-label": { + "message": "What data is being collected?" + }, + "project.settings.disclosures.telemetry.data-placeholder": { + "message": "e.g. Anonymous launch analytics to track Minecraft version and mod loader usage." + }, + "project.settings.disclosures.telemetry.description": { + "message": "You must enable this if your project sends usage data back to yourself or a third party." + }, + "project.settings.disclosures.telemetry.title": { + "message": "Contains telemetry" + }, + "project.settings.editing-project": { + "message": "Editing {projectType} project" + }, "project.settings.general.name.description": { "message": "Avoid prefixes, suffixes, parentheticals, or added descriptions—just the project's actual name." }, @@ -3791,6 +3968,9 @@ "project.settings.general.url.title": { "message": "URL" }, + "project.settings.head-title": { + "message": "⚙️{section} - {project}" + }, "project.settings.permissions.attention-needed.description.proj-approved": { "message": "Please provide proof that you have permission to redistribute all of the following files. Once completed, withheld versions will be automatically published." }, @@ -3974,12 +4154,6 @@ "project.settings.tags.upload-version-first": { "message": "Please upload a version first in order to select tags!" }, - "project.settings.title": { - "message": "Settings" - }, - "project.settings.visit-dashboard": { - "message": "Visit projects dashboard" - }, "project.status.archived.message": { "message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project." }, diff --git a/apps/frontend/src/locales/es-419/index.json b/apps/frontend/src/locales/es-419/index.json index d78081990a..60476d3e83 100644 --- a/apps/frontend/src/locales/es-419/index.json +++ b/apps/frontend/src/locales/es-419/index.json @@ -3497,12 +3497,6 @@ "project.settings.permissions.sort.status": { "message": "Estado" }, - "project.settings.title": { - "message": "Configuración" - }, - "project.settings.visit-dashboard": { - "message": "Ver el panel de control de proyectos" - }, "project.status.archived.message": { "message": "{title} se ha archivado. {title} no recibirá más actualizaciones hasta que el autor decida desarchivar el proyecto." }, diff --git a/apps/frontend/src/locales/es-ES/index.json b/apps/frontend/src/locales/es-ES/index.json index 51c1a75596..ef17cda774 100644 --- a/apps/frontend/src/locales/es-ES/index.json +++ b/apps/frontend/src/locales/es-ES/index.json @@ -3221,12 +3221,6 @@ "project.settings.permissions.sort.status": { "message": "Estado" }, - "project.settings.title": { - "message": "Ajustes" - }, - "project.settings.visit-dashboard": { - "message": "Visita el panel de proyectos" - }, "project.status.archived.message": { "message": "{title} ha sido archivado. {title} no recibirá ninguna futura actualización excepto que el autor decida desarchivar el proyecto." }, diff --git a/apps/frontend/src/locales/fi-FI/index.json b/apps/frontend/src/locales/fi-FI/index.json index b49d334a9c..83210976eb 100644 --- a/apps/frontend/src/locales/fi-FI/index.json +++ b/apps/frontend/src/locales/fi-FI/index.json @@ -980,9 +980,6 @@ "project.settings.general.url.title": { "message": "URL" }, - "project.settings.title": { - "message": "Asetukset" - }, "project.versions.title": { "message": "Versiot" }, diff --git a/apps/frontend/src/locales/fil-PH/index.json b/apps/frontend/src/locales/fil-PH/index.json index ba75875867..e3a1e2acee 100644 --- a/apps/frontend/src/locales/fil-PH/index.json +++ b/apps/frontend/src/locales/fil-PH/index.json @@ -2411,12 +2411,6 @@ "project.settings.permissions.sort.status": { "message": "Estado" }, - "project.settings.title": { - "message": "Mga Setting" - }, - "project.settings.visit-dashboard": { - "message": "Bisitahin ang dashboard ng mga proyekto" - }, "project.status.archived.message": { "message": "Inarkibo ang {title}. Ang {title} ay hindi na tatanggap ng mga update maliban kung iwawala nila ang pagkaarkibo ng proyekto." }, diff --git a/apps/frontend/src/locales/fr-FR/index.json b/apps/frontend/src/locales/fr-FR/index.json index 4b04ddb3a9..f4a7229fad 100644 --- a/apps/frontend/src/locales/fr-FR/index.json +++ b/apps/frontend/src/locales/fr-FR/index.json @@ -3932,12 +3932,6 @@ "project.settings.tags.upload-version-first": { "message": "Merci d'upload une version en premier afin de pouvoir sélectionner des tags !" }, - "project.settings.title": { - "message": "Paramètres" - }, - "project.settings.visit-dashboard": { - "message": "Visiter le tableau de contrôle des projets" - }, "project.status.archived.message": { "message": "{title} a été archivé. {title} ne recevra plus de mises à jour jusqu'à ce que l'auteur du projet décide de désarchiver le projet." }, diff --git a/apps/frontend/src/locales/he-IL/index.json b/apps/frontend/src/locales/he-IL/index.json index 45383ef9bd..2e68a18674 100644 --- a/apps/frontend/src/locales/he-IL/index.json +++ b/apps/frontend/src/locales/he-IL/index.json @@ -2021,12 +2021,6 @@ "project.settings.general.url.title": { "message": "URL" }, - "project.settings.title": { - "message": "הגדרות" - }, - "project.settings.visit-dashboard": { - "message": "ללוח הבקרה של הפרויקטים" - }, "project.status.archived.message": { "message": "הפרויקט {title} הועבר לארכיון. {title} לא יקבל עדכונים נוספים, אלא אם המחבר יחליט להוציא את הפרויקט מהארכיון." }, diff --git a/apps/frontend/src/locales/hu-HU/index.json b/apps/frontend/src/locales/hu-HU/index.json index a837ca3cc2..b50342c7eb 100644 --- a/apps/frontend/src/locales/hu-HU/index.json +++ b/apps/frontend/src/locales/hu-HU/index.json @@ -3506,12 +3506,6 @@ "project.settings.tags.multiple-resolution-tags-warning": { "message": "{count} felbontási címkét választottál ki ({tags}). A forráscsomagoknak általában csak egy felbontási címkéjük lehet." }, - "project.settings.title": { - "message": "Beállítások" - }, - "project.settings.visit-dashboard": { - "message": "Projektek irányítópult megnyitása" - }, "project.status.archived.message": { "message": "A(z) {title} archiválásra került. A(z) {title} nem kap további frissítéseket, kivéve, ha a fejlesztő úgy dönt, hogy visszavonja a projekt archiválását." }, diff --git a/apps/frontend/src/locales/id-ID/index.json b/apps/frontend/src/locales/id-ID/index.json index 705302bffd..31671b421a 100644 --- a/apps/frontend/src/locales/id-ID/index.json +++ b/apps/frontend/src/locales/id-ID/index.json @@ -2414,12 +2414,6 @@ "project.settings.general.url.title": { "message": "URL" }, - "project.settings.title": { - "message": "Pengaturan" - }, - "project.settings.visit-dashboard": { - "message": "Kunjungi dasbor proyek" - }, "project.status.archived.message": { "message": "{title} telah diarsip. {title} tidak akan menerima pembaruan di masa mendatang kecuali pembuat memutuskan untuk membuka arsip proyek ini." }, diff --git a/apps/frontend/src/locales/it-IT/index.json b/apps/frontend/src/locales/it-IT/index.json index 147b5344f9..31c1e8b4de 100644 --- a/apps/frontend/src/locales/it-IT/index.json +++ b/apps/frontend/src/locales/it-IT/index.json @@ -3947,12 +3947,6 @@ "project.settings.tags.upload-version-first": { "message": "Carica una vesione prima di selezionare i tag!" }, - "project.settings.title": { - "message": "Impostazioni" - }, - "project.settings.visit-dashboard": { - "message": "Visita bacheca del progetto" - }, "project.status.archived.message": { "message": "{title} è stato archiviato. {title} non riceverà più aggiornamenti a meno che l'autore decida di rimuoverlo dall'archivio." }, diff --git a/apps/frontend/src/locales/ja-JP/index.json b/apps/frontend/src/locales/ja-JP/index.json index 7dde5c5570..f1b93803b6 100644 --- a/apps/frontend/src/locales/ja-JP/index.json +++ b/apps/frontend/src/locales/ja-JP/index.json @@ -3002,12 +3002,6 @@ "project.settings.permissions.sort.status": { "message": "ステータス" }, - "project.settings.title": { - "message": "設定" - }, - "project.settings.visit-dashboard": { - "message": "プロジェクトダッシュボードを見る" - }, "project.status.archived.message": { "message": "{title} はアーカイブされました。作者がプロジェクトのアーカイブを解除しない限り、今後 {title} に更新はありません。" }, diff --git a/apps/frontend/src/locales/ko-KR/index.json b/apps/frontend/src/locales/ko-KR/index.json index 33619cbc35..83e4d32768 100644 --- a/apps/frontend/src/locales/ko-KR/index.json +++ b/apps/frontend/src/locales/ko-KR/index.json @@ -3896,12 +3896,6 @@ "project.settings.tags.upload-version-first": { "message": "먼저 버전을 업로드해야 태그를 선택할 수 있습니다!" }, - "project.settings.title": { - "message": "설정" - }, - "project.settings.visit-dashboard": { - "message": "프로젝트 대시보드 방문" - }, "project.status.archived.message": { "message": "{title} 은(는) 보관되었습니다. 작성자가 프로젝트 보관 해제를 결정하지 않는 한 더 이상 업데이트가 제공되지 않습니다." }, diff --git a/apps/frontend/src/locales/ms-MY/index.json b/apps/frontend/src/locales/ms-MY/index.json index 28a2097a63..0a975d7730 100644 --- a/apps/frontend/src/locales/ms-MY/index.json +++ b/apps/frontend/src/locales/ms-MY/index.json @@ -3038,12 +3038,6 @@ "project.settings.permissions.sort.status": { "message": "Status" }, - "project.settings.title": { - "message": "Tetapan" - }, - "project.settings.visit-dashboard": { - "message": "Kunjungi papan pemuka projek" - }, "project.status.archived.message": { "message": "{title} telah diarkibkan. {title} tidak akan menerima sebarang kemas kini lanjut melainkan pengarang memutuskan untuk menyaharkibkan projek." }, diff --git a/apps/frontend/src/locales/nl-NL/index.json b/apps/frontend/src/locales/nl-NL/index.json index f527efaaa5..9cc80973cc 100644 --- a/apps/frontend/src/locales/nl-NL/index.json +++ b/apps/frontend/src/locales/nl-NL/index.json @@ -3968,12 +3968,6 @@ "project.settings.tags.upload-version-first": { "message": "Upload eerst een versie om tags te kunnen selecteren!" }, - "project.settings.title": { - "message": "Instellingen" - }, - "project.settings.visit-dashboard": { - "message": "Bezoek het project dashboard" - }, "project.status.archived.message": { "message": "{title} is gearchiveerd. {title} zal geen verdere updates krijgen tenzij de auteur het project de-archiveerd." }, diff --git a/apps/frontend/src/locales/no-NO/index.json b/apps/frontend/src/locales/no-NO/index.json index f401ca004f..f044f865dd 100644 --- a/apps/frontend/src/locales/no-NO/index.json +++ b/apps/frontend/src/locales/no-NO/index.json @@ -2888,12 +2888,6 @@ "project.settings.permissions.sort.status": { "message": "Status" }, - "project.settings.title": { - "message": "Innstillinger" - }, - "project.settings.visit-dashboard": { - "message": "Besøk prosjekt-dashbordet" - }, "project.status.archived.message": { "message": "{title} har blitt arkivert. {title} kommer ikke til å få noen nye oppdateringer, hvis ikke forfatteren bestemmer seg for å dearkivere prosjektet." }, diff --git a/apps/frontend/src/locales/pl-PL/index.json b/apps/frontend/src/locales/pl-PL/index.json index 0c99e2440f..03cd87b15c 100644 --- a/apps/frontend/src/locales/pl-PL/index.json +++ b/apps/frontend/src/locales/pl-PL/index.json @@ -3887,12 +3887,6 @@ "project.settings.tags.upload-version-first": { "message": "Prześlij wersję, by móc wybierać tagi!" }, - "project.settings.title": { - "message": "Ustawienia" - }, - "project.settings.visit-dashboard": { - "message": "Odwiedź pulpit projektów" - }, "project.status.archived.message": { "message": "{title} zostało zarchiwizowane. {title} nie będzie otrzymywać żadnych dalszych aktualizacji, chyba że autor zdecyduje się przywrócić projekt." }, diff --git a/apps/frontend/src/locales/pt-BR/index.json b/apps/frontend/src/locales/pt-BR/index.json index 4ded8625c6..c6dc39467a 100644 --- a/apps/frontend/src/locales/pt-BR/index.json +++ b/apps/frontend/src/locales/pt-BR/index.json @@ -3971,12 +3971,6 @@ "project.settings.tags.upload-version-first": { "message": "Por favor, envie uma versão primeiro para que as etiquetas possam ser selecionadas!" }, - "project.settings.title": { - "message": "Configurações" - }, - "project.settings.visit-dashboard": { - "message": "Visitar painel de projetos" - }, "project.status.archived.message": { "message": "{title} foi arquivado. {title} não receberá atualizações a menos que o autor decida desarquivar o projeto." }, diff --git a/apps/frontend/src/locales/pt-PT/index.json b/apps/frontend/src/locales/pt-PT/index.json index d39fb90970..4b3a18d8c3 100644 --- a/apps/frontend/src/locales/pt-PT/index.json +++ b/apps/frontend/src/locales/pt-PT/index.json @@ -2756,12 +2756,6 @@ "project.settings.general.url.title": { "message": "URL" }, - "project.settings.title": { - "message": "Definições" - }, - "project.settings.visit-dashboard": { - "message": "Visitar o painel de projetos" - }, "project.status.archived.message": { "message": "{title} foi arquivado. {title} não receberá mais atualizações, a menos que o autor decida desarquivar o projeto." }, diff --git a/apps/frontend/src/locales/ro-RO/index.json b/apps/frontend/src/locales/ro-RO/index.json index 819f979afe..8146721562 100644 --- a/apps/frontend/src/locales/ro-RO/index.json +++ b/apps/frontend/src/locales/ro-RO/index.json @@ -1448,12 +1448,6 @@ "project.settings.general.url.title": { "message": "URL" }, - "project.settings.title": { - "message": "Setări" - }, - "project.settings.visit-dashboard": { - "message": "Vizitează panoul de control al proiectelor" - }, "project.status.archived.message": { "message": "Proiectul {title} a fost arhivat. Proiectul {title} nu va mai primi actualizări, cu excepția cazului în care autorul decide să-l dezarhiveze." }, diff --git a/apps/frontend/src/locales/ru-RU/index.json b/apps/frontend/src/locales/ru-RU/index.json index 317e381a00..b8fced173b 100644 --- a/apps/frontend/src/locales/ru-RU/index.json +++ b/apps/frontend/src/locales/ru-RU/index.json @@ -3935,12 +3935,6 @@ "project.settings.tags.upload-version-first": { "message": "Загрузите версию перед тем, как выбрать теги!" }, - "project.settings.title": { - "message": "Настройки" - }, - "project.settings.visit-dashboard": { - "message": "Управление проектами" - }, "project.status.archived.message": { "message": "{title} помещён в архив. {title} больше не будет получать обновления, если только автор не решит разархивировать проект." }, diff --git a/apps/frontend/src/locales/sv-SE/index.json b/apps/frontend/src/locales/sv-SE/index.json index 9382a0e614..1187f1cbe0 100644 --- a/apps/frontend/src/locales/sv-SE/index.json +++ b/apps/frontend/src/locales/sv-SE/index.json @@ -3158,9 +3158,6 @@ "project.settings.server.server-online": { "message": "Servern är online!" }, - "project.settings.title": { - "message": "Inställningar" - }, "project.status.archived.message": { "message": "{title} har arkiverats. {title} kommer inte få några ytterligare uppdateringar såvida inte författaren bestämmer sig för att avarkivera projektet." }, diff --git a/apps/frontend/src/locales/tr-TR/index.json b/apps/frontend/src/locales/tr-TR/index.json index a3ca07e3ae..bd1aa90ed5 100644 --- a/apps/frontend/src/locales/tr-TR/index.json +++ b/apps/frontend/src/locales/tr-TR/index.json @@ -3443,12 +3443,6 @@ "project.settings.permissions.sort.status": { "message": "Durum" }, - "project.settings.title": { - "message": "Ayarlar" - }, - "project.settings.visit-dashboard": { - "message": "Projeler panelini ziyaret et" - }, "project.status.archived.message": { "message": "{title} arşivlenmiş. {title}, yapımcı fikrini değiştirmediği sürece daha fazla güncelleme almayacak." }, diff --git a/apps/frontend/src/locales/uk-UA/index.json b/apps/frontend/src/locales/uk-UA/index.json index 539566c37d..4d2ac8d1de 100644 --- a/apps/frontend/src/locales/uk-UA/index.json +++ b/apps/frontend/src/locales/uk-UA/index.json @@ -3899,12 +3899,6 @@ "project.settings.tags.upload-version-first": { "message": "Спершу завантажте версію, щоб обирати теґи!" }, - "project.settings.title": { - "message": "Налаштування" - }, - "project.settings.visit-dashboard": { - "message": "Панель керування проєктами" - }, "project.status.archived.message": { "message": "«{title}» було архівовано. «{title}» не отримуватиме подальших оновлень допоки автор не вирішить розархівувати проєкт." }, diff --git a/apps/frontend/src/locales/vi-VN/index.json b/apps/frontend/src/locales/vi-VN/index.json index 8caa0b4214..e155143aa2 100644 --- a/apps/frontend/src/locales/vi-VN/index.json +++ b/apps/frontend/src/locales/vi-VN/index.json @@ -3179,12 +3179,6 @@ "project.settings.permissions.learn-more": { "message": "Tìm hiểu thêm" }, - "project.settings.title": { - "message": "Cài đặt" - }, - "project.settings.visit-dashboard": { - "message": "Truy cập bảng điều khiển dự án" - }, "project.status.archived.message": { "message": "{title} đã được lưu trữ. {title} sẽ không nhận được bất kỳ bản cập nhật nào trong tương lai trừ khi tác giả quyết định hủy lưu trữ dự án." }, diff --git a/apps/frontend/src/locales/zh-CN/index.json b/apps/frontend/src/locales/zh-CN/index.json index 0cc7b282c8..00fd402dcd 100644 --- a/apps/frontend/src/locales/zh-CN/index.json +++ b/apps/frontend/src/locales/zh-CN/index.json @@ -3971,12 +3971,6 @@ "project.settings.tags.upload-version-first": { "message": "请先上传一个版本再选择标签!" }, - "project.settings.title": { - "message": "设置" - }, - "project.settings.visit-dashboard": { - "message": "访问项目的控制面板" - }, "project.status.archived.message": { "message": "{title} 已归档。除非作者决定取消归档,否则 {title} 将不再更新。" }, diff --git a/apps/frontend/src/locales/zh-TW/index.json b/apps/frontend/src/locales/zh-TW/index.json index b9aa299ce4..0e542accf5 100644 --- a/apps/frontend/src/locales/zh-TW/index.json +++ b/apps/frontend/src/locales/zh-TW/index.json @@ -3971,12 +3971,6 @@ "project.settings.tags.upload-version-first": { "message": "請先上傳一個版本再選擇標籤!" }, - "project.settings.title": { - "message": "設定" - }, - "project.settings.visit-dashboard": { - "message": "前往專案資訊主頁" - }, "project.status.archived.message": { "message": "{title} 已封存。{title} 將不會再收到任何後續更新,除非作者決定解除封存該專案。" }, diff --git a/apps/frontend/src/pages/[type]/[project].vue b/apps/frontend/src/pages/[type]/[project].vue index 456d8b2a82..7bb1c75edb 100644 --- a/apps/frontend/src/pages/[type]/[project].vue +++ b/apps/frontend/src/pages/[type]/[project].vue @@ -5,29 +5,49 @@ diff --git a/apps/frontend/src/pages/[type]/[project]/settings/description.vue b/apps/frontend/src/pages/[type]/[project]/settings/description.vue index ef168ec53f..ac31b035f7 100644 --- a/apps/frontend/src/pages/[type]/[project]/settings/description.vue +++ b/apps/frontend/src/pages/[type]/[project]/settings/description.vue @@ -42,6 +42,7 @@ import { TriangleAlertIcon } from '@modrinth/assets' import { countText, MIN_DESCRIPTION_CHARS } from '@modrinth/moderation' import { + commonProjectSettingsMessages, ConfirmLeaveModal, injectProjectPageContext, MarkdownEditor, @@ -56,6 +57,8 @@ import { useImageUpload } from '~/composables/image-upload.ts' const { projectV2: project, currentMember, patchProject } = injectProjectPageContext() +useProjectSettingsHeadTitle(commonProjectSettingsMessages.description) + const { saved, current, saving, hasChanges, reset, save } = useSavable( () => ({ description: project.value.body }), async ({ description }) => { diff --git a/apps/frontend/src/pages/[type]/[project]/settings/disclosures.vue b/apps/frontend/src/pages/[type]/[project]/settings/disclosures.vue new file mode 100644 index 0000000000..0331e115d0 --- /dev/null +++ b/apps/frontend/src/pages/[type]/[project]/settings/disclosures.vue @@ -0,0 +1,183 @@ + + + diff --git a/apps/frontend/src/pages/[type]/[project]/settings/environment.vue b/apps/frontend/src/pages/[type]/[project]/settings/environment.vue index 84e4ec6cb9..b1acaa6a75 100644 --- a/apps/frontend/src/pages/[type]/[project]/settings/environment.vue +++ b/apps/frontend/src/pages/[type]/[project]/settings/environment.vue @@ -28,6 +28,7 @@ import { SettingsIcon } from '@modrinth/assets' import { ButtonStyled, + commonProjectSettingsMessages, EnvironmentMigration, injectProjectPageContext, LoadingIndicator, @@ -36,6 +37,8 @@ import { isStaff } from '@modrinth/utils' const { currentMember, projectV2, projectV3 } = injectProjectPageContext() +useProjectSettingsHeadTitle(commonProjectSettingsMessages.environment) + const showEnvironmentMigration = computed(() => { return isStaff(currentMember.value?.user) }) diff --git a/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue b/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue index cf135ac425..018b62d599 100644 --- a/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue +++ b/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue @@ -291,6 +291,7 @@ import { } from '@modrinth/assets' import { ButtonStyled, + commonProjectSettingsMessages, ConfirmModal, DropArea, FileInput, @@ -316,15 +317,7 @@ const { deleteGalleryItem: deleteGalleryItemMutation, } = injectProjectPageContext() -const title = `${project.value.title} - Gallery` -const description = `View ${project.value.gallery?.length ?? 0} images of ${project.value.title} on Modrinth.` - -useSeoMeta({ - title, - description, - ogTitle: title, - ogDescription: description, -}) +useProjectSettingsHeadTitle(commonProjectSettingsMessages.gallery) const modal_edit_item = ref(null) const modal_confirm = ref(null) diff --git a/apps/frontend/src/pages/[type]/[project]/settings/general.vue b/apps/frontend/src/pages/[type]/[project]/settings/general.vue index 54570dde34..8a4bf18911 100644 --- a/apps/frontend/src/pages/[type]/[project]/settings/general.vue +++ b/apps/frontend/src/pages/[type]/[project]/settings/general.vue @@ -1,5 +1,6 @@ + + diff --git a/packages/api-client/src/modules/labrinth/projects/v3.ts b/packages/api-client/src/modules/labrinth/projects/v3.ts index 1c86154e88..077168575d 100644 --- a/packages/api-client/src/modules/labrinth/projects/v3.ts +++ b/packages/api-client/src/modules/labrinth/projects/v3.ts @@ -222,4 +222,52 @@ export class LabrinthProjectsV3Module extends AbstractModule { method: 'DELETE', }) } + + /** + * Get content disclosures for a project + * + * @param id - Project ID or slug + * @returns Promise resolving to the project's disclosures + * + * @example + * ```typescript + * const { disclosures } = await client.labrinth.projects_v3.getDisclosures('sodium') + * ``` + */ + public async getDisclosures(id: string): Promise { + return this.client.request( + `/project/${id}/disclosures`, + { + api: 'labrinth', + version: 3, + method: 'GET', + }, + ) + } + + /** + * Modify content disclosures for a project + * + * @param id - Project ID or slug + * @param data - Disclosures to set and types to remove + * + * @example + * ```typescript + * await client.labrinth.projects_v3.modifyDisclosures('sodium', { + * set: [{ type: 'ai_content', uses: ['text'], note: 'Translations are AI-generated.' }], + * remove: ['advertisements'], + * }) + * ``` + */ + public async modifyDisclosures( + id: string, + data: Labrinth.Projects.v3.ModifyProjectDisclosures, + ): Promise { + return this.client.request(`/project/${id}/disclosures`, { + api: 'labrinth', + version: 3, + method: 'PATCH', + body: data, + }) + } } diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 0e571a6291..a2d7fca8ab 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -1300,6 +1300,70 @@ export namespace Labrinth { projects: Project[] versions: Labrinth.Versions.v3.Version[] } + + export type TelemetryConsent = 'opt_in' | 'opt_out' | 'always_active' + + export type AiUsage = 'code' | 'assets' | 'text' | 'functionality' + + export type DerivativeSource = { + label: string + link?: string | null + note?: string | null + } + + export type ProjectDisclosure = + | { + type: 'ai_content' + uses: AiUsage[] + note?: string | null + } + | { + type: 'advertisements' + note?: string | null + } + | { + type: 'epilepsy_triggers' + note?: string | null + } + | { + type: 'system_interactions' + note?: string | null + } + | { + type: 'telemetry' + consent: TelemetryConsent + data_collected: string[] + } + | { + type: 'derivative_work' + sources: DerivativeSource[] + } + | { + type: 'paid_features' + features: string[] + } + + export type ProjectDisclosureData = ProjectDisclosure & { + set_by_moderator: boolean + updated_at: string + updated_by?: string | null + } + + export type ProjectDisclosureType = ProjectDisclosure['type'] + + export type ProjectDisclosureOf = Extract< + ProjectDisclosureData, + { type: T } + > + + export type GetProjectDisclosures = { + disclosures: ProjectDisclosureData[] + } + + export type ModifyProjectDisclosures = { + set: ProjectDisclosure[] + remove: ProjectDisclosureType[] + } } } diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts index 0b524d52ad..60f65b1ca2 100644 --- a/packages/assets/generated-icons.ts +++ b/packages/assets/generated-icons.ts @@ -80,7 +80,9 @@ import _ChevronUpIcon from './icons/chevron-up.svg?component' import _CircleAlertIcon from './icons/circle-alert.svg?component' import _CircleArrowRightIcon from './icons/circle-arrow-right.svg?component' import _CircleDashedIcon from './icons/circle-dashed.svg?component' +import _CircleDollarSignIcon from './icons/circle-dollar-sign.svg?component' import _CircleUserIcon from './icons/circle-user.svg?component' +import _CircuitBoardIcon from './icons/circuit-board.svg?component' import _ClearIcon from './icons/clear.svg?component' import _ClientIcon from './icons/client.svg?component' import _ClipboardCopyIcon from './icons/clipboard-copy.svg?component' @@ -135,6 +137,7 @@ import _GapIcon from './icons/gap.svg?component' import _GaugeIcon from './icons/gauge.svg?component' import _GenericListIcon from './icons/generic-list.svg?component' import _GiftIcon from './icons/gift.svg?component' +import _GitForkIcon from './icons/git-fork.svg?component' import _GitGraphIcon from './icons/git-graph.svg?component' import _GlassesIcon from './icons/glasses.svg?component' import _GlobeIcon from './icons/globe.svg?component' @@ -174,6 +177,7 @@ import _ListBulletedIcon from './icons/list-bulleted.svg?component' import _ListEndIcon from './icons/list-end.svg?component' import _ListFilterIcon from './icons/list-filter.svg?component' import _ListOrderedIcon from './icons/list-ordered.svg?component' +import _ListPlusIcon from './icons/list-plus.svg?component' import _LoaderIcon from './icons/loader.svg?component' import _LoaderCircleIcon from './icons/loader-circle.svg?component' import _LockIcon from './icons/lock.svg?component' @@ -183,6 +187,7 @@ import _LogOutIcon from './icons/log-out.svg?component' import _MailIcon from './icons/mail.svg?component' import _ManageIcon from './icons/manage.svg?component' import _MaximizeIcon from './icons/maximize.svg?component' +import _MegaphoneIcon from './icons/megaphone.svg?component' import _MemoryStickIcon from './icons/memory-stick.svg?component' import _MessageIcon from './icons/message.svg?component' import _MessagesSquareIcon from './icons/messages-square.svg?component' @@ -218,6 +223,7 @@ import _PowerIcon from './icons/power.svg?component' import _PowerOffIcon from './icons/power-off.svg?component' import _RadioButtonIcon from './icons/radio-button.svg?component' import _RadioButtonCheckedIcon from './icons/radio-button-checked.svg?component' +import _RadioTowerIcon from './icons/radio-tower.svg?component' import _ReceiptTextIcon from './icons/receipt-text.svg?component' import _RedoIcon from './icons/redo.svg?component' import _RefreshCwIcon from './icons/refresh-cw.svg?component' @@ -512,7 +518,9 @@ export const ChevronUpIcon = _ChevronUpIcon export const CircleAlertIcon = _CircleAlertIcon export const CircleArrowRightIcon = _CircleArrowRightIcon export const CircleDashedIcon = _CircleDashedIcon +export const CircleDollarSignIcon = _CircleDollarSignIcon export const CircleUserIcon = _CircleUserIcon +export const CircuitBoardIcon = _CircuitBoardIcon export const ClearIcon = _ClearIcon export const ClientIcon = _ClientIcon export const ClipboardCopyIcon = _ClipboardCopyIcon @@ -567,6 +575,7 @@ export const GapIcon = _GapIcon export const GaugeIcon = _GaugeIcon export const GenericListIcon = _GenericListIcon export const GiftIcon = _GiftIcon +export const GitForkIcon = _GitForkIcon export const GitGraphIcon = _GitGraphIcon export const GlassesIcon = _GlassesIcon export const GlobeIcon = _GlobeIcon @@ -606,6 +615,7 @@ export const ListBulletedIcon = _ListBulletedIcon export const ListEndIcon = _ListEndIcon export const ListFilterIcon = _ListFilterIcon export const ListOrderedIcon = _ListOrderedIcon +export const ListPlusIcon = _ListPlusIcon export const LoaderIcon = _LoaderIcon export const LoaderCircleIcon = _LoaderCircleIcon export const LockIcon = _LockIcon @@ -615,6 +625,7 @@ export const LogOutIcon = _LogOutIcon export const MailIcon = _MailIcon export const ManageIcon = _ManageIcon export const MaximizeIcon = _MaximizeIcon +export const MegaphoneIcon = _MegaphoneIcon export const MemoryStickIcon = _MemoryStickIcon export const MessageIcon = _MessageIcon export const MessagesSquareIcon = _MessagesSquareIcon @@ -650,6 +661,7 @@ export const PowerIcon = _PowerIcon export const PowerOffIcon = _PowerOffIcon export const RadioButtonIcon = _RadioButtonIcon export const RadioButtonCheckedIcon = _RadioButtonCheckedIcon +export const RadioTowerIcon = _RadioTowerIcon export const ReceiptTextIcon = _ReceiptTextIcon export const RedoIcon = _RedoIcon export const RefreshCwIcon = _RefreshCwIcon diff --git a/packages/assets/icons/circle-dollar-sign.svg b/packages/assets/icons/circle-dollar-sign.svg new file mode 100644 index 0000000000..0df8dc3e65 --- /dev/null +++ b/packages/assets/icons/circle-dollar-sign.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/packages/assets/icons/circuit-board.svg b/packages/assets/icons/circuit-board.svg new file mode 100644 index 0000000000..a6dac3f828 --- /dev/null +++ b/packages/assets/icons/circuit-board.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/packages/assets/icons/git-fork.svg b/packages/assets/icons/git-fork.svg new file mode 100644 index 0000000000..23f47b1729 --- /dev/null +++ b/packages/assets/icons/git-fork.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/packages/assets/icons/list-plus.svg b/packages/assets/icons/list-plus.svg new file mode 100644 index 0000000000..9b8bc82e71 --- /dev/null +++ b/packages/assets/icons/list-plus.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/packages/assets/icons/megaphone.svg b/packages/assets/icons/megaphone.svg new file mode 100644 index 0000000000..a2fe8be6f8 --- /dev/null +++ b/packages/assets/icons/megaphone.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/packages/assets/icons/radio-tower.svg b/packages/assets/icons/radio-tower.svg new file mode 100644 index 0000000000..8776e18f64 --- /dev/null +++ b/packages/assets/icons/radio-tower.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/packages/ui/src/components/base/SettingsFormGroup.vue b/packages/ui/src/components/base/SettingsFormGroup.vue new file mode 100644 index 0000000000..cdf723cf15 --- /dev/null +++ b/packages/ui/src/components/base/SettingsFormGroup.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/ui/src/components/base/SettingsToggleCard.vue b/packages/ui/src/components/base/SettingsToggleCard.vue new file mode 100644 index 0000000000..3929845195 --- /dev/null +++ b/packages/ui/src/components/base/SettingsToggleCard.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/ui/src/components/base/SmartClickable.vue b/packages/ui/src/components/base/SmartClickable.vue index 961de1732d..d6fd4144fc 100644 --- a/packages/ui/src/components/base/SmartClickable.vue +++ b/packages/ui/src/components/base/SmartClickable.vue @@ -49,8 +49,7 @@ defineOptions({ // When clickable is being hovered or focus-visible, give contents an effect :first-child:hover + .smart-clickable__contents, :first-child:focus-visible + .smart-clickable__contents, - .smart-clickable__contents:hover, - .smart-clickable__contents:focus-within { + .smart-clickable__contents:hover { // Utility classes for contents :deep(.smart-clickable\:underline-on-hover) { text-decoration: underline; @@ -79,7 +78,7 @@ defineOptions({ // When clickable is being clicked, give contents an effect :first-child:active + .smart-clickable__contents { - scale: 0.97; + scale: 0.98; } } diff --git a/packages/ui/src/components/base/ToggleCard.vue b/packages/ui/src/components/base/ToggleCard.vue new file mode 100644 index 0000000000..42cf15f52b --- /dev/null +++ b/packages/ui/src/components/base/ToggleCard.vue @@ -0,0 +1,76 @@ + + + + diff --git a/packages/ui/src/components/base/UnsavedChangesPopup.vue b/packages/ui/src/components/base/UnsavedChangesPopup.vue index 4606042c86..5876ae8507 100644 --- a/packages/ui/src/components/base/UnsavedChangesPopup.vue +++ b/packages/ui/src/components/base/UnsavedChangesPopup.vue @@ -14,20 +14,25 @@ const emit = defineEmits<{ (e: 'reset' | 'save', event: MouseEvent): void }>() +type SaveDisabledReason = MessageDescriptor | string + const props = withDefaults( defineProps<{ canReset?: boolean + canSave?: boolean original: T modified: Partial saving?: boolean text?: MessageDescriptor | string saveLabel?: MessageDescriptor | string savingLabel?: MessageDescriptor | string + saveDisabledReason?: SaveDisabledReason | SaveDisabledReason[] saveIcon?: Component inline?: boolean }>(), { canReset: true, + canSave: true, saving: false, text: () => defineMessage({ @@ -36,6 +41,7 @@ const props = withDefaults( }), saveLabel: () => commonMessages.saveButton, savingLabel: () => commonMessages.savingButton, + saveDisabledReason: undefined, saveIcon: SaveIcon, inline: false, }, @@ -49,6 +55,22 @@ function localizeIfPossible(message: MessageDescriptor | string) { return typeof message === 'string' ? message : formatMessage(message) } +const saveDisabled = computed(() => props.saving || !props.canSave) + +const saveDisabledTooltip = computed(() => { + if (!saveDisabled.value || props.saving || !props.saveDisabledReason) { + return undefined + } + + const reasons = ( + Array.isArray(props.saveDisabledReason) ? props.saveDisabledReason : [props.saveDisabledReason] + ).map(localizeIfPossible) + + return reasons.length > 0 + ? { content: reasons.join('\n'), popperClass: 'unsaved-changes-save-tooltip' } + : undefined +}) + const actionBar = ref | null>(null) function nudge(): void { @@ -67,13 +89,32 @@ defineExpose({ nudge }) {{ formatMessage(commonMessages.resetButton) }} - - - + + + + + + + diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts index 1373522c33..b27ad214bf 100644 --- a/packages/ui/src/components/base/index.ts +++ b/packages/ui/src/components/base/index.ts @@ -92,7 +92,9 @@ export { default as RadioButtons } from './RadioButtons.vue' export { default as ReadyTransition } from './ReadyTransition.vue' export { default as ScrollablePanel } from './ScrollablePanel.vue' export { default as ServerNotice } from './ServerNotice.vue' +export { default as SettingsFormGroup } from './SettingsFormGroup.vue' export { default as SettingsLabel } from './SettingsLabel.vue' +export { default as SettingsToggleCard } from './SettingsToggleCard.vue' export { default as SimpleBadge } from './SimpleBadge.vue' export { default as Slider } from './Slider.vue' export { default as SmartClickable } from './SmartClickable.vue' @@ -121,4 +123,5 @@ export type { export { default as TimeFramePicker } from './TimeFramePicker.vue' export { default as Timeline } from './Timeline.vue' export { default as Toggle } from './Toggle.vue' +export { default as ToggleCard } from './ToggleCard.vue' export { default as UnsavedChangesPopup } from './UnsavedChangesPopup.vue' diff --git a/packages/ui/src/components/project/ProjectSidebarDetails.vue b/packages/ui/src/components/project/ProjectSidebarDetails.vue index bfe45a68c3..67d4d33d57 100644 --- a/packages/ui/src/components/project/ProjectSidebarDetails.vue +++ b/packages/ui/src/components/project/ProjectSidebarDetails.vue @@ -13,7 +13,91 @@

{{ formatMessage(commonMessages.detailsLabel) }}

-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+