diff --git a/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue b/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue index de0ae7132..06b45fd1d 100644 --- a/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue +++ b/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue @@ -118,6 +118,7 @@ import { type Cape, type SkinModel, get_normalized_skin_texture, + determineModelType, } from '@/helpers/skins.ts' import { handleError } from '@/store/notifications' import { @@ -253,7 +254,7 @@ async function showNew(e: MouseEvent, skinTextureUrl: string) { mode.value = 'new' currentSkin.value = null uploadedTextureUrl.value = skinTextureUrl - variant.value = 'CLASSIC' + variant.value = await determineModelType(skinTextureUrl) selectedCape.value = undefined visibleCapeList.value = [] initVisibleCapeList() diff --git a/apps/app-frontend/src/components/ui/world/WorldItem.vue b/apps/app-frontend/src/components/ui/world/WorldItem.vue index f30aca810..c8c1893f7 100644 --- a/apps/app-frontend/src/components/ui/world/WorldItem.vue +++ b/apps/app-frontend/src/components/ui/world/WorldItem.vue @@ -128,6 +128,14 @@ const messages = defineMessages({ id: 'instance.worlds.game_already_open', defaultMessage: 'Instance is already open', }, + noContact: { + id: 'instance.worlds.no_contact', + defaultMessage: "Server couldn't be contacted", + }, + incompatibleServer: { + id: 'instance.worlds.incompatible_server', + defaultMessage: 'Server is incompatible', + }, copyAddress: { id: 'instance.worlds.copy_address', defaultMessage: 'Copy address', @@ -302,39 +310,33 @@ const messages = defineMessages({
- - - diff --git a/apps/app-frontend/src/helpers/skins.ts b/apps/app-frontend/src/helpers/skins.ts index 28a29ba1a..9b5953f53 100644 --- a/apps/app-frontend/src/helpers/skins.ts +++ b/apps/app-frontend/src/helpers/skins.ts @@ -62,16 +62,14 @@ export async function determineModelType(texture: string): Promise<'SLIM' | 'CLA context.drawImage(image, 0, 0) - const armX = 44 - const armY = 16 - const armWidth = 4 + const armX = 54 + const armY = 20 + const armWidth = 2 const armHeight = 12 - const imageData = context.getImageData(armX, armY, armWidth, armHeight).data - - for (let y = 0; y < armHeight; y++) { - const alphaIndex = (3 + y * armWidth) * 4 + 3 - if (imageData[alphaIndex] !== 0) { + for (let index = 1; index <= imageData.length; index++) { + //every fourth value in RGBA is the alpha channel + if (index % 4 == 0 && imageData[index - 1] !== 0) { resolve('CLASSIC') return } diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index fa2563da9..b2410efc5 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -377,6 +377,12 @@ "instance.worlds.hardcore": { "message": "Hardcore mode" }, + "instance.worlds.incompatible_server": { + "message": "Server is incompatible" + }, + "instance.worlds.no_contact": { + "message": "Server couldn't be contacted" + }, "instance.worlds.no_quick_play": { "message": "You can only jump straight into worlds on Minecraft 1.20+" }, diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 21cb1752e..7cded8314 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -38,10 +38,10 @@ "@intercom/messenger-js-sdk": "^0.0.14", "@ltd/j-toml": "^1.38.0", "@modrinth/assets": "workspace:*", - "@modrinth/ui": "workspace:*", - "@modrinth/utils": "workspace:*", "@modrinth/blog": "workspace:*", "@modrinth/moderation": "workspace:*", + "@modrinth/ui": "workspace:*", + "@modrinth/utils": "workspace:*", "@pinia/nuxt": "^0.5.1", "@types/three": "^0.172.0", "@vintl/vintl": "^4.4.1", @@ -59,6 +59,7 @@ "markdown-it": "14.1.0", "pathe": "^1.1.2", "pinia": "^2.1.7", + "prettier": "^3.6.2", "qrcode.vue": "^3.4.0", "semver": "^7.5.4", "three": "^0.172.0", diff --git a/apps/frontend/src/assets/styles/components.scss b/apps/frontend/src/assets/styles/components.scss index a1ea3a1e9..f647e7e25 100644 --- a/apps/frontend/src/assets/styles/components.scss +++ b/apps/frontend/src/assets/styles/components.scss @@ -197,13 +197,13 @@ } > :where( - input + *, - .input-group + *, - .textarea-wrapper + *, - .chips + *, - .resizable-textarea-wrapper + *, - .input-div + * - ) { + input + *, + .input-group + *, + .textarea-wrapper + *, + .chips + *, + .resizable-textarea-wrapper + *, + .input-div + * + ) { &:not(:empty) { margin-block-start: var(--spacing-card-md); } diff --git a/apps/frontend/src/assets/styles/global.scss b/apps/frontend/src/assets/styles/global.scss index b52b738ed..cefb9460c 100644 --- a/apps/frontend/src/assets/styles/global.scss +++ b/apps/frontend/src/assets/styles/global.scss @@ -115,10 +115,12 @@ html { --shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 11%, 0.15); --shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2); - --shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15), + --shadow-raised: + 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15), 1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12), 4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09); - --shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, + --shadow-floating: + hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px; --shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px; @@ -150,8 +152,8 @@ html { rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0.2695) 100% ); - --landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16), - inset 2px 2px 64px rgba(255, 255, 255, 0.45); + --landing-blob-shadow: + 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(255, 255, 255, 0.45); --landing-card-bg: rgba(255, 255, 255, 0.8); --landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16); @@ -251,13 +253,15 @@ html { --shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2); --shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1); - --shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, + --shadow-floating: + hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px; --shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px; --landing-maze-bg: url("https://cdn.modrinth.com/landing-new/landing.webp"); - --landing-maze-gradient-bg: linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%), + --landing-maze-gradient-bg: + linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%), url("https://cdn.modrinth.com/landing-new/landing-lower.webp"); --landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%); @@ -284,7 +288,8 @@ html { rgba(44, 48, 79, 0.35) 0%, rgba(32, 35, 50, 0.2695) 100% ); - --landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45); + --landing-blob-shadow: + 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45); --landing-card-bg: rgba(59, 63, 85, 0.15); --landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16); @@ -360,8 +365,9 @@ body { // Defaults background-color: var(--color-bg); color: var(--color-text); - --font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, - Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + --font-standard: + Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, + Fira Sans, Droid Sans, Helvetica Neue, sans-serif; --mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; font-family: var(--font-standard); font-size: 16px; diff --git a/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue b/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue index ff5db66ac..124a49336 100644 --- a/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue +++ b/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue @@ -365,26 +365,24 @@ function getJudgements(): ModerationJudgements { const judgements: ModerationJudgements = {}; modPackData.value.forEach((item) => { - if (item.status && item.status !== "unidentified") { - if (item.type === "flame") { - judgements[item.sha1] = { - type: "flame", - id: item.id, - status: item.status, - link: item.url, - title: item.title, - file_name: item.file_name, - }; - } else if (item.type === "unknown") { - judgements[item.sha1] = { - type: "unknown", - status: item.status, - proof: item.proof, - link: item.url, - title: item.title, - file_name: item.file_name, - }; - } + if (item.type === "flame") { + judgements[item.sha1] = { + type: "flame", + id: item.id, + status: item.status, + link: item.url, + title: item.title, + file_name: item.file_name, + }; + } else if (item.type === "unknown") { + judgements[item.sha1] = { + type: "unknown", + status: item.status, + proof: item.proof, + link: item.url, + title: item.title, + file_name: item.file_name, + }; } }); diff --git a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue index 955b87dd0..2126091ad 100644 --- a/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/NewModerationChecklist.vue @@ -116,8 +116,10 @@ {{ action.label }} {{ action.label }}
>(); @@ -461,7 +464,7 @@ const stageTextExpanded = computedAsync(async () => { const stage = checklist[stageIndex]; if (stage.text) { return renderHighlightedString( - expandVariables(await stage.text(), props.project, variables.value), + expandVariables(await stage.text(props.project), props.project, variables.value), ); } return null; @@ -559,14 +562,20 @@ function handleKeybinds(event: KeyboardEvent) { }, trySelectDropdownOption: (actionIndex: number, optionIndex: number) => { const action = visibleActions.value[actionIndex] as DropdownAction; - if (action && action.type === "dropdown" && action.options[optionIndex]) { - selectDropdownOption(action, action.options[optionIndex]); + if (action && action.type === "dropdown") { + const visibleOptions = getVisibleDropdownOptions(action); + if (optionIndex < visibleOptions.length) { + selectDropdownOption(action, visibleOptions[optionIndex]); + } } }, tryToggleChip: (actionIndex: number, chipIndex: number) => { const action = visibleActions.value[actionIndex] as MultiSelectChipsAction; if (action && action.type === "multi-select-chips") { - toggleChip(action, chipIndex); + const visibleOptions = getVisibleMultiSelectOptions(action); + if (chipIndex < visibleOptions.length) { + toggleChip(action, chipIndex); + } } }, @@ -733,13 +742,17 @@ const multiSelectActions = computed(() => function getDropdownValue(action: DropdownAction) { const actionId = getActionId(action); + const visibleOptions = getVisibleDropdownOptions(action); const currentValue = actionStates.value[actionId]?.value ?? action.defaultOption ?? 0; - if (action.options && action.options[currentValue]) { - return action.options[currentValue]; + const allOptions = action.options; + const storedOption = allOptions[currentValue]; + + if (storedOption && visibleOptions.includes(storedOption)) { + return storedOption; } - return action.options?.[0] || null; + return visibleOptions[0] || null; } function isActionSelected(action: Action): boolean { @@ -775,20 +788,31 @@ function selectDropdownOption(action: DropdownAction, selected: any) { function isChipSelected(action: MultiSelectChipsAction, optionIndex: number): boolean { const actionId = getActionId(action); const selectedSet = actionStates.value[actionId]?.value as Set | undefined; - return selectedSet?.has(optionIndex) || false; + + const visibleOptions = getVisibleMultiSelectOptions(action); + const visibleOption = visibleOptions[optionIndex]; + const originalIndex = action.options.findIndex((opt) => opt === visibleOption); + + return selectedSet?.has(originalIndex) || false; } function toggleChip(action: MultiSelectChipsAction, optionIndex: number) { const actionId = getActionId(action); const state = actionStates.value[actionId]; if (state && state.value instanceof Set) { - if (state.value.has(optionIndex)) { - state.value.delete(optionIndex); - } else { - state.value.add(optionIndex); + const visibleOptions = getVisibleMultiSelectOptions(action); + const visibleOption = visibleOptions[optionIndex]; + const originalIndex = action.options.findIndex((opt) => opt === visibleOption); + + if (originalIndex !== -1) { + if (state.value.has(originalIndex)) { + state.value.delete(originalIndex); + } else { + state.value.add(originalIndex); + } + state.selected = state.value.size > 0; + persistState(); } - state.selected = state.value.size > 0; - persistState(); } } @@ -869,9 +893,23 @@ async function processAction( stageIndex: number, messageParts: MessagePart[], ) { + const allValidActionIds: string[] = []; + checklist.forEach((stage, stageIdx) => { + stage.actions.forEach((stageAction, actionIdx) => { + allValidActionIds.push(getActionIdForStage(stageAction, stageIdx, actionIdx)); + if (stageAction.enablesActions) { + stageAction.enablesActions.forEach((enabledAction, enabledIdx) => { + allValidActionIds.push( + getActionIdForStage(enabledAction, stageIdx, actionIdx, enabledIdx), + ); + }); + } + }); + }); + if (action.type === "button" || action.type === "toggle") { const buttonAction = action as ButtonAction | ToggleAction; - const message = await getActionMessage(buttonAction, selectedActionIds); + const message = await getActionMessage(buttonAction, selectedActionIds, allValidActionIds); if (message) { messageParts.push({ weight: buttonAction.weight, @@ -885,16 +923,29 @@ async function processAction( const matchingVariant = findMatchingVariant( conditionalAction.messageVariants, selectedActionIds, + allValidActionIds, + stageIndex, ); + + let message: string; + let weight: number; + if (matchingVariant) { - const message = (await matchingVariant.message()) as string; - messageParts.push({ - weight: matchingVariant.weight, - content: processMessage(message, action, stageIndex, textInputValues.value), - actionId, - stageIndex, - }); + message = (await matchingVariant.message()) as string; + weight = matchingVariant.weight; + } else if (conditionalAction.fallbackMessage) { + message = (await conditionalAction.fallbackMessage()) as string; + weight = conditionalAction.fallbackWeight ?? 0; + } else { + return; } + + messageParts.push({ + weight, + content: processMessage(message, action, stageIndex, textInputValues.value), + actionId, + stageIndex, + }); } else if (action.type === "dropdown") { const dropdownAction = action as DropdownAction; const selectedIndex = state.value ?? 0; @@ -929,6 +980,18 @@ async function processAction( } function shouldShowStage(stage: Stage): boolean { + let hasVisibleActions = false; + + for (const a of stage.actions) { + if (shouldShowAction(a)) { + hasVisibleActions = true; + } + } + + if (!hasVisibleActions) { + return false; + } + if (typeof stage.shouldShow === "function") { return stage.shouldShow(props.project); } @@ -944,6 +1007,24 @@ function shouldShowAction(action: Action): boolean { return true; } +function getVisibleDropdownOptions(action: DropdownAction) { + return action.options.filter((option) => { + if (typeof option.shouldShow === "function") { + return option.shouldShow(props.project); + } + return true; + }); +} + +function getVisibleMultiSelectOptions(action: MultiSelectChipsAction) { + return action.options.filter((option) => { + if (typeof option.shouldShow === "function") { + return option.shouldShow(props.project); + } + return true; + }); +} + function shouldShowStageIndex(stageIndex: number): boolean { return shouldShowStage(checklist[stageIndex]); } @@ -1021,7 +1102,20 @@ async function generateMessage() { } } - message.value = fullMessage; + try { + const formattedMessage = await prettier.format(fullMessage, { + parser: "markdown", + printWidth: 80, + proseWrap: "always", + tabWidth: 2, + useTabs: false, + }); + message.value = formattedMessage; + } catch (formattingError) { + console.warn("Failed to format markdown, using original:", formattingError); + message.value = fullMessage; + } + generatedMessage.value = true; } catch (error) { console.error("Error generating message:", error); diff --git a/apps/frontend/src/components/ui/report/ReportInfo.vue b/apps/frontend/src/components/ui/report/ReportInfo.vue index 5e2b924c1..74bdfd0d8 100644 --- a/apps/frontend/src/components/ui/report/ReportInfo.vue +++ b/apps/frontend/src/components/ui/report/ReportInfo.vue @@ -172,6 +172,7 @@ const flags = useFeatureFlags(); .markdown-body { grid-area: body; + max-width: 100%; } .reporter-info { diff --git a/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue b/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue index b142bb814..d204e7aee 100644 --- a/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue +++ b/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue @@ -31,9 +31,9 @@ class="flex cursor-pointer items-center gap-1 bg-transparent p-0" @click=" versionFilter && - (unlockFilterAccordion.isOpen - ? unlockFilterAccordion.close() - : unlockFilterAccordion.open()) + (unlockFilterAccordion.isOpen + ? unlockFilterAccordion.close() + : unlockFilterAccordion.open()) " > { - if (!this.general?.datacenter) { - console.warn("No datacenter info available for ping test"); + if (!this.general?.node?.instance) { + console.warn("No node instance available for ping test"); return false; } - const datacenter = this.general.datacenter; - const wsUrl = `wss://${datacenter}.nodes.modrinth.com/pingtest`; + const wsUrl = `wss://${this.general.node.instance}/pingtest`; try { return await new Promise((resolve) => { diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index 5b5d925b1..45bd48c68 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -112,7 +112,8 @@ export async function useServersFetch( const response = await $fetch(fullUrl, { method, headers, - body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined, + body: + body && contentType === "application/json" ? JSON.stringify(body) : (body ?? undefined), timeout: 10000, }); diff --git a/apps/frontend/src/layouts/default.vue b/apps/frontend/src/layouts/default.vue index 643765456..842eaf376 100644 --- a/apps/frontend/src/layouts/default.vue +++ b/apps/frontend/src/layouts/default.vue @@ -1346,6 +1346,15 @@ const footerLinks = [ }), ), }, + { + href: "/legal/copyright", + label: formatMessage( + defineMessage({ + id: "layout.footer.legal.copyright-policy", + defaultMessage: "Copyright Policy and DMCA", + }), + ), + }, ], }, ]; diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json index dff91ec44..468227e53 100644 --- a/apps/frontend/src/locales/en-US/index.json +++ b/apps/frontend/src/locales/en-US/index.json @@ -404,6 +404,9 @@ "layout.footer.legal-disclaimer": { "message": "NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT." }, + "layout.footer.legal.copyright-policy": { + "message": "Copyright Policy and DMCA" + }, "layout.footer.legal.privacy-policy": { "message": "Privacy Policy" }, diff --git a/apps/frontend/src/pages/[type]/[id].vue b/apps/frontend/src/pages/[type]/[id].vue index 0b51f73bb..7167610ee 100644 --- a/apps/frontend/src/pages/[type]/[id].vue +++ b/apps/frontend/src/pages/[type]/[id].vue @@ -29,12 +29,11 @@ class="settings-header__icon" />
-

- {{ project.title }} -

+

{{ project.title }}

+

Project settings

+
+
@@ -716,25 +719,14 @@ :dropdown-id="`${baseId}-more-options`" >
+

{{ formatMessage(detailsMessages.title) }}

+
+
+
+
+
+
-
- -
+
+