mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
684b385a0f | ||
|
|
97d1aa4374 | ||
|
|
0109fdfef6 | ||
|
|
e9d42f276c | ||
|
|
34e1db3a30 | ||
|
|
95b1e71ea3 | ||
|
|
a85c7ca6f2 |
@@ -35,6 +35,7 @@
|
||||
"fuse.js": "^6.6.2",
|
||||
"intl-messageformat": "^10.7.7",
|
||||
"ofetch": "^1.3.4",
|
||||
"overlayscrollbars": "^2.15.1",
|
||||
"pinia": "^3.0.0",
|
||||
"posthog-js": "^1.158.2",
|
||||
"three": "^0.172.0",
|
||||
|
||||
@@ -157,6 +157,11 @@ provideModrinthClient(tauriApiClient)
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(true),
|
||||
showAds: ref(false),
|
||||
featureFlags: {
|
||||
serverRamAsBytesAlwaysOn: computed(() =>
|
||||
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
|
||||
),
|
||||
},
|
||||
openExternalUrl: (url) => openUrl(url),
|
||||
})
|
||||
provideModalBehavior({
|
||||
@@ -423,6 +428,13 @@ loading.startLoading()
|
||||
|
||||
let suspensePending = false
|
||||
|
||||
const sidebarOverlayScrollbarsOptions = Object.freeze({
|
||||
overflow: {
|
||||
x: 'hidden',
|
||||
y: 'scroll',
|
||||
},
|
||||
})
|
||||
|
||||
router.beforeEach(() => {
|
||||
suspensePending = false
|
||||
loading.startLoading()
|
||||
@@ -1274,29 +1286,26 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</RouterView>
|
||||
</div>
|
||||
<div
|
||||
class="app-sidebar mt-px shrink-0 flex flex-col border-0 border-l-[1px] border-[--brand-gradient-border] border-solid overflow-auto"
|
||||
class="app-sidebar mt-px shrink-0 flex flex-col border-0 border-l-[1px] border-[--brand-gradient-border] border-solid"
|
||||
:class="{ 'has-plus': hasPlus }"
|
||||
data-overlayscrollbars-initialize
|
||||
v-overlay-scrollbars="sidebarOverlayScrollbarsOptions"
|
||||
>
|
||||
<div
|
||||
class="app-sidebar-scrollable flex-grow shrink overflow-y-auto relative"
|
||||
:class="{ 'pb-12': !hasPlus }"
|
||||
>
|
||||
<div class="app-sidebar-scrollable flex-grow shrink relative" :class="{ 'pb-12': !hasPlus }">
|
||||
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
|
||||
<div class="sidebar-default-content" :class="{ 'sidebar-enabled': sidebarVisible }">
|
||||
<div
|
||||
class="p-4 pr-1 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
|
||||
>
|
||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<h3 class="text-base text-primary font-medium m-0">Playing as</h3>
|
||||
<suspense>
|
||||
<AccountsCard ref="accounts" mode="small" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div class="py-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<suspense>
|
||||
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div v-if="news && news.length > 0" class="p-4 pr-1 flex flex-col items-center">
|
||||
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
|
||||
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
|
||||
<div class="space-y-4 flex flex-col items-center w-full">
|
||||
<NewsArticleCard
|
||||
@@ -1644,6 +1653,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.os-theme-dark,
|
||||
.os-theme-light {
|
||||
--os-handle-bg: var(--color-scrollbar) !important;
|
||||
--os-handle-bg-hover: var(--color-scrollbar) !important;
|
||||
--os-handle-bg-active: var(--color-scrollbar) !important;
|
||||
}
|
||||
|
||||
.mac {
|
||||
.app-grid-statusbar {
|
||||
padding-left: 5rem;
|
||||
|
||||
@@ -288,7 +288,7 @@ const messages = defineMessages({
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 ml-2 mr-1">
|
||||
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 -ml-1">
|
||||
<template v-if="sortedFriends.length > 0">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
@@ -309,7 +309,7 @@ const messages = defineMessages({
|
||||
@keyup.esc="search = ''"
|
||||
/>
|
||||
</template>
|
||||
<h3 v-else class="ml-2 w-full text-base text-primary font-medium m-0">
|
||||
<h3 v-else class="w-full text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.friends) }}
|
||||
</h3>
|
||||
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
|
||||
@@ -331,11 +331,11 @@ const messages = defineMessages({
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 v-if="loading" class="ml-4 mr-1 text-base text-primary font-medium m-0">
|
||||
<h3 v-if="loading" class="text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.friends) }}
|
||||
</h3>
|
||||
<template v-if="loading">
|
||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse ml-4 mr-1">
|
||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
||||
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
||||
@@ -344,7 +344,7 @@ const messages = defineMessages({
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="sortedFriends.length === 0">
|
||||
<div class="text-sm ml-4 mr-1">
|
||||
<div class="text-sm">
|
||||
<div v-if="!userCredentials">
|
||||
<IntlFormatted :message-id="messages.signInToAddFriends">
|
||||
<template #link="{ children }">
|
||||
|
||||
@@ -106,7 +106,7 @@ const messages = defineMessages({
|
||||
:open-by-default="openByDefault"
|
||||
:force-open="isSearching"
|
||||
:button-class="
|
||||
'pl-4 pr-3 flex w-full items-center bg-transparent border-0 p-0' +
|
||||
'flex w-full items-center bg-transparent border-0 p-0' +
|
||||
(isSearching
|
||||
? ''
|
||||
: ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all')
|
||||
@@ -122,7 +122,7 @@ const messages = defineMessages({
|
||||
<div
|
||||
v-for="friend in friends"
|
||||
:key="friend.username"
|
||||
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full ml-4 mr-1"
|
||||
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
|
||||
@contextmenu.prevent.stop="
|
||||
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
|
||||
"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { OverlayScrollbars, type PartialOptions } from 'overlayscrollbars'
|
||||
import type { ObjectDirective } from 'vue'
|
||||
|
||||
const defaultOverlayScrollbarsOptions = Object.freeze<PartialOptions>({
|
||||
scrollbars: {
|
||||
theme: 'os-theme-dark',
|
||||
autoHide: 'leave',
|
||||
autoHideSuspend: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mergeOptions = (options: PartialOptions = {}): PartialOptions => ({
|
||||
...defaultOverlayScrollbarsOptions,
|
||||
...options,
|
||||
scrollbars: {
|
||||
...defaultOverlayScrollbarsOptions.scrollbars,
|
||||
...(options.scrollbars ?? {}),
|
||||
},
|
||||
})
|
||||
|
||||
export const overlayScrollbarsDirective: ObjectDirective<HTMLElement, PartialOptions | undefined> = {
|
||||
mounted(el, binding) {
|
||||
OverlayScrollbars(el, mergeOptions(binding.value))
|
||||
},
|
||||
updated(el, binding) {
|
||||
if (binding.value === binding.oldValue) return
|
||||
const instance = OverlayScrollbars(el)
|
||||
instance?.options(mergeOptions(binding.value))
|
||||
},
|
||||
unmounted(el) {
|
||||
const instance = OverlayScrollbars(el)
|
||||
instance?.destroy()
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'floating-vue/dist/style.css'
|
||||
import 'overlayscrollbars/overlayscrollbars.css'
|
||||
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
||||
@@ -8,6 +9,7 @@ import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from '@/App.vue'
|
||||
import { overlayScrollbarsDirective } from '@/directives/overlayScrollbars'
|
||||
import i18nPlugin from '@/plugins/i18n'
|
||||
import i18nDebugPlugin from '@/plugins/i18n-debug'
|
||||
import router from '@/routes'
|
||||
@@ -50,5 +52,6 @@ app.use(FloatingVue, {
|
||||
})
|
||||
app.use(i18nPlugin)
|
||||
app.use(i18nDebugPlugin)
|
||||
app.directive('overlay-scrollbars', overlayScrollbarsDirective)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -6,6 +6,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
||||
worlds_tab: false,
|
||||
worlds_in_home: true,
|
||||
server_project_qa: false,
|
||||
server_ram_as_bytes_always_on: false,
|
||||
i18n_debug: false,
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
showProjectPageQuickServerButton: false,
|
||||
newProjectGeneralSettings: false,
|
||||
newProjectEnvironmentSettings: true,
|
||||
serverRamAsBytesAlwaysOn: false,
|
||||
archonSentryCapture: false,
|
||||
hideRussiaCensorshipBanner: false,
|
||||
disablePrettyProjectUrlRedirects: false,
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
<div class="flex flex-col justify-between gap-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<ModrinthServersIcon class="flex h-8 w-fit" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-6">
|
||||
<ServerListing
|
||||
v-if="subscription.serverInfo"
|
||||
v-bind="subscription.serverInfo"
|
||||
@@ -311,15 +311,16 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="m-0 mt-4 text-xl font-semibold leading-none text-contrast">
|
||||
{{
|
||||
formatMessage(messages.planTitle, {
|
||||
size: getProductSize(getPyroProduct(subscription)),
|
||||
})
|
||||
}}
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="m-0 mb-1 text-xl font-semibold leading-none">
|
||||
{{
|
||||
formatMessage(messages.planTitle, {
|
||||
size: getProductSize(getPyroProduct(subscription)),
|
||||
})
|
||||
}}
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<CheckCircleIcon class="h-5 w-5 text-brand" />
|
||||
<span>
|
||||
@@ -370,8 +371,8 @@
|
||||
</div>
|
||||
<div class="flex flex-col items-end justify-between">
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<div class="flex text-2xl font-bold text-contrast">
|
||||
<span class="text-contrast">
|
||||
<h3 class="m-0 flex text-lg font-semibold text-contrast">
|
||||
<span class="leading-none text-contrast">
|
||||
{{
|
||||
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
||||
? formatPrice(
|
||||
@@ -383,14 +384,14 @@
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
<span class="leading-none">
|
||||
{{
|
||||
formatMessage(messages.slashInterval, {
|
||||
interval: getIntervalNounLabel(subscription.interval),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</h3>
|
||||
<div
|
||||
v-if="
|
||||
getPyroCharge(subscription) &&
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { provideModalBehavior, providePageContext } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useFeatureFlags } from '~/composables/featureFlags.ts'
|
||||
|
||||
export function setupPageContextProvider() {
|
||||
const cosmetics = useCosmetics()
|
||||
const featureFlags = useFeatureFlags()
|
||||
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(false),
|
||||
showAds: ref(false),
|
||||
featureFlags: {
|
||||
serverRamAsBytesAlwaysOn: computed(() => featureFlags.value.serverRamAsBytesAlwaysOn),
|
||||
},
|
||||
openExternalUrl: (url) => window.open(url, '_blank'),
|
||||
})
|
||||
provideModalBehavior({
|
||||
|
||||
@@ -55,6 +55,7 @@ pub enum FeatureFlag {
|
||||
ProjectBackground,
|
||||
WorldsTab,
|
||||
WorldsInHome,
|
||||
ServerRamAsBytesAlwaysOn,
|
||||
ServersInApp,
|
||||
ServerProjectQa,
|
||||
I18nDebug,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</ButtonStyled>
|
||||
|
||||
<template v-else>
|
||||
<ButtonStyled v-if="showStopButton" type="transparent" size="large">
|
||||
<ButtonStyled v-if="showStopButton" type="standard" color="red" size="large">
|
||||
<button
|
||||
v-tooltip="busyTooltip"
|
||||
:disabled="!canTakeAction"
|
||||
@@ -21,9 +21,38 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled type="standard" color="brand" size="large">
|
||||
<div v-if="showRestartDropdown" class="joined-buttons">
|
||||
<ButtonStyled type="standard" color="orange" size="large">
|
||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||
<UpdatedIcon />
|
||||
<span>{{ primaryActionText }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="standard" color="orange" size="large">
|
||||
<OverflowMenu
|
||||
v-tooltip="busyTooltip"
|
||||
:disabled="!canTakeAction"
|
||||
:options="[
|
||||
{
|
||||
id: 'kill_server',
|
||||
action: () => initiateAction('Kill'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="w-0 text-xl relative top-0.5 right-2.5">
|
||||
<DropdownIcon />
|
||||
</div>
|
||||
|
||||
<template #kill_server>
|
||||
<SlashIcon class="h-5 w-5" />
|
||||
Kill server
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled v-else type="standard" color="brand" size="large">
|
||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||
<component :is="isRunning || showStopButton ? UpdatedIcon : PlayIcon" />
|
||||
<PlayIcon />
|
||||
<span>{{ primaryActionText }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -33,10 +62,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LoaderCircleIcon, PlayIcon, StopCircleIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import {
|
||||
DropdownIcon,
|
||||
LoaderCircleIcon,
|
||||
PlayIcon,
|
||||
SlashIcon,
|
||||
StopCircleIcon,
|
||||
UpdatedIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { ButtonStyled } from '#ui/components'
|
||||
import { ButtonStyled, OverflowMenu } from '#ui/components'
|
||||
|
||||
import { useServerPowerAction } from './use-server-power-action'
|
||||
|
||||
@@ -51,7 +87,6 @@ const props = withDefaults(
|
||||
|
||||
const {
|
||||
isInstalling,
|
||||
isRunning,
|
||||
showStopButton,
|
||||
busyTooltip,
|
||||
canTakeAction,
|
||||
@@ -61,4 +96,32 @@ const {
|
||||
} = useServerPowerAction({
|
||||
disabled: computed(() => props.disabled),
|
||||
})
|
||||
|
||||
const showRestartDropdown = computed(() => primaryActionText.value === 'Restart')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.joined-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn:first-child) {
|
||||
border-top-left-radius: var(--radius-md);
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn:last-child) {
|
||||
border-top-right-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.joined-buttons > :deep(.btn:not(:last-child)) {
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
<ButtonStyled circular type="transparent" size="large">
|
||||
<TeleportOverflowMenu :options="menuOptions">
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #kill>
|
||||
<SlashIcon class="h-5 w-5" />
|
||||
<span>Kill server</span>
|
||||
</template>
|
||||
<template #allServers>
|
||||
<ServerIcon class="h-5 w-5" />
|
||||
<span>All servers</span>
|
||||
@@ -21,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ClipboardCopyIcon, MoreVerticalIcon, ServerIcon, SlashIcon } from '@modrinth/assets'
|
||||
import { ClipboardCopyIcon, MoreVerticalIcon, ServerIcon } from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -29,8 +25,6 @@ import { ButtonStyled } from '#ui/components'
|
||||
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
|
||||
import { useServerPowerAction } from './use-server-power-action'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -49,21 +43,7 @@ const props = withDefaults(
|
||||
const router = useRouter()
|
||||
const { serverId } = injectModrinthServerContext()
|
||||
|
||||
const { isInstalling, initiateAction } = useServerPowerAction({
|
||||
disabled: computed(() => props.disabled),
|
||||
})
|
||||
|
||||
const menuOptions = computed(() => [
|
||||
...(isInstalling.value
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: 'kill',
|
||||
label: 'Kill server',
|
||||
icon: SlashIcon,
|
||||
action: () => initiateAction('Kill'),
|
||||
},
|
||||
]),
|
||||
{
|
||||
id: 'allServers',
|
||||
label: 'All servers',
|
||||
|
||||
@@ -89,26 +89,36 @@
|
||||
</div>
|
||||
<span>{{ prefConfig.description }}</span>
|
||||
</label>
|
||||
<Toggle
|
||||
:id="`pref-${key}`"
|
||||
v-model="newUserPreferences[key]"
|
||||
class="flex-none"
|
||||
:disabled="!prefConfig.implemented"
|
||||
/>
|
||||
<div v-tooltip="getPreferenceTooltip(key)">
|
||||
<Toggle
|
||||
:id="`pref-${key}`"
|
||||
:model-value="getPreferenceValue(key)"
|
||||
class="flex-none"
|
||||
:disabled="!prefConfig.implemented || isPreferenceForcedByFeatureFlag(key)"
|
||||
@update:model-value="(value) => setPreferenceValue(key, !!value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex flex-col gap-2.5 pb-10">
|
||||
<div class="text-lg m-0 font-semibold text-contrast">Info</div>
|
||||
<div class="flex flex-col gap-2.5 rounded-xl bg-surface-2 p-4">
|
||||
<div
|
||||
v-for="property in infoProperties"
|
||||
:key="property.name"
|
||||
class="flex items-center justify-between gap-4"
|
||||
class="flex items-start justify-between gap-4"
|
||||
>
|
||||
<template v-if="property.value !== 'Unknown'">
|
||||
<span>{{ property.name }}</span>
|
||||
<CopyCode :text="property.value" />
|
||||
<span class="mt-1">{{ property.name }}</span>
|
||||
<CopyCode v-if="property.type === 'copy'" :text="property.value" />
|
||||
<div
|
||||
v-else-if="property.type === 'specs'"
|
||||
class="flex flex-col items-end text-right text-sm leading-5 break-words"
|
||||
>
|
||||
<span v-for="line in property.lines" :key="line">{{ line }}</span>
|
||||
</div>
|
||||
<span v-else class="text-right text-sm break-words">{{ property.value }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,7 +137,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
@@ -138,11 +149,13 @@ import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
} from '#ui/providers'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
const { server: data, serverId, busyReasons } = injectModrinthServerContext()
|
||||
const { featureFlags } = injectPageContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const serverName = ref(data.value?.name)
|
||||
@@ -207,11 +220,118 @@ const userPreferences = useStorage<UserPreferences>(
|
||||
|
||||
const newUserPreferences = ref<UserPreferences>(JSON.parse(JSON.stringify(userPreferences.value)))
|
||||
|
||||
const isRamAsBytesForcedByFeatureFlag = computed(
|
||||
() => featureFlags?.serverRamAsBytesAlwaysOn?.value ?? false,
|
||||
)
|
||||
|
||||
const isPreferenceForcedByFeatureFlag = (key: string) =>
|
||||
key === 'ramAsNumber' && isRamAsBytesForcedByFeatureFlag.value
|
||||
|
||||
const getPreferenceTooltip = (key: string) =>
|
||||
isPreferenceForcedByFeatureFlag(key)
|
||||
? 'Feature flag enabled to always show RAM as bytes.'
|
||||
: undefined
|
||||
|
||||
const getPreferenceValue = (key: string) =>
|
||||
isPreferenceForcedByFeatureFlag(key) ? true : newUserPreferences.value[key as PreferenceKeys]
|
||||
|
||||
const setPreferenceValue = (key: string, value: boolean) => {
|
||||
if (isPreferenceForcedByFeatureFlag(key)) {
|
||||
return
|
||||
}
|
||||
newUserPreferences.value[key as PreferenceKeys] = value
|
||||
}
|
||||
|
||||
const { data: subscriptions } = useQuery({
|
||||
queryKey: ['billing', 'subscriptions'],
|
||||
queryFn: () => client.labrinth.billing_internal.getSubscriptions(),
|
||||
})
|
||||
|
||||
const { data: products } = useQuery({
|
||||
queryKey: ['billing', 'products'],
|
||||
queryFn: () => client.labrinth.billing_internal.getProducts(),
|
||||
})
|
||||
|
||||
const serverSubscription = computed(() =>
|
||||
subscriptions.value?.find(
|
||||
(subscription) =>
|
||||
subscription.metadata?.type === 'pyro' && subscription.metadata.id === serverId,
|
||||
),
|
||||
)
|
||||
|
||||
const serverProduct = computed(() =>
|
||||
products.value?.find((product) =>
|
||||
product.prices.some((price) => price.id === serverSubscription.value?.price_id),
|
||||
),
|
||||
)
|
||||
|
||||
const formatSpecNumber = (value: number) =>
|
||||
Number.isInteger(value) ? String(value) : value.toFixed(1)
|
||||
|
||||
const getServerSpecs = (product?: Labrinth.Billing.Internal.Product | null) => {
|
||||
const metadata = product?.metadata
|
||||
if (!metadata || (metadata.type !== 'pyro' && metadata.type !== 'medal')) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sharedCpus = formatSpecNumber(metadata.cpu / 2)
|
||||
const burstCpus = formatSpecNumber(metadata.cpu)
|
||||
const ramGb = formatSpecNumber(metadata.ram / 1024)
|
||||
const swapGb = formatSpecNumber(metadata.swap / 1024)
|
||||
const storageGb = formatSpecNumber(metadata.storage / 1024)
|
||||
|
||||
return {
|
||||
sharedCpus,
|
||||
burstCpus,
|
||||
ramGb,
|
||||
swapGb,
|
||||
storageGb,
|
||||
}
|
||||
}
|
||||
|
||||
const serverHostname = computed(() =>
|
||||
serverSubdomain.value ? `${serverSubdomain.value}.modrinth.gg` : 'Unknown',
|
||||
)
|
||||
|
||||
const serverSpecs = computed(() => getServerSpecs(serverProduct.value))
|
||||
|
||||
type InfoProperty =
|
||||
| {
|
||||
name: string
|
||||
value: string
|
||||
type: 'copy'
|
||||
}
|
||||
| {
|
||||
name: string
|
||||
value: string
|
||||
type: 'text'
|
||||
}
|
||||
| {
|
||||
name: string
|
||||
value: string
|
||||
type: 'specs'
|
||||
lines: string[]
|
||||
}
|
||||
|
||||
// Info properties
|
||||
const infoProperties = [
|
||||
{ name: 'Server ID', value: serverId ?? 'Unknown' },
|
||||
{ name: 'Node', value: data.value?.node?.instance ?? 'Unknown' },
|
||||
]
|
||||
const infoProperties = computed<InfoProperty[]>(() => [
|
||||
{ name: 'Server ID', value: serverId ?? 'Unknown', type: 'copy' },
|
||||
{ name: 'Node', value: data.value?.node?.instance ?? 'Unknown', type: 'copy' },
|
||||
{ name: 'Hostname', value: serverHostname.value, type: 'copy' },
|
||||
{
|
||||
name: 'Server specs',
|
||||
value: serverSpecs.value ? 'Available' : 'Unknown',
|
||||
type: 'specs',
|
||||
lines: serverSpecs.value
|
||||
? [
|
||||
`${serverSpecs.value.sharedCpus} Shared CPU${Number(serverSpecs.value.sharedCpus) > 1 ? 's' : ''} (Bursts up to ${serverSpecs.value.burstCpus} CPUs)`,
|
||||
`${serverSpecs.value.ramGb} GB RAM`,
|
||||
`${serverSpecs.value.swapGb} GB Swap`,
|
||||
`${serverSpecs.value.storageGb} GB SSD`,
|
||||
]
|
||||
: [],
|
||||
},
|
||||
])
|
||||
|
||||
// Unsaved changes tracking (API fields + preferences)
|
||||
const hasUnsavedChanges = computed(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
data-pyro-server-stats
|
||||
style="font-variant-numeric: tabular-nums"
|
||||
class="flex select-none flex-col items-center gap-4 md:flex-row"
|
||||
class="flex select-none flex-col items-center gap-3 md:flex-row"
|
||||
:class="{ 'pointer-events-none': loading }"
|
||||
:aria-hidden="loading"
|
||||
>
|
||||
@@ -60,11 +60,12 @@ import { useStorage } from '@vueuse/core'
|
||||
import { computed, defineAsyncComponent, ref, shallowRef, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
import { injectModrinthServerContext, injectPageContext } from '#ui/providers'
|
||||
|
||||
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||
|
||||
const { serverId } = injectModrinthServerContext()
|
||||
const { featureFlags } = injectPageContext()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -83,6 +84,9 @@ const chartsReady = ref(new Set<number>())
|
||||
const userPreferences = useStorage(`pyro-server-${serverId || 'unknown'}-preferences`, {
|
||||
ramAsNumber: false,
|
||||
})
|
||||
const isRamAsBytesForcedByFeatureFlag = computed(
|
||||
() => featureFlags?.serverRamAsBytesAlwaysOn?.value ?? false,
|
||||
)
|
||||
|
||||
const stats = shallowRef(
|
||||
props.data?.current || {
|
||||
@@ -214,7 +218,9 @@ const metrics = computed(() => {
|
||||
{
|
||||
title: 'Memory',
|
||||
value:
|
||||
props.showMemoryAsBytes || userPreferences.value.ramAsNumber
|
||||
props.showMemoryAsBytes ||
|
||||
isRamAsBytesForcedByFeatureFlag.value ||
|
||||
userPreferences.value.ramAsNumber
|
||||
? formatBytes(stats.value.ram_usage_bytes ?? 0)
|
||||
: `${ramPercent.value.toFixed(2)}%`,
|
||||
icon: DatabaseIcon,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="relative flex select-none flex-col gap-6" data-pyro-server-manager-root>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<ServerManageStats
|
||||
:data="!isWsAuthIncorrect ? stats : undefined"
|
||||
:loading="isWsAuthIncorrect"
|
||||
/>
|
||||
|
||||
<div class="flex min-h-[700px] flex-col gap-4">
|
||||
<div class="flex min-h-[700px] flex-col gap-2">
|
||||
<span class="text-2xl font-semibold text-contrast">Console</span>
|
||||
|
||||
<ConsolePageLayout />
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<div
|
||||
v-else-if="serverData"
|
||||
data-pyro-server-manager-root
|
||||
class="experimental-styles-within relative mx-auto pb-12 box-border flex min-h-[calc(100svh-100px)] w-full min-w-0 flex-col gap-6 px-6 transition-all duration-300"
|
||||
class="experimental-styles-within relative mx-auto pb-12 box-border flex min-h-[calc(100svh-100px)] w-full min-w-0 flex-col gap-4 px-6 transition-all duration-300"
|
||||
:style="{
|
||||
'--server-bg-image': serverImage
|
||||
? `url(${serverImage})`
|
||||
|
||||
@@ -6,6 +6,9 @@ export interface PageContext {
|
||||
// pages may render sidebar content in #sidebar-teleport-target instead of in the main layout when true
|
||||
hierarchicalSidebarAvailable: Ref<boolean>
|
||||
showAds: Ref<boolean>
|
||||
featureFlags?: {
|
||||
serverRamAsBytesAlwaysOn?: Ref<boolean>
|
||||
}
|
||||
openExternalUrl: (url: string) => void
|
||||
}
|
||||
|
||||
|
||||
Generated
+8
@@ -137,6 +137,9 @@ importers:
|
||||
ofetch:
|
||||
specifier: ^1.3.4
|
||||
version: 1.5.1
|
||||
overlayscrollbars:
|
||||
specifier: ^2.15.1
|
||||
version: 2.15.1
|
||||
pinia:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
||||
@@ -7604,6 +7607,9 @@ packages:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
overlayscrollbars@2.15.1:
|
||||
resolution: {integrity: sha512-glX26JwjL+Tkzv0JNOWdW4VozP5dGXO+Wx8+TPrdTEJTSYT/8eJS8yXM+fewjU0nFq/JeCa+X+BqABNjC4YZSA==}
|
||||
|
||||
oxc-minify@0.110.0:
|
||||
resolution: {integrity: sha512-KWGTzPo83QmGrXC4ml83PM9HDwUPtZFfasiclUvTV4i3/0j7xRRqINVkrL77CbQnoWura3CMxkRofjQKVDuhBw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -17717,6 +17723,8 @@ snapshots:
|
||||
type-check: 0.4.0
|
||||
word-wrap: 1.2.5
|
||||
|
||||
overlayscrollbars@2.15.1: {}
|
||||
|
||||
oxc-minify@0.110.0:
|
||||
optionalDependencies:
|
||||
'@oxc-minify/binding-android-arm-eabi': 0.110.0
|
||||
|
||||
Reference in New Issue
Block a user