feat: modrinth hosting - files tab refactor (#4912)

* feat: api-client module for content v0

* feat: delete unused components + modules + setting

* feat: xhr uploading

* feat: fs module -> api-client

* feat: migrate files.vue to use tanstack

* fix: mem leak + other issues

* fix: build

* feat: switch to monaco

* fix: go back to using ace, but improve preloading + theme

* fix: styling + dead attrs

* feat: match figma

* fix: padding

* feat: files-new for ui page structure

* feat: finalize files.vue

* fix: lint

* fix: qa

* fix: dep

* fix: lint

* fix: lockfile merge

* feat: icons on navtab

* fix: surface alternating on table

* fix: hover surface color

---------

Signed-off-by: Calum H. <contact@cal.engineer>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-01-06 00:35:51 +00:00
committed by GitHub
parent 61d4a34f0f
commit 099011a177
89 changed files with 5863 additions and 2091 deletions

View File

@@ -100,13 +100,11 @@
</div>
</div>
<FilesUploadDropdown
v-if="props.server.fs"
ref="uploadDropdownRef"
class="rounded-xl bg-bg-raised"
:margin-bottom="16"
:file-type="type"
:current-path="`/${type.toLocaleLowerCase()}s`"
:fs="props.server.fs"
:accepted-types="acceptFileFromProjectType(type.toLocaleLowerCase()).split(',')"
@upload-complete="() => props.server.refresh(['content'])"
/>
@@ -355,7 +353,7 @@ import {
TrashIcon,
WrenchIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { Avatar, ButtonStyled, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import type { Mod } from '@modrinth/utils'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
@@ -369,6 +367,8 @@ import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const props = defineProps<{
server: ModrinthServer
}>()
@@ -621,7 +621,7 @@ async function toggleMod(mod: ContentItem) {
mod.disabled = newFilename.endsWith('.disabled')
mod.filename = newFilename
await props.server.fs?.moveFileOrFolder(sourcePath, destinationPath)
await client.kyros.files_v0.moveFileOrFolder(sourcePath, destinationPath)
await props.server.refresh(['general', 'content'])
} catch (error) {

File diff suppressed because it is too large Load Diff

View File

@@ -205,6 +205,9 @@ type ServerProps = {
const props = defineProps<ServerProps>()
const client = injectModrinthClient()
const serverId = props.server.serverId
interface ErrorData {
id: string
name: string
@@ -242,7 +245,8 @@ const inspectingError = ref<ErrorData | null>(null)
const inspectError = async () => {
try {
const log = await props.server.fs?.downloadFile('logs/latest.log')
const blob = await client.kyros.files_v0.downloadFile('/logs/latest.log')
const log = await blob.text()
if (!log) return
// @ts-ignore
@@ -287,9 +291,6 @@ if (props.serverPowerState === 'crashed' && !props.powerStateDetails?.oom_killed
inspectError()
}
const client = injectModrinthClient()
const serverId = props.server.serverId
const DYNAMIC_ARG = Symbol('DYNAMIC_ARG')
const commandTree: any = {

View File

@@ -116,13 +116,15 @@
<script setup lang="ts">
import { EditIcon, TransferIcon } from '@modrinth/assets'
import { injectNotificationManager, ServerIcon } from '@modrinth/ui'
import { injectModrinthClient, injectNotificationManager, ServerIcon } from '@modrinth/ui'
import ButtonStyled from '@modrinth/ui/src/components/base/ButtonStyled.vue'
import SaveBanner from '~/components/ui/servers/SaveBanner.vue'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const props = defineProps<{
server: ModrinthServer
}>()
@@ -242,12 +244,12 @@ const uploadFile = async (e: Event) => {
try {
if (data.value?.image) {
await props.server.fs?.deleteFileOrFolder('/server-icon.png', false)
await props.server.fs?.deleteFileOrFolder('/server-icon-original.png', false)
await client.kyros.files_v0.deleteFileOrFolder('/server-icon.png', false)
await client.kyros.files_v0.deleteFileOrFolder('/server-icon-original.png', false)
}
await props.server.fs?.uploadFile('/server-icon.png', scaledFile)
await props.server.fs?.uploadFile('/server-icon-original.png', file)
await client.kyros.files_v0.uploadFile('/server-icon.png', scaledFile).promise
await client.kyros.files_v0.uploadFile('/server-icon-original.png', file).promise
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
@@ -284,8 +286,8 @@ const uploadFile = async (e: Event) => {
const resetIcon = async () => {
if (data.value?.image) {
try {
await props.server.fs?.deleteFileOrFolder('/server-icon.png', false)
await props.server.fs?.deleteFileOrFolder('/server-icon-original.png', false)
await client.kyros.files_v0.deleteFileOrFolder('/server-icon.png', false)
await client.kyros.files_v0.deleteFileOrFolder('/server-icon-original.png', false)
useState(`server-icon-${props.server.serverId}`).value = undefined
if (data.value) data.value.image = undefined

View File

@@ -78,11 +78,6 @@ const preferences = {
description: 'When enabled, you will be prompted before stopping and restarting your server.',
implemented: true,
},
backupWhileRunning: {
displayName: 'Create backups while running',
description: 'When enabled, backups will be created even if the server is running.',
implemented: true,
},
} as const
type PreferenceKeys = keyof typeof preferences
@@ -96,7 +91,6 @@ const defaultPreferences: UserPreferences = {
hideSubdomainLabel: false,
autoRestart: false,
powerDontAskAgain: false,
backupWhileRunning: false,
}
const userPreferences = useStorage<UserPreferences>(

View File

@@ -1,32 +1,7 @@
<template>
<div class="relative h-full w-full select-none overflow-y-auto">
<div
v-if="server.moduleErrors.fs"
class="flex w-full flex-col items-center justify-center gap-4 p-4"
>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
<IssuesIcon class="size-12 text-orange" />
</div>
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load properties</h1>
</div>
<p class="text-lg text-secondary">
We couldn't access your server's properties. Here's what we know:
<span class="break-all font-mono">{{
JSON.stringify(server.moduleErrors.fs.error)
}}</span>
</p>
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
<button class="mt-6 !w-full">Retry</button>
</ButtonStyled>
</div>
</div>
</div>
<div
v-else-if="propsData && status === 'success'"
v-if="propsData && status === 'success'"
class="flex h-full w-full flex-col justify-between gap-6 overflow-y-auto"
>
<div class="card flex flex-col gap-4">
@@ -158,8 +133,8 @@
</template>
<script setup lang="ts">
import { EyeIcon, IssuesIcon, SearchIcon } from '@modrinth/assets'
import { ButtonStyled, Combobox, injectNotificationManager } from '@modrinth/ui'
import { EyeIcon, SearchIcon } from '@modrinth/assets'
import { Combobox, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import Fuse from 'fuse.js'
import { computed, inject, ref, watch } from 'vue'
@@ -167,6 +142,8 @@ import SaveBanner from '~/components/ui/servers/SaveBanner.vue'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const props = defineProps<{
server: ModrinthServer
}>()
@@ -181,33 +158,39 @@ const data = computed(() => props.server.general)
const modulesLoaded = inject<Promise<void>>('modulesLoaded')
const { data: propsData, status } = await useAsyncData('ServerProperties', async () => {
await modulesLoaded
const rawProps = await props.server.fs?.downloadFile('server.properties')
if (!rawProps) return null
try {
const blob = await client.kyros.files_v0.downloadFile('/server.properties')
const rawProps = await blob.text()
if (!rawProps) return null
const properties: Record<string, any> = {}
const lines = rawProps.split('\n')
const properties: Record<string, any> = {}
const lines = rawProps.split('\n')
for (const line of lines) {
if (line.startsWith('#') || !line.includes('=')) continue
const [key, ...valueParts] = line.split('=')
let value = valueParts.join('=')
for (const line of lines) {
if (line.startsWith('#') || !line.includes('=')) continue
const [key, ...valueParts] = line.split('=')
const rawValue = valueParts.join('=')
let value: string | boolean | number = rawValue
if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
value = value.toLowerCase() === 'true'
} else {
const intLike = /^[-+]?\d+$/.test(value)
if (intLike) {
const n = Number(value)
if (Number.isSafeInteger(n)) {
value = n
if (rawValue.toLowerCase() === 'true' || rawValue.toLowerCase() === 'false') {
value = rawValue.toLowerCase() === 'true'
} else {
const intLike = /^[-+]?\d+$/.test(rawValue)
if (intLike) {
const n = Number(rawValue)
if (Number.isSafeInteger(n)) {
value = n
}
}
}
properties[key.trim()] = value
}
properties[key.trim()] = value
return properties
} catch {
return null
}
return properties
})
const liveProperties = ref<Record<string, any>>({})
@@ -302,7 +285,7 @@ const constructServerProperties = (): string => {
const saveProperties = async () => {
try {
isUpdating.value = true
await props.server.fs?.updateFile('server.properties', constructServerProperties())
await client.kyros.files_v0.updateFile('/server.properties', constructServerProperties())
await new Promise((resolve) => setTimeout(resolve, 500))
originalProperties.value = JSON.parse(JSON.stringify(liveProperties.value))
await props.server.refresh()