You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit 'daf699911104207a477751916b36a371ee8f7e38' into feature-clean
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
})?)?;
|
||||
|
||||
1
packages/assets/icons/bot.svg
Normal file
1
packages/assets/icons/bot.svg
Normal 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 |
1
packages/assets/icons/folder-archive.svg
Normal file
1
packages/assets/icons/folder-archive.svg
Normal 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 |
1
packages/assets/icons/rotate-ccw.svg
Normal file
1
packages/assets/icons/rotate-ccw.svg
Normal 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 |
1
packages/assets/icons/rotate-cw.svg
Normal file
1
packages/assets/icons/rotate-cw.svg
Normal 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 |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
83
packages/ui/src/components/base/ProgressBar.vue
Normal file
83
packages/ui/src/components/base/ProgressBar.vue
Normal 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>
|
||||
99
packages/ui/src/components/base/ServerNotice.vue
Normal file
99
packages/ui/src/components/base/ServerNotice.vue
Normal 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>
|
||||
@@ -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' &&
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
25
packages/ui/src/components/servers/backups/BackupWarning.vue
Normal file
25
packages/ui/src/components/servers/backups/BackupWarning.vue
Normal 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>
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
73
packages/ui/src/utils/notices.ts
Normal file
73
packages/ui/src/utils/notices.ts
Normal 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
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user