Merge commit 'daf699911104207a477751916b36a371ee8f7e38' into feature-clean

This commit is contained in:
2025-04-19 17:29:54 +03:00
89 changed files with 4249 additions and 2575 deletions

View File

@@ -9,7 +9,6 @@ bytes = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_ini = "0.2.0"
toml = "0.8.12"
sha1_smol = { version = "1.0.0", features = ["std"] }
sha2 = "0.10.8"
url = "2.2"
@@ -18,7 +17,6 @@ zip = "0.6.5"
async_zip = { version = "0.0.17", features = ["chrono", "tokio-fs", "deflate", "bzip2", "zstd", "deflate64"] }
flate2 = "1.0.28"
tempfile = "3.5.0"
urlencoding = "2.1.3"
dashmap = { version = "6.0.1", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] }

View File

@@ -78,7 +78,7 @@ pub async fn import_curseforge(
let icon_bytes =
fetch(&thumbnail_url, None, &state.fetch_semaphore, &state.pool)
.await?;
let filename = thumbnail_url.rsplit('/').last();
let filename = thumbnail_url.rsplit('/').next_back();
if let Some(filename) = filename {
icon = Some(
write_cached_icon(

View File

@@ -139,9 +139,7 @@ pub async fn write(
})
})
.await
.map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "background task failed")
})??;
.map_err(|_| std::io::Error::other("background task failed"))??;
Ok(())
}
@@ -152,8 +150,7 @@ fn sync_write(
) -> Result<(), std::io::Error> {
let mut tempfile =
NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
std::io::Error::other(
"could not get parent directory for temporary file",
)
})?)?;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot-icon lucide-bot"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-archive-icon lucide-folder-archive"><circle cx="15" cy="19" r="2"/><path d="M20.9 19.8A2 2 0 0 0 22 18V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h5.1"/><path d="M15 11v-1"/><path d="M15 17v-2"/></svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw-icon lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw-icon lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -40,6 +40,7 @@ import _BellRingIcon from './icons/bell-ring.svg?component'
import _BookIcon from './icons/book.svg?component'
import _BookTextIcon from './icons/book-text.svg?component'
import _BookmarkIcon from './icons/bookmark.svg?component'
import _BotIcon from './icons/bot.svg?component'
import _BoxIcon from './icons/box.svg?component'
import _BoxImportIcon from './icons/box-import.svg?component'
import _BracesIcon from './icons/braces.svg?component'
@@ -76,6 +77,7 @@ import _FileIcon from './icons/file.svg?component'
import _FileTextIcon from './icons/file-text.svg?component'
import _FilterIcon from './icons/filter.svg?component'
import _FilterXIcon from './icons/filter-x.svg?component'
import _FolderArchiveIcon from './icons/folder-archive.svg?component'
import _FolderOpenIcon from './icons/folder-open.svg?component'
import _FolderSearchIcon from './icons/folder-search.svg?component'
import _GapIcon from './icons/gap.svg?component'
@@ -137,6 +139,8 @@ import _ReplyIcon from './icons/reply.svg?component'
import _ReportIcon from './icons/report.svg?component'
import _RestoreIcon from './icons/restore.svg?component'
import _RightArrowIcon from './icons/right-arrow.svg?component'
import _RotateCounterClockwiseIcon from './icons/rotate-ccw.svg?component'
import _RotateClockwiseIcon from './icons/rotate-cw.svg?component'
import _SaveIcon from './icons/save.svg?component'
import _ScaleIcon from './icons/scale.svg?component'
import _ScanEyeIcon from './icons/scan-eye.svg?component'
@@ -254,6 +258,7 @@ export const BellRingIcon = _BellRingIcon
export const BookIcon = _BookIcon
export const BookTextIcon = _BookTextIcon
export const BookmarkIcon = _BookmarkIcon
export const BotIcon = _BotIcon
export const BoxIcon = _BoxIcon
export const BoxImportIcon = _BoxImportIcon
export const BracesIcon = _BracesIcon
@@ -290,6 +295,7 @@ export const FileIcon = _FileIcon
export const FileTextIcon = _FileTextIcon
export const FilterIcon = _FilterIcon
export const FilterXIcon = _FilterXIcon
export const FolderArchiveIcon = _FolderArchiveIcon
export const FolderOpenIcon = _FolderOpenIcon
export const FolderSearchIcon = _FolderSearchIcon
export const GapIcon = _GapIcon
@@ -351,6 +357,8 @@ export const ReplyIcon = _ReplyIcon
export const ReportIcon = _ReportIcon
export const RestoreIcon = _RestoreIcon
export const RightArrowIcon = _RightArrowIcon
export const RotateCounterClockwiseIcon = _RotateCounterClockwiseIcon
export const RotateClockwiseIcon = _RotateClockwiseIcon
export const SaveIcon = _SaveIcon
export const ScaleIcon = _ScaleIcon
export const ScanEyeIcon = _ScanEyeIcon

View File

@@ -17,5 +17,4 @@ readme = "README.md"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
bytes = "1"
thiserror = "1.0"

View File

@@ -2,14 +2,14 @@
<div v-bind="$attrs">
<button
v-if="!!slots.title"
:class="buttonClass ?? 'flex flex-col gap-2'"
:class="buttonClass ?? 'flex flex-col gap-2 bg-transparent m-0 p-0 border-none'"
@click="() => (isOpen ? close() : open())"
>
<slot name="button" :open="isOpen">
<div class="flex items-center w-full">
<div class="flex items-center gap-1 w-full">
<slot name="title" />
<DropdownIcon
class="ml-auto size-5 transition-transform duration-300 shrink-0 text-contrast"
class="ml-auto size-5 transition-transform duration-300 shrink-0"
:class="{ 'rotate-180': isOpen }"
/>
</div>

View File

@@ -10,13 +10,16 @@
:class="['hidden h-8 w-8 flex-none sm:block', iconClasses[type]]"
/>
<div class="flex flex-col gap-2">
<div class="font-semibold">
<div class="font-semibold flex justify-between gap-4">
<slot name="header">{{ header }}</slot>
</div>
<div class="font-normal">
<slot>{{ body }}</slot>
</div>
</div>
<div class="ml-auto w-fit">
<slot name="actions" />
</div>
</div>
</template>

View File

@@ -6,7 +6,7 @@ const props = withDefaults(
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
size?: 'standard' | 'large'
circular?: boolean
type?: 'standard' | 'outlined' | 'transparent' | 'highlight'
type?: 'standard' | 'outlined' | 'transparent' | 'highlight' | 'highlight-colored-text'
colorFill?: 'auto' | 'background' | 'text' | 'none'
hoverColorFill?: 'auto' | 'background' | 'text' | 'none'
highlightedStyle?: 'main-nav-primary' | 'main-nav-secondary'
@@ -24,20 +24,40 @@ const props = withDefaults(
},
)
const highlightedColorVar = computed(() => {
switch (props.color) {
case 'brand':
return 'var(--color-brand-highlight)'
case 'red':
return 'var(--color-red-highlight)'
case 'orange':
return 'var(--color-orange-highlight)'
case 'green':
return 'var(--color-green-highlight)'
case 'blue':
return 'var(--color-blue-highlight)'
case 'purple':
return 'var(--color-purple-highlight)'
case 'standard':
default:
return null
}
})
const colorVar = computed(() => {
switch (props.color) {
case 'brand':
return props.type === 'highlight' ? 'var(--color-brand-highlight)' : 'var(--color-brand)'
return 'var(--color-brand)'
case 'red':
return props.type === 'highlight' ? 'var(--color-red-highlight)' : 'var(--color-red)'
return 'var(--color-red)'
case 'orange':
return props.type === 'highlight' ? 'var(--color-orange-highlight)' : 'var(--color-orange)'
return 'var(--color-orange)'
case 'green':
return props.type === 'highlight' ? 'var(--color-green-highlight)' : 'var(--color-green)'
return 'var(--color-green)'
case 'blue':
return props.type === 'highlight' ? 'var(--color-blue-highlight)' : 'var(--color-blue)'
return 'var(--color-blue)'
case 'purple':
return props.type === 'highlight' ? 'var(--color-purple-highlight)' : 'var(--color-purple)'
return 'var(--color-purple)'
case 'standard':
default:
return null
@@ -111,10 +131,14 @@ function setColorFill(
): { bg: string; text: string } {
if (colorVar.value) {
if (fill === 'background') {
colors.bg = colorVar.value
if (props.type === 'highlight') {
if (props.type === 'highlight' && highlightedColorVar.value) {
colors.bg = highlightedColorVar.value
colors.text = 'var(--color-contrast)'
} else if (props.type === 'highlight-colored-text' && highlightedColorVar.value) {
colors.bg = highlightedColorVar.value
colors.text = colorVar.value
} else {
colors.bg = colorVar.value
colors.text = 'var(--color-accent-contrast)'
}
} else if (fill === 'text') {
@@ -195,7 +219,7 @@ const colorVariables = computed(() => {
> *:first-child
> *:first-child
> :is(button, a, .button-like):first-child {
@apply flex cursor-pointer flex-row items-center justify-center border-solid border-2 border-transparent bg-[--_bg] text-[--_text] h-[--_height] min-w-[--_width] rounded-[--_radius] px-[--_padding-x] py-[--_padding-y] gap-[--_gap] font-[--_font-weight];
@apply flex cursor-pointer flex-row items-center justify-center border-solid border-2 border-transparent bg-[--_bg] text-[--_text] h-[--_height] min-w-[--_width] rounded-[--_radius] px-[--_padding-x] py-[--_padding-y] gap-[--_gap] font-[--_font-weight] whitespace-nowrap;
transition:
scale 0.125s ease-in-out,
background-color 0.25s ease-in-out,
@@ -204,6 +228,7 @@ const colorVariables = computed(() => {
svg:first-child {
color: var(--_icon, var(--_text));
transition: color 0.25s ease-in-out;
flex-shrink: 0;
}
&[disabled],

View File

@@ -11,10 +11,10 @@
</h1>
<slot name="title-suffix" />
</div>
<p class="m-0 line-clamp-2 max-w-[40rem] empty:hidden">
<p v-if="$slots.summary" class="m-0 line-clamp-2 max-w-[40rem] empty:hidden">
<slot name="summary" />
</p>
<div class="mt-auto flex flex-wrap gap-4 empty:hidden">
<div v-if="$slots.stats" class="mt-auto flex flex-wrap gap-4 empty:hidden">
<slot name="stats" />
</div>
</div>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
progress: number
max?: number
color?: 'brand' | 'green' | 'red' | 'orange' | 'blue' | 'purple' | 'gray'
waiting?: boolean
}>(),
{
max: 1,
color: 'brand',
waiting: false,
},
)
const colors = {
brand: {
fg: 'bg-brand',
bg: 'bg-brand-highlight',
},
green: {
fg: 'bg-green',
bg: 'bg-bg-green',
},
red: {
fg: 'bg-red',
bg: 'bg-bg-red',
},
orange: {
fg: 'bg-orange',
bg: 'bg-bg-orange',
},
blue: {
fg: 'bg-blue',
bg: 'bg-bg-blue',
},
purple: {
fg: 'bg-purple',
bg: 'bg-bg-purple',
},
gray: {
fg: 'bg-gray',
bg: 'bg-bg-gray',
},
}
const percent = computed(() => props.progress / props.max)
</script>
<template>
<div class="flex w-[15rem] h-1 rounded-full overflow-hidden" :class="colors[props.color].bg">
<div
class="rounded-full progress-bar"
:class="[colors[props.color].fg, { 'progress-bar--waiting': waiting }]"
:style="!waiting ? { width: `${percent * 100}%` } : {}"
></div>
</div>
</template>
<style scoped lang="scss">
.progress-bar {
transition: width 0.2s ease-in-out;
}
.progress-bar--waiting {
animation: progress-bar-waiting 1s linear infinite;
position: relative;
}
@keyframes progress-bar-waiting {
0% {
left: -50%;
width: 20%;
}
50% {
width: 60%;
}
100% {
left: 100%;
width: 20%;
}
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<div
v-if="level === 'survey'"
class="flex items-center gap-2 border-2 border-solid border-brand-purple bg-bg-purple p-4 rounded-2xl"
>
<span class="text-contrast font-bold">Survey ID:</span> <CopyCode :text="message" />
</div>
<Admonition v-else :type="NOTICE_TYPE[level]">
<template #header>
<template v-if="!hideDefaultTitle">
{{ formatMessage(heading) }}
</template>
<template v-if="title">
<template v-if="hideDefaultTitle">
{{ title.substring(1) }}
</template>
<template v-else> - {{ title }}</template>
</template>
</template>
<template #actions>
<ButtonStyled v-if="dismissable" circular>
<button
v-tooltip="formatMessage(messages.dismiss)"
@click="() => (preview ? {} : emit('dismiss'))"
>
<XIcon />
</button>
</ButtonStyled>
</template>
<div v-if="message" class="markdown-body" v-html="renderString(message)" />
</Admonition>
</template>
<script setup lang="ts">
import { renderString } from '@modrinth/utils'
import { Admonition } from '../index'
import { XIcon } from '@modrinth/assets'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import ButtonStyled from './ButtonStyled.vue'
import CopyCode from './CopyCode.vue'
const { formatMessage } = useVIntl()
const emit = defineEmits<{
(e: 'dismiss'): void
}>()
const props = withDefaults(
defineProps<{
level: string
message: string
dismissable: boolean
preview?: boolean
title?: string
}>(),
{
preview: false,
title: undefined,
},
)
const hideDefaultTitle = computed(
() => props.title && props.title.length > 1 && props.title.startsWith('\\'),
)
const messages = defineMessages({
info: {
id: 'servers.notice.heading.info',
defaultMessage: 'Info',
},
attention: {
id: 'servers.notice.heading.attention',
defaultMessage: 'Attention',
},
dismiss: {
id: 'servers.notice.dismiss',
defaultMessage: 'Dismiss',
},
})
const NOTICE_HEADINGS: Record<string, MessageDescriptor> = {
info: messages.info,
warn: messages.attention,
critical: messages.attention,
}
const NOTICE_TYPE: Record<string, 'info' | 'warning' | 'critical'> = {
info: 'info',
warn: 'warning',
critical: 'critical',
}
const heading = computed(() => NOTICE_HEADINGS[props.level] ?? messages.info)
</script>
<style scoped lang="scss">
.markdown-body > *:first-child {
margin-top: 0;
}
</style>

View File

@@ -106,13 +106,16 @@
</p>
<IssuesIcon
v-if="customServerConfig.ramInGb < 4"
v-tooltip="'This might not be enough resources for your Minecraft server.'"
v-tooltip="'This might not be powerful enough for your Minecraft server.'"
class="h-6 w-6 text-orange"
/>
</div>
<p v-if="existingPlan" class="mt-1 mb-2 text-secondary">
Your current plan has <strong>{{ existingPlan.metadata.ram / 1024 }} GB RAM</strong> and
<strong>{{ existingPlan.metadata.cpu }} vCPUs</strong>.
<strong
>{{ existingPlan.metadata.cpu / 2 }} shared CPUs (bursts up to
{{ existingPlan.metadata.cpu }} CPUs)</strong
>.
</p>
<div class="flex flex-col gap-4">
<div class="flex w-full gap-2 items-center">
@@ -131,12 +134,28 @@
class="flex sm:flex-row flex-col gap-4 w-full"
>
<div class="flex flex-col w-full gap-2">
<div class="font-semibold">vCPUs</div>
<input v-model="mutatedProduct.metadata.cpu" disabled class="input" />
<div class="font-semibold">Shared CPUs</div>
<input :value="sharedCpus" disabled class="input w-full" />
</div>
<div class="flex flex-col w-full gap-2">
<div class="font-semibold flex items-center gap-1">
Max Burst CPUs
<UnknownIcon
v-tooltip="
'CPU bursting allows your server to temporarily use additional threads to help mitigate TPS spikes. See Modrinth Servers FAQ for more info.'
"
class="h-4 w-4text-secondary opacity-60"
/>
</div>
<input :value="mutatedProduct.metadata.cpu" disabled class="input w-full" />
</div>
<div class="flex flex-col w-full gap-2">
<div class="font-semibold">Storage</div>
<input v-model="customServerConfig.storageGbFormatted" disabled class="input" />
<input
v-model="customServerConfig.storageGbFormatted"
disabled
class="input w-full"
/>
</div>
</div>
<Admonition
@@ -153,10 +172,11 @@
later, or try a different amount.
</Admonition>
<div class="flex items-center gap-2">
<InfoIcon class="hidden sm:block" />
<div class="flex gap-2">
<InfoIcon class="hidden sm:block shrink-0 mt-1" />
<span class="text-sm text-secondary">
Storage and vCPUs are currently not configurable.
Storage and shared CPU count are currently not configurable independently, and are
based on the amount of RAM you select.
</span>
</div>
</div>
@@ -500,6 +520,7 @@
import { ref, computed, nextTick, reactive, watch } from 'vue'
import NewModal from '../modal/NewModal.vue'
import {
UnknownIcon,
SpinnerIcon,
CardIcon,
CheckCircleIcon,
@@ -765,7 +786,11 @@ function updateRamValues() {
customMinRam.value = Math.min(...ramValues)
customMaxRam.value = Math.max(...ramValues)
customServerConfig.ramInGb = customMinRam.value
if (props.product.some((product) => product.metadata.ram / 1024 === 4)) {
customServerConfig.ramInGb = 4
} else {
customServerConfig.ramInGb = customMinRam.value
}
}
if (props.customServer) {
@@ -832,6 +857,10 @@ const metadata = computed(() => {
return null
})
const sharedCpus = computed(() => {
return (mutatedProduct.value?.metadata?.cpu ?? 0) / 2
})
function nextStep() {
if (
mutatedProduct.value.metadata.type === 'pyro' &&

View File

@@ -26,10 +26,12 @@ export { default as Page } from './base/Page.vue'
export { default as Pagination } from './base/Pagination.vue'
export { default as PopoutMenu } from './base/PopoutMenu.vue'
export { default as PreviewSelectButton } from './base/PreviewSelectButton.vue'
export { default as ProgressBar } from './base/ProgressBar.vue'
export { default as ProjectCard } from './base/ProjectCard.vue'
export { default as RadialHeader } from './base/RadialHeader.vue'
export { default as RadioButtons } from './base/RadioButtons.vue'
export { default as ScrollablePanel } from './base/ScrollablePanel.vue'
export { default as ServerNotice } from './base/ServerNotice.vue'
export { default as SimpleBadge } from './base/SimpleBadge.vue'
export { default as Slider } from './base/Slider.vue'
export { default as StatItem } from './base/StatItem.vue'
@@ -97,3 +99,6 @@ export { default as VersionSummary } from './version/VersionSummary.vue'
// Settings
export { default as ThemeSelector } from './settings/ThemeSelector.vue'
// Servers
export { default as BackupWarning } from './servers/backups/BackupWarning.vue'

View File

@@ -5,26 +5,28 @@
<span class="font-extrabold text-contrast text-lg">{{ title }}</span>
</slot>
</template>
<div>
<div class="markdown-body max-w-[35rem]" v-html="renderString(description)" />
<label v-if="hasToType" for="confirmation" class="confirmation-label">
<div class="flex flex-col gap-4">
<div
v-if="description"
class="markdown-body max-w-[35rem]"
v-html="renderString(description)"
/>
<slot />
<label v-if="hasToType" for="confirmation">
<span>
<strong>To verify, type</strong>
<em class="confirmation-text"> {{ confirmationText }} </em>
<strong>below:</strong>
To confirm you want to proceed, type
<span class="italic font-bold">{{ confirmationText }}</span> below:
</span>
</label>
<div class="confirmation-input">
<input
v-if="hasToType"
id="confirmation"
v-model="confirmation_typed"
type="text"
placeholder="Type here..."
@input="type"
/>
</div>
<div class="flex gap-2 mt-6">
<input
v-if="hasToType"
id="confirmation"
v-model="confirmation_typed"
type="text"
placeholder="Type here..."
class="max-w-[20rem]"
/>
<div class="flex gap-2">
<ButtonStyled :color="danger ? 'red' : 'brand'">
<button :disabled="action_disabled" @click="proceed">
<component :is="proceedIcon" />
@@ -65,8 +67,8 @@ const props = defineProps({
},
description: {
type: String,
default: 'No description defined',
required: true,
default: undefined,
required: false,
},
proceedIcon: {
type: Object,
@@ -95,21 +97,20 @@ const props = defineProps({
const emit = defineEmits(['proceed'])
const modal = ref(null)
const action_disabled = ref(props.hasToType)
const confirmation_typed = ref('')
const action_disabled = computed(
() =>
props.hasToType &&
confirmation_typed.value.toLowerCase() !== props.confirmationText.toLowerCase(),
)
function proceed() {
modal.value.hide()
confirmation_typed.value = ''
emit('proceed')
}
function type() {
if (props.hasToType) {
action_disabled.value =
confirmation_typed.value.toLowerCase() !== props.confirmationText.toLowerCase()
}
}
function show() {
modal.value.show()
}

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { IssuesIcon } from '@modrinth/assets'
import AutoLink from '../../base/AutoLink.vue'
defineProps<{
backupLink: string
}>()
</script>
<template>
<div
class="flex gap-3 rounded-2xl border-2 border-solid border-orange bg-bg-orange px-4 py-3 font-medium text-contrast"
>
<IssuesIcon class="mt-1 h-5 w-5 shrink-0 text-orange" />
<span class="leading-normal">
You may want to
<AutoLink
:to="backupLink"
class="font-semibold text-orange hover:underline active:brightness-125"
>create a backup</AutoLink
>
before proceeding, as this process is irreversible and may permanently alter your world or the
files on your server.
</span>
</div>
</template>

View File

@@ -11,6 +11,12 @@
"button.create-a-project": {
"defaultMessage": "Create a project"
},
"button.download": {
"defaultMessage": "Download"
},
"button.downloading": {
"defaultMessage": "Downloading"
},
"button.edit": {
"defaultMessage": "Edit"
},
@@ -404,6 +410,33 @@
"search.filter_type.shader_loader": {
"defaultMessage": "Loader"
},
"servers.notice.dismiss": {
"defaultMessage": "Dismiss"
},
"servers.notice.dismissable": {
"defaultMessage": "Dismissable"
},
"servers.notice.heading.attention": {
"defaultMessage": "Attention"
},
"servers.notice.heading.info": {
"defaultMessage": "Info"
},
"servers.notice.level.critical.name": {
"defaultMessage": "Critical"
},
"servers.notice.level.info.name": {
"defaultMessage": "Info"
},
"servers.notice.level.survey.name": {
"defaultMessage": "Survey"
},
"servers.notice.level.warn.name": {
"defaultMessage": "Warning"
},
"servers.notice.undismissable": {
"defaultMessage": "Undismissable"
},
"settings.account.title": {
"defaultMessage": "Account and security"
},

View File

@@ -49,6 +49,14 @@ export const commonMessages = defineMessages({
id: 'label.description',
defaultMessage: 'Description',
},
downloadButton: {
id: 'button.download',
defaultMessage: 'Download',
},
downloadingButton: {
id: 'button.downloading',
defaultMessage: 'Downloading',
},
editButton: {
id: 'button.edit',
defaultMessage: 'Edit',
@@ -145,6 +153,10 @@ export const commonMessages = defineMessages({
id: 'button.upload-image',
defaultMessage: 'Upload image',
},
removeImageButton: {
id: 'button.remove-image',
defaultMessage: 'Remove image',
},
visibilityLabel: {
id: 'label.visibility',
defaultMessage: 'Visibility',

View File

@@ -0,0 +1,73 @@
import { defineMessage, type MessageDescriptor } from '@vintl/vintl'
export const NOTICE_LEVELS: Record<
string,
{ name: MessageDescriptor; colors: { text: string; bg: string } }
> = {
info: {
name: defineMessage({
id: 'servers.notice.level.info.name',
defaultMessage: 'Info',
}),
colors: {
text: 'var(--color-blue)',
bg: 'var(--color-blue-bg)',
},
},
warn: {
name: defineMessage({
id: 'servers.notice.level.warn.name',
defaultMessage: 'Warning',
}),
colors: {
text: 'var(--color-orange)',
bg: 'var(--color-orange-bg)',
},
},
critical: {
name: defineMessage({
id: 'servers.notice.level.critical.name',
defaultMessage: 'Critical',
}),
colors: {
text: 'var(--color-red)',
bg: 'var(--color-red-bg)',
},
},
survey: {
name: defineMessage({
id: 'servers.notice.level.survey.name',
defaultMessage: 'Survey',
}),
colors: {
text: 'var(--color-purple)',
bg: 'var(--color-purple-bg)',
},
},
}
const DISMISSABLE = {
name: defineMessage({
id: 'servers.notice.dismissable',
defaultMessage: 'Dismissable',
}),
colors: {
text: 'var(--color-green)',
bg: 'var(--color-green-bg)',
},
}
const UNDISMISSABLE = {
name: defineMessage({
id: 'servers.notice.undismissable',
defaultMessage: 'Undismissable',
}),
colors: {
text: 'var(--color-red)',
bg: 'var(--color-red-bg)',
},
}
export function getDismissableMetadata(dismissable: boolean) {
return dismissable ? DISMISSABLE : UNDISMISSABLE
}

View File

@@ -10,6 +10,45 @@ export type VersionEntry = {
}
const VERSIONS: VersionEntry[] = [
{
date: `2025-04-18T22:30:00-07:00`,
product: 'web',
body: `### Improvements
- Updated Modrinth Servers marketing page to be accurate to post-Pyro infrastructure.`,
},
{
date: `2025-04-17T02:25:00-07:00`,
product: 'servers',
body: `### Improvements
- Completely overhauled the Backups interface and fixed them being non-functional.
- Backups will now show progress when creating and restoring.
- Backups now have a "Prepare download" phase, which will prepare a backup file for downloading.
- You can now cancel a backup in progress and retry a failed backup.
- When a backup is in progress, you will no longer be allowed to modify the modpack or loader.
- Removed the ability to create backups on install automatically, and replaced with a notice that you may want to create a backup before installing a new modpack or loader. This is because the previous implementation of backup on install was unreliable and buggy. We are working on a better implementation for this feature and plan for it to return in the future.
- Temporarily disabled auto backups button, since they are currently not working.`,
},
{
date: `2025-04-15T16:35:00-07:00`,
product: 'servers',
body: `### Added
- Added ability to send surveys to customers in the panel via notices.
### Improvements
- Added titles to notices.`,
},
{
date: `2025-04-12T22:10:00-07:00`,
product: 'servers',
body: `### Added
- Added ability to notify customers in the panel with notices concerning their servers.`,
},
{
date: `2025-04-12T22:10:00-07:00`,
product: 'web',
body: `### Improvements
- Fix missing dropdown icon in publishing checklist.`,
},
{
date: `2025-04-01T21:15:00-07:00`,
product: 'web',

View File

@@ -252,3 +252,22 @@ export type Report = {
created: string
body: string
}
export type ServerNotice = {
id: number
message: string
title?: string
level: 'info' | 'warn' | 'critical' | 'survey'
dismissable: boolean
announce_at: string
expires: string
assigned: {
kind: 'server' | 'node'
id: string
name: string
}[]
dismissed_by: {
server: string
dismissed_on: string
}[]
}