diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 041209b551..54d9320c26 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -5,6 +5,7 @@ import { nodeAuthState, PanelVersionFeature, TauriModrinthClient, + VerboseLoggingFeature, } from '@modrinth/api-client' import { ArrowBigUpDashIcon, @@ -146,6 +147,7 @@ const tauriApiClient = new TauriModrinthClient({ token: async () => (await getCreds())?.session, }), new PanelVersionFeature(), + new VerboseLoggingFeature(), ], }) provideModrinthClient(tauriApiClient) @@ -420,6 +422,7 @@ const route = useRoute() const loading = useLoading() loading.setEnabled(false) +loading.startLoading() const error = useError() const errorModal = ref() @@ -1023,6 +1026,8 @@ provideAppUpdateDownloadProgress(appUpdateDownload) v-if="themeStore.featureFlags.servers_in_app" v-tooltip.right="'Servers'" to="/hosting/manage" + :is-primary="(r) => r.path === '/hosting/manage' || r.path === '/hosting/manage/'" + :is-subpage="(r) => r.path.startsWith('/hosting/manage/') && r.path !== '/hosting/manage/'" > @@ -1195,7 +1200,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
- {{ - installing - ? 'Installing' - : installed - ? 'Installed' - : modpack || instance - ? 'Install' - : 'Add to an instance' - }} + {{ installActionLabel }} @@ -109,14 +101,44 @@ const props = defineProps({ type: String, default: null, }, + customInstall: { + type: Function, + default: null, + }, }) const emit = defineEmits(['open', 'install']) const installing = ref(false) +const installActionLabel = computed(() => + installing.value + ? 'Installing' + : props.installed + ? 'Installed' + : props.customInstall || modpack.value || props.instance + ? 'Install' + : 'Add to an instance', +) +const shouldUseInstallIcon = computed( + () => !!props.customInstall || !!modpack.value || !!props.instance, +) async function install() { installing.value = true + if (props.customInstall) { + try { + const didInstall = await props.customInstall(props.project) + if (didInstall !== false) { + emit('install', props.project.project_id ?? props.project.id) + } + } catch (err) { + handleError(err) + } finally { + installing.value = false + } + return + } + await installVersion( props.project.project_id ?? props.project.id, null, diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index d93726e438..f7f558ae65 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -185,9 +185,6 @@ "app.modal.update-to-play.update-required-description": { "message": "An update is required to play {name}. Please update to the latest version to launch the game." }, - "app.server-settings.failed-to-load-server": { - "message": "Failed to load server settings" - }, "app.settings.developer-mode-enabled": { "message": "Developer mode enabled." }, @@ -601,11 +598,5 @@ }, "search.filter.locked.server-loader.title": { "message": "Loader is provided by the server" - }, - "servers.busy.installing": { - "message": "Server is installing" - }, - "servers.busy.syncing-content": { - "message": "Content sync in progress" } } diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue index f7ec4dcef4..386bac5159 100644 --- a/apps/app-frontend/src/pages/Browse.vue +++ b/apps/app-frontend/src/pages/Browse.vue @@ -5,6 +5,7 @@ import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, + LeftArrowIcon, PlayIcon, PlusIcon, SearchIcon, @@ -16,6 +17,7 @@ import { ButtonStyled, Checkbox, commonMessages, + CreationFlowModal, defineMessages, DropdownSelect, injectNotificationManager, @@ -39,7 +41,6 @@ import type { LocationQuery } from 'vue-router' import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router' import ContextMenu from '@/components/ui/ContextMenu.vue' -import type Instance from '@/components/ui/Instance.vue' import InstanceIndicator from '@/components/ui/InstanceIndicator.vue' import SearchCard from '@/components/ui/SearchCard.vue' import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js' @@ -55,6 +56,10 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags' import type { GameInstance } from '@/helpers/types' import { add_server_to_profile, get_profile_worlds, getServerLatency } from '@/helpers/worlds' import { injectServerInstall } from '@/providers/server-install' +import { + createServerInstallContent, + provideServerInstallContent, +} from '@/providers/setup/server-install-content' import { useBreadcrumbs } from '@/store/breadcrumbs' import { getServerAddress } from '@/store/install.js' @@ -66,6 +71,31 @@ const debugLog = useDebugLogger('Browse') const router = useRouter() const route = useRoute() +const serverSetupModalRef = ref | null>(null) +const serverInstallContent = createServerInstallContent({ serverSetupModalRef }) +provideServerInstallContent(serverInstallContent) +const { + serverIdQuery, + serverFlowFrom, + isFromWorlds, + isServerContext, + isSetupServerContext, + effectiveServerWorldId, + serverContextServerData, + serverContentProjectIds, + serverBackUrl, + serverBackLabel, + serverBrowseHeading, + initServerContext, + watchServerContextChanges, + searchServerModpacks, + getServerProjectVersions, + enforceSetupModpackRoute, + installProjectToServer, + onServerFlowBack, + handleServerModpackFlowCreate, + markServerProjectInstalled, +} = serverInstallContent const projectTypes = computed(() => { debugLog('projectTypes computed', route.params.projectType) @@ -110,7 +140,6 @@ const installedProjectIds: Ref = ref(null) const instanceHideInstalled = ref(false) const newlyInstalled = ref([]) const isServerInstance = ref(false) -const isFromWorlds = computed(() => route.query.from === 'worlds') if (isFromWorlds.value && route.params.projectType !== 'server') { router.replace({ @@ -119,16 +148,28 @@ if (isFromWorlds.value && route.params.projectType !== 'server') { }) } +enforceSetupModpackRoute(route.params.projectType as string | undefined) + const allInstalledIds = computed( () => new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])]), ) -const PERSISTENT_QUERY_PARAMS = ['i', 'ai', 'from'] +const PERSISTENT_QUERY_PARAMS = ['i', 'ai', 'sid', 'wid', 'from'] + +watchServerContextChanges() await initInstanceContext() async function initInstanceContext() { - debugLog('initInstanceContext', { queryI: route.query.i, queryAi: route.query.ai }) + debugLog('initInstanceContext', { + queryI: route.query.i, + queryAi: route.query.ai, + querySid: route.query.sid, + queryWid: route.query.wid, + queryFrom: route.query.from, + }) + await initServerContext() + if (route.query.i) { instance.value = (await getInstance(route.query.i as string).catch(handleError)) ?? null debugLog('instance loaded', { @@ -267,6 +308,14 @@ const activeGameVersion = computed(() => { return instance.value?.game_version ?? null }) +function onSearchResultInstalled(id: string) { + if (isServerContext.value) { + markServerProjectInstalled(id) + return + } + newlyInstalled.value.push(id) +} + const serverHits = shallowRef([]) const filteredServerHits = computed(() => { if (!instanceHideInstalled.value || allInstalledIds.value.size === 0) return serverHits.value @@ -581,11 +630,10 @@ async function refreshSearch() { page: 1, } } else { - if (instance.value) { - const allInstalledIds = new Set([ - ...newlyInstalled.value, - ...(installedProjectIds.value ?? []), - ]) + if (instance.value || isServerContext.value) { + const allInstalledIds = instance.value + ? new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])]) + : serverContentProjectIds.value rawResults.result.hits = rawResults.result.hits.map((val) => ({ ...val, @@ -625,6 +673,13 @@ async function refreshSearch() { } } + if (serverIdQuery.value) { + persistentParams.sid = serverIdQuery.value + if (effectiveServerWorldId.value) { + persistentParams.wid = effectiveServerWorldId.value + } + } + if (instanceHideInstalled.value) { persistentParams.ai = 'true' } else { @@ -673,6 +728,11 @@ function clearSearch() { watch( () => route.params.projectType as ProjectType, async (newType) => { + if (isSetupServerContext.value) { + enforceSetupModpackRoute(newType) + if (newType !== 'modpack') return + } + // Check if the newType is not the same as the current value if (!newType || newType === projectType.value) return @@ -731,10 +791,20 @@ const selectableProjectTypes = computed(() => { if (route.query.from) { params.from = route.query.from } + if (route.query.sid) { + params.sid = route.query.sid + } + if (effectiveServerWorldId.value) { + params.wid = effectiveServerWorldId.value + } const queryString = new URLSearchParams(params as Record).toString() const suffix = queryString ? `?${queryString}` : '' + if (isSetupServerContext.value) { + return [{ label: 'Modpacks', href: `/browse/modpack${suffix}` }] + } + if (isFromWorlds.value) { return [{ label: 'Servers', href: `/browse/server${suffix}` }] } @@ -897,7 +967,24 @@ previousFilterState.value = JSON.stringify({
- @@ -1128,5 +1220,19 @@ previousFilterState.value = JSON.stringify({ />
+ + diff --git a/apps/app-frontend/src/pages/hosting/manage/Index.vue b/apps/app-frontend/src/pages/hosting/manage/Index.vue index fc1015e7a0..1b9eb37ab7 100644 --- a/apps/app-frontend/src/pages/hosting/manage/Index.vue +++ b/apps/app-frontend/src/pages/hosting/manage/Index.vue @@ -1,346 +1,80 @@ diff --git a/apps/app-frontend/src/pages/hosting/manage/Overview.vue b/apps/app-frontend/src/pages/hosting/manage/Overview.vue new file mode 100644 index 0000000000..7b29977bb4 --- /dev/null +++ b/apps/app-frontend/src/pages/hosting/manage/Overview.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/app-frontend/src/pages/hosting/manage/index.js b/apps/app-frontend/src/pages/hosting/manage/index.js index 127c22afce..50052e3f9e 100644 --- a/apps/app-frontend/src/pages/hosting/manage/index.js +++ b/apps/app-frontend/src/pages/hosting/manage/index.js @@ -2,5 +2,6 @@ import Backups from './Backups.vue' import Content from './Content.vue' import Files from './Files.vue' import Index from './Index.vue' +import Overview from './Overview.vue' -export { Backups, Content, Files, Index } +export { Backups, Content, Files, Index, Overview } diff --git a/apps/app-frontend/src/pages/instance/Logs.vue b/apps/app-frontend/src/pages/instance/Logs.vue index 38fba56b0a..682f3596f2 100644 --- a/apps/app-frontend/src/pages/instance/Logs.vue +++ b/apps/app-frontend/src/pages/instance/Logs.vue @@ -1,122 +1,19 @@ - - diff --git a/apps/app-frontend/src/providers/setup/auth.ts b/apps/app-frontend/src/providers/setup/auth.ts index bf522c59a3..2de8c0acfa 100644 --- a/apps/app-frontend/src/providers/setup/auth.ts +++ b/apps/app-frontend/src/providers/setup/auth.ts @@ -1,6 +1,6 @@ import type { Labrinth } from '@modrinth/api-client' import { type AuthProvider, provideAuth } from '@modrinth/ui' -import { type Ref, ref, watchEffect } from 'vue' +import { computed, type Ref, ref, watchEffect } from 'vue' type AppCredentials = { session?: string | null @@ -13,10 +13,12 @@ export function setupAuthProvider( ) { const sessionToken = ref(null) const user = ref(null) + const isReady = computed(() => credentials.value !== undefined) const authProvider: AuthProvider = { session_token: sessionToken, user, + isReady, requestSignIn, } diff --git a/apps/app-frontend/src/providers/setup/server-install-content.ts b/apps/app-frontend/src/providers/setup/server-install-content.ts new file mode 100644 index 0000000000..35919d9d73 --- /dev/null +++ b/apps/app-frontend/src/providers/setup/server-install-content.ts @@ -0,0 +1,393 @@ +import type { Archon, Labrinth } from '@modrinth/api-client' +import { + createContext, + type CreationFlowContextValue, + injectModrinthClient, + injectNotificationManager, +} from '@modrinth/ui' +import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue' +import { useRoute, useRouter } from 'vue-router' + +type ServerFlowFrom = 'onboarding' | 'reset-server' +type ServerInstallableType = 'modpack' | 'mod' | 'plugin' | 'datapack' + +type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & { + installing?: boolean + installed?: boolean +} + +interface ServerModpackSelectionRequest { + projectId: string + versionId: string + name: string + iconUrl?: string +} + +interface ServerSetupModalHandle { + show: () => void | Promise + hide: () => void + ctx?: CreationFlowContextValue | null +} + +export interface ServerInstallContentContext { + serverIdQuery: ComputedRef + worldIdQuery: ComputedRef + browseFrom: ComputedRef + serverFlowFrom: ComputedRef + isFromWorlds: ComputedRef + isServerContext: ComputedRef + isSetupServerContext: ComputedRef + effectiveServerWorldId: ComputedRef + serverContextServerData: Ref + serverContentProjectIds: Ref> + serverBackUrl: ComputedRef + serverBackLabel: ComputedRef + serverBrowseHeading: ComputedRef + initServerContext: () => Promise + watchServerContextChanges: () => void + searchServerModpacks: ( + query: string, + limit?: number, + ) => Promise + getServerProjectVersions: (projectId: string) => Promise<{ id: string }[]> + enforceSetupModpackRoute: (currentProjectType: string | undefined) => void + installProjectToServer: (project: InstallableSearchResult) => Promise + onServerFlowBack: () => void + handleServerModpackFlowCreate: (config: CreationFlowContextValue) => Promise + markServerProjectInstalled: (id: string) => void +} + +export const [injectServerInstallContent, provideServerInstallContent] = + createContext('Browse', 'serverInstallContent') + +function readQueryString(value: unknown): string | null { + if (Array.isArray(value)) return value[0] ?? null + return typeof value === 'string' && value.length > 0 ? value : null +} + +export function createServerInstallContent(opts: { + serverSetupModalRef: Ref +}) { + const { serverSetupModalRef } = opts + const route = useRoute() + const router = useRouter() + const client = injectModrinthClient() + const { handleError } = injectNotificationManager() + + const serverIdQuery = computed(() => readQueryString(route.query.sid)) + const worldIdQuery = computed(() => readQueryString(route.query.wid)) + const browseFrom = computed(() => readQueryString(route.query.from)) + const serverFlowFrom = computed(() => + browseFrom.value === 'onboarding' || browseFrom.value === 'reset-server' + ? browseFrom.value + : null, + ) + + const isFromWorlds = computed(() => browseFrom.value === 'worlds') + const isServerContext = computed(() => !!serverIdQuery.value) + const isSetupServerContext = computed(() => !!serverIdQuery.value && !!serverFlowFrom.value) + + const serverContextWorldId = ref(worldIdQuery.value) + const serverContextServerData = ref(null) + const serverContentProjectIds = ref>(new Set()) + const effectiveServerWorldId = computed(() => worldIdQuery.value ?? serverContextWorldId.value) + + const serverBackUrl = computed(() => { + const sid = serverIdQuery.value + if (!sid) return '/hosting/manage' + if (serverFlowFrom.value === 'onboarding') { + return `/hosting/manage/${sid}?resumeModal=setup-type` + } + if (serverFlowFrom.value === 'reset-server') { + return `/hosting/manage/${sid}?openSettings=installation` + } + return `/hosting/manage/${sid}/content` + }) + const serverBackLabel = computed(() => { + if (serverFlowFrom.value === 'onboarding') return 'Back to setup' + if (serverFlowFrom.value === 'reset-server') return 'Cancel reset' + return 'Back to server' + }) + const serverBrowseHeading = computed(() => { + if (serverFlowFrom.value === 'reset-server') { + return 'Select modpack to install after reset' + } + return 'Install content to server' + }) + + async function resolveServerContextWorldId(serverId: string) { + try { + const server = await client.archon.servers_v1.get(serverId) + const activeWorld = server.worlds.find((world) => world.is_active) + return activeWorld?.id ?? server.worlds[0]?.id ?? null + } catch (err) { + handleError(err as Error) + return null + } + } + + async function refreshServerInstalledContent(serverId: string, worldId: string) { + try { + const content = await client.archon.content_v1.getAddons(serverId, worldId) + const ids = new Set( + (content.addons ?? []) + .map((addon) => addon.project_id) + .filter((projectId): projectId is string => !!projectId), + ) + serverContentProjectIds.value = ids + } catch (err) { + handleError(err as Error) + } + } + + async function initServerContext() { + const sid = serverIdQuery.value + if (!sid) return + + try { + serverContextServerData.value = await client.archon.servers_v0.get(sid) + } catch (err) { + handleError(err as Error) + } + + let resolvedWorldId = effectiveServerWorldId.value + if (!resolvedWorldId) { + resolvedWorldId = await resolveServerContextWorldId(sid) + if (resolvedWorldId) { + serverContextWorldId.value = resolvedWorldId + } + } + + if (resolvedWorldId) { + await refreshServerInstalledContent(sid, resolvedWorldId) + } + } + + function watchServerContextChanges() { + watch([serverIdQuery, effectiveServerWorldId], async ([sid, wid], [prevSid, prevWid]) => { + if (!sid) { + serverContextServerData.value = null + serverContentProjectIds.value = new Set() + return + } + + if (sid !== prevSid) { + serverContentProjectIds.value = new Set() + try { + serverContextServerData.value = await client.archon.servers_v0.get(sid) + } catch (err) { + handleError(err as Error) + } + } + + if (wid && (sid !== prevSid || wid !== prevWid)) { + await refreshServerInstalledContent(sid, wid) + } + }) + } + + function normalizeLoader(loader: string) { + return loader.toLowerCase().replaceAll('_', '').replaceAll('-', '').replaceAll(' ', '') + } + + function getCompatibleLoaders(loader: string) { + const normalized = normalizeLoader(loader) + if (!normalized) return new Set() + if (normalized === 'paper' || normalized === 'purpur' || normalized === 'spigot') { + return new Set(['paper', 'purpur', 'spigot', 'bukkit']) + } + if (normalized === 'neoforge' || normalized === 'neo') { + return new Set(['neoforge', 'neo']) + } + return new Set([normalized]) + } + + function enforceSetupModpackRoute(currentProjectType: string | undefined) { + if (!isSetupServerContext.value || currentProjectType === 'modpack') return + router.replace({ + path: '/browse/modpack', + query: route.query, + }) + } + + async function searchServerModpacks(query: string, limit: number = 10) { + return client.labrinth.projects_v2.search({ + query: query || undefined, + new_filters: + 'project_types = "modpack" AND (client_side = "optional" OR client_side = "required") AND server_side = "required"', + limit, + }) + } + + async function getServerProjectVersions(projectId: string) { + const versions = await client.labrinth.versions_v3.getProjectVersions(projectId) + return versions.map((version) => ({ id: version.id })) + } + + async function openServerModpackInstallFlow(request: ServerModpackSelectionRequest) { + if (!serverIdQuery.value || !effectiveServerWorldId.value) { + throw new Error('Missing server context') + } + + const modalInstance = serverSetupModalRef.value + if (!modalInstance) return + + modalInstance.show() + await nextTick() + + const ctx = modalInstance.ctx + if (!ctx) return + + ctx.setupType.value = 'modpack' + ctx.modpackSelection.value = { + projectId: request.projectId, + versionId: request.versionId, + name: request.name, + iconUrl: request.iconUrl, + } + ctx.modal.value?.setStage('final-config') + } + + function getCurrentServerInstallType(): ServerInstallableType { + const raw = Array.isArray(route.params.projectType) + ? route.params.projectType[0] + : route.params.projectType + if (raw === 'modpack' || raw === 'mod' || raw === 'plugin' || raw === 'datapack') { + return raw + } + throw new Error('This content type cannot be installed to a server from browse.') + } + + async function installProjectToServer(project: InstallableSearchResult) { + const contentType = getCurrentServerInstallType() + const sid = serverIdQuery.value + const wid = effectiveServerWorldId.value + if (!sid || !wid) { + throw new Error('No server world is available for install.') + } + + if (contentType === 'modpack') { + const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, { + include_changelog: false, + }) + const versionId = versions[0]?.id ?? project.version_id + if (!versionId) { + throw new Error('No version found for this modpack') + } + + await openServerModpackInstallFlow({ + projectId: project.project_id, + versionId, + name: project.name, + iconUrl: project.icon_url ?? undefined, + }) + return false + } + + const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, { + include_changelog: false, + }) + const serverLoader = (serverContextServerData.value?.loader ?? '').toLowerCase() + const serverGameVersion = (serverContextServerData.value?.mc_version ?? '').trim() + const compatibleLoaders = getCompatibleLoaders(serverLoader) + + const hasGameVersionMatch = (version: Labrinth.Versions.v2.Version) => + !serverGameVersion || version.game_versions.includes(serverGameVersion) + const hasLoaderMatch = (version: Labrinth.Versions.v2.Version) => { + if (contentType === 'datapack') return true + if (compatibleLoaders.size === 0) return true + return version.loaders.some((loader) => compatibleLoaders.has(normalizeLoader(loader))) + } + + let matchingVersion = versions.find( + (version) => hasGameVersionMatch(version) && hasLoaderMatch(version), + ) + if (!matchingVersion) { + matchingVersion = versions.find((version) => hasLoaderMatch(version)) + } + if (!matchingVersion) { + matchingVersion = versions.find((version) => hasGameVersionMatch(version)) + } + if (!matchingVersion) { + matchingVersion = versions[0] + } + if (!matchingVersion) { + throw new Error('No installable version was found for this project.') + } + + await client.archon.content_v1.addAddon(sid, wid, { + project_id: matchingVersion.project_id, + version_id: matchingVersion.id, + }) + + serverContentProjectIds.value = new Set([...serverContentProjectIds.value, project.project_id]) + return true + } + + function onServerFlowBack() { + serverSetupModalRef.value?.hide() + } + + async function handleServerModpackFlowCreate(config: CreationFlowContextValue) { + const sid = serverIdQuery.value + const wid = effectiveServerWorldId.value + if (!sid || !wid || !config.modpackSelection.value) { + config.loading.value = false + return + } + + try { + await client.archon.content_v1.installContent(sid, wid, { + content_variant: 'modpack', + spec: { + platform: 'modrinth', + project_id: config.modpackSelection.value.projectId, + version_id: config.modpackSelection.value.versionId, + }, + soft_override: false, + properties: config.buildProperties(), + } satisfies Archon.Content.v1.InstallWorldContent) + serverSetupModalRef.value?.hide() + + if (serverFlowFrom.value === 'onboarding') { + await client.archon.servers_v1.endIntro(sid) + await router.push(`/hosting/manage/${sid}/content`) + return + } + + await router.push(`/hosting/manage/${sid}?openSettings=installation`) + } catch (err) { + handleError(err as Error) + config.loading.value = false + } + } + + function markServerProjectInstalled(id: string) { + serverContentProjectIds.value = new Set([...serverContentProjectIds.value, id]) + } + + return { + serverIdQuery, + worldIdQuery, + browseFrom, + serverFlowFrom, + isFromWorlds, + isServerContext, + isSetupServerContext, + effectiveServerWorldId, + serverContextServerData, + serverContentProjectIds, + serverBackUrl, + serverBackLabel, + serverBrowseHeading, + initServerContext, + watchServerContextChanges, + searchServerModpacks, + getServerProjectVersions, + enforceSetupModpackRoute, + installProjectToServer, + onServerFlowBack, + handleServerModpackFlowCreate, + markServerProjectInstalled, + } +} diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index 2748853d58..841d4a97fc 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -43,11 +43,8 @@ export default new createRouter({ children: [ { path: '', - redirect: (to) => { - const rawId = Array.isArray(to.params.id) ? to.params.id[0] : to.params.id - if (!rawId) return '/hosting/manage' - return `/hosting/manage/${encodeURIComponent(rawId)}/content` - }, + name: 'ServerManageOverview', + component: Hosting.Overview, }, { path: 'content', diff --git a/apps/app-frontend/tsconfig.app.json b/apps/app-frontend/tsconfig.app.json index f723e2026f..504558d95f 100644 --- a/apps/app-frontend/tsconfig.app.json +++ b/apps/app-frontend/tsconfig.app.json @@ -3,7 +3,7 @@ "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2021", "DOM", "DOM.Iterable"], "skipLibCheck": true, "moduleResolution": "bundler", diff --git a/apps/app/capabilities/plugins.json b/apps/app/capabilities/plugins.json index f1a45bf27d..4813b88730 100644 --- a/apps/app/capabilities/plugins.json +++ b/apps/app/capabilities/plugins.json @@ -22,7 +22,11 @@ { "identifier": "http:default", - "allow": [{ "url": "https://modrinth.com/*" }, { "url": "https://*.modrinth.com/*" }] + "allow": [ + { "url": "https://modrinth.com/*" }, + { "url": "https://*.modrinth.com/*" }, + { "url": "https://*.nodes.modrinth.com/*" } + ] }, "dialog:allow-save", diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json index bc06fa6405..e5c46f4f3b 100644 --- a/apps/app/tauri.conf.json +++ b/apps/app/tauri.conf.json @@ -87,7 +87,7 @@ "capabilities": ["ads", "core", "plugins"], "csp": { "default-src": "'self' customprotocol: asset:", - "connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com wss://*.nodes.modrinth.com 'self' data: blob:", + "connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.nodes.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com wss://*.nodes.modrinth.com 'self' data: blob:", "font-src": ["https://cdn-raw.modrinth.com/fonts/"], "img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:", "style-src": "'unsafe-inline' 'self'", diff --git a/apps/frontend/src/components/ui/servers/ModrinthServersIcon.vue b/apps/frontend/src/components/brand/ModrinthServersIcon.vue similarity index 100% rename from apps/frontend/src/components/ui/servers/ModrinthServersIcon.vue rename to apps/frontend/src/components/brand/ModrinthServersIcon.vue diff --git a/apps/frontend/src/components/ui/servers/notice/AssignNoticeModal.vue b/apps/frontend/src/components/ui/admin/AssignNoticeModal.vue similarity index 100% rename from apps/frontend/src/components/ui/servers/notice/AssignNoticeModal.vue rename to apps/frontend/src/components/ui/admin/AssignNoticeModal.vue diff --git a/apps/frontend/src/components/ui/servers/LoaderSelector.vue b/apps/frontend/src/components/ui/servers/LoaderSelector.vue deleted file mode 100644 index 0998a9ced1..0000000000 --- a/apps/frontend/src/components/ui/servers/LoaderSelector.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/LoaderSelectorCard.vue b/apps/frontend/src/components/ui/servers/LoaderSelectorCard.vue deleted file mode 100644 index 0017f1076e..0000000000 --- a/apps/frontend/src/components/ui/servers/LoaderSelectorCard.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/LogLine.vue b/apps/frontend/src/components/ui/servers/LogLine.vue deleted file mode 100644 index 07df871a33..0000000000 --- a/apps/frontend/src/components/ui/servers/LogLine.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue b/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue deleted file mode 100644 index 3022a7b06a..0000000000 --- a/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue +++ /dev/null @@ -1,276 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/PanelServerStatus.vue b/apps/frontend/src/components/ui/servers/PanelServerStatus.vue deleted file mode 100644 index f5b42fa0e1..0000000000 --- a/apps/frontend/src/components/ui/servers/PanelServerStatus.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/PanelSpinner.vue b/apps/frontend/src/components/ui/servers/PanelSpinner.vue deleted file mode 100644 index c2c7f55eab..0000000000 --- a/apps/frontend/src/components/ui/servers/PanelSpinner.vue +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/PanelTerminal.vue b/apps/frontend/src/components/ui/servers/PanelTerminal.vue deleted file mode 100644 index 950a2dc9ea..0000000000 --- a/apps/frontend/src/components/ui/servers/PanelTerminal.vue +++ /dev/null @@ -1,1414 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/ui/servers/PlatformChangeModpackVersionModal.vue b/apps/frontend/src/components/ui/servers/PlatformChangeModpackVersionModal.vue deleted file mode 100644 index e7b0c74ed7..0000000000 --- a/apps/frontend/src/components/ui/servers/PlatformChangeModpackVersionModal.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue b/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue deleted file mode 100644 index f4ca68f7c0..0000000000 --- a/apps/frontend/src/components/ui/servers/PlatformVersionSelectModal.vue +++ /dev/null @@ -1,538 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/SaveBanner.vue b/apps/frontend/src/components/ui/servers/SaveBanner.vue deleted file mode 100644 index a43e4dd1f1..0000000000 --- a/apps/frontend/src/components/ui/servers/SaveBanner.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/ServerSidebar.vue b/apps/frontend/src/components/ui/servers/ServerSidebar.vue deleted file mode 100644 index 1b455df0c1..0000000000 --- a/apps/frontend/src/components/ui/servers/ServerSidebar.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/ServerStats.vue b/apps/frontend/src/components/ui/servers/ServerStats.vue deleted file mode 100644 index a16aa63226..0000000000 --- a/apps/frontend/src/components/ui/servers/ServerStats.vue +++ /dev/null @@ -1,256 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/ui/servers/TeleportOverflowMenu.vue b/apps/frontend/src/components/ui/servers/TeleportOverflowMenu.vue deleted file mode 100644 index 4a8f774134..0000000000 --- a/apps/frontend/src/components/ui/servers/TeleportOverflowMenu.vue +++ /dev/null @@ -1,438 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/icons/ChevronDownIcon.vue b/apps/frontend/src/components/ui/servers/icons/ChevronDownIcon.vue deleted file mode 100644 index 783d18a402..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/ChevronDownIcon.vue +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/ChevronUpIcon.vue b/apps/frontend/src/components/ui/servers/icons/ChevronUpIcon.vue deleted file mode 100644 index da6cf408ab..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/ChevronUpIcon.vue +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/CodeFileIcon.vue b/apps/frontend/src/components/ui/servers/icons/CodeFileIcon.vue deleted file mode 100644 index 42022a33ce..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/CodeFileIcon.vue +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/CogFolderIcon.vue b/apps/frontend/src/components/ui/servers/icons/CogFolderIcon.vue deleted file mode 100644 index cc8fc1bc66..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/CogFolderIcon.vue +++ /dev/null @@ -1,26 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/EarthIcon.vue b/apps/frontend/src/components/ui/servers/icons/EarthIcon.vue deleted file mode 100644 index e96f944bcc..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/EarthIcon.vue +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/FullscreenIcon.vue b/apps/frontend/src/components/ui/servers/icons/FullscreenIcon.vue deleted file mode 100644 index 383912d2b8..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/FullscreenIcon.vue +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/ImageFileIcon.vue b/apps/frontend/src/components/ui/servers/icons/ImageFileIcon.vue deleted file mode 100644 index 6aa81c2779..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/ImageFileIcon.vue +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue b/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue deleted file mode 100644 index 090bb945bd..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/icons/LoadingIcon.vue b/apps/frontend/src/components/ui/servers/icons/LoadingIcon.vue deleted file mode 100644 index 9e9a8ad3fa..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/LoadingIcon.vue +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/MinimizeIcon.vue.vue b/apps/frontend/src/components/ui/servers/icons/MinimizeIcon.vue.vue deleted file mode 100644 index 27b0fcad21..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/MinimizeIcon.vue.vue +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/PanelErrorIcon.vue b/apps/frontend/src/components/ui/servers/icons/PanelErrorIcon.vue deleted file mode 100644 index 2ecad74dce..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/PanelErrorIcon.vue +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/SlashIcon.vue b/apps/frontend/src/components/ui/servers/icons/SlashIcon.vue deleted file mode 100644 index 7f6e62fca2..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/SlashIcon.vue +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/TextFileIcon.vue b/apps/frontend/src/components/ui/servers/icons/TextFileIcon.vue deleted file mode 100644 index 99bcee1ac1..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/TextFileIcon.vue +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/icons/Timer.vue b/apps/frontend/src/components/ui/servers/icons/Timer.vue deleted file mode 100644 index e1ead004b1..0000000000 --- a/apps/frontend/src/components/ui/servers/icons/Timer.vue +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/apps/frontend/src/components/ui/servers/notice/NoticeDashboardItem.vue b/apps/frontend/src/components/ui/servers/notice/NoticeDashboardItem.vue deleted file mode 100644 index 2208808456..0000000000 --- a/apps/frontend/src/components/ui/servers/notice/NoticeDashboardItem.vue +++ /dev/null @@ -1,127 +0,0 @@ - - diff --git a/apps/frontend/src/components/ui/thread/ThreadView.vue b/apps/frontend/src/components/ui/thread/ThreadView.vue index 608f0f1f10..dc9bcaa6c4 100644 --- a/apps/frontend/src/components/ui/thread/ThreadView.vue +++ b/apps/frontend/src/components/ui/thread/ThreadView.vue @@ -74,7 +74,7 @@ - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/index.vue b/apps/frontend/src/pages/hosting/manage/[id]/index.vue index 46e19ea119..9b4ae555cc 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/index.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/index.vue @@ -1,728 +1,13 @@ - - + + diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options.vue b/apps/frontend/src/pages/hosting/manage/[id]/options.vue deleted file mode 100644 index 6fa28b857f..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options.vue +++ /dev/null @@ -1,56 +0,0 @@ - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/advanced.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/advanced.vue deleted file mode 100644 index b730006fff..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/advanced.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/billing.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/billing.vue deleted file mode 100644 index 6e470aaafc..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/billing.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue deleted file mode 100644 index 311bee0a9c..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/index.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/loader.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/loader.vue deleted file mode 100644 index e3a54224ea..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/loader.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/network.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/network.vue deleted file mode 100644 index a63867e288..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/network.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue deleted file mode 100644 index 48c1fb8a6d..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/properties.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/settings/billing/index.vue b/apps/frontend/src/pages/settings/billing/index.vue index 0c6df65c7c..e2db08495c 100644 --- a/apps/frontend/src/pages/settings/billing/index.vue +++ b/apps/frontend/src/pages/settings/billing/index.vue @@ -718,7 +718,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query' import { useIntervalFn } from '@vueuse/core' import { computed, ref, watch } from 'vue' -import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue' +import ModrinthServersIcon from '~/components/brand/ModrinthServersIcon.vue' import ServersUpgradeModalWrapper from '~/components/ui/servers/ServersUpgradeModalWrapper.vue' import { products } from '~/generated/state.json' diff --git a/apps/frontend/src/providers/setup/auth.ts b/apps/frontend/src/providers/setup/auth.ts index ae717324b0..02ce738f6b 100644 --- a/apps/frontend/src/providers/setup/auth.ts +++ b/apps/frontend/src/providers/setup/auth.ts @@ -10,6 +10,7 @@ export function setupAuthProvider(auth: Awaited>) { const authProvider: AuthProvider = { session_token: sessionToken, user, + isReady: ref(true), requestSignIn: async (redirectPath: string) => { await router.push({ path: '/auth/sign-in', diff --git a/apps/frontend/src/store/console.ts b/apps/frontend/src/store/console.ts index 5e9d0169ad..fb72b37c86 100644 --- a/apps/frontend/src/store/console.ts +++ b/apps/frontend/src/store/console.ts @@ -1,164 +1 @@ -import { createGlobalState } from '@vueuse/core' -import { type Ref, shallowRef } from 'vue' - -/** - * Maximum number of console output lines to store - * @type {number} - */ -const maxLines = 10000 -const batchTimeout = 300 // ms -const initialBatchSize = 256 - -/** - * Provides a global console output state management system - * Allows adding, storing, and clearing console output with a maximum line limit - * - * @returns {Object} Console state management methods and reactive state - * @property {Ref} consoleOutput - Reactive array of console output lines - * @property {function(string): void} addConsoleOutput - Method to add a new console output line - * @property {function(): void} clear - Method to clear all console output - */ -export const useModrinthServersConsole = createGlobalState(() => { - /** - * Reactive array storing console output lines - * @type {Ref} - */ - const output: Ref = shallowRef([]) - const searchQuery: Ref = shallowRef('') - const filteredOutput: Ref = shallowRef([]) - let searchRegex: RegExp | null = null - - let lineBuffer: string[] = [] - let batchTimer: NodeJS.Timeout | null = null - let isProcessingInitialBatch = false - - let refilterTimer: NodeJS.Timeout | null = null - const refilterTimeout = 100 // ms - - const updateFilter = () => { - if (!searchQuery.value) { - filteredOutput.value = [] - return - } - - if (!searchRegex) { - searchRegex = new RegExp(searchQuery.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') - } - - filteredOutput.value = output.value.filter((line) => searchRegex?.test(line) ?? false) - } - - const scheduleRefilter = () => { - if (refilterTimer) clearTimeout(refilterTimer) - refilterTimer = setTimeout(updateFilter, refilterTimeout) - } - - const flushBuffer = () => { - if (lineBuffer.length === 0) return - - const processedLines = lineBuffer.flatMap((line) => line.split('\n').filter(Boolean)) - - if (isProcessingInitialBatch && processedLines.length >= initialBatchSize) { - isProcessingInitialBatch = false - output.value = processedLines.slice(-maxLines) - } else { - const newOutput = [...output.value, ...processedLines] - output.value = newOutput.slice(-maxLines) - } - - lineBuffer = [] - batchTimer = null - - if (searchQuery.value) { - scheduleRefilter() - } - } - - /** - * Adds a new output line to the console output - * Automatically removes the oldest line if max output is exceeded - * - * @param {string} line - The console output line to add - */ - const addLine = (line: string): void => { - lineBuffer.push(line) - - if (!batchTimer) { - batchTimer = setTimeout(flushBuffer, batchTimeout) - } - } - - /** - * Adds multiple output lines to the console output - * Automatically removes the oldest lines if max output is exceeded - * - * @param {string[]} lines - The console output lines to add - * @returns {void} - */ - const addLines = (lines: string[]): void => { - if (output.value.length === 0 && lines.length >= initialBatchSize) { - isProcessingInitialBatch = true - lineBuffer = lines - flushBuffer() - return - } - - lineBuffer.push(...lines) - - if (!batchTimer) { - batchTimer = setTimeout(flushBuffer, batchTimeout) - } - } - - /** - * Sets the search query and filters the output based on the query - * - * @param {string} query - The search query - */ - const setSearchQuery = (query: string): void => { - searchQuery.value = query - searchRegex = null - updateFilter() - } - - /** - * Clears all console output lines - */ - const clear = (): void => { - output.value = [] - filteredOutput.value = [] - searchQuery.value = '' - lineBuffer = [] - isProcessingInitialBatch = false - if (batchTimer) { - clearTimeout(batchTimer) - batchTimer = null - } - if (refilterTimer) { - clearTimeout(refilterTimer) - refilterTimer = null - } - searchRegex = null - } - - /** - * Finds the index of a line in the main output - * - * @param {string} line - The line to find - * @returns {number} The index of the line, or -1 if not found - */ - const findLineIndex = (line: string): number => { - return output.value.findIndex((l) => l === line) - } - - return { - output, - searchQuery, - filteredOutput, - addLine, - addLines, - setSearchQuery, - clear, - findLineIndex, - } -}) +export { useModrinthServersConsole } from '@modrinth/ui' diff --git a/packages/api-client/src/modules/kyros/files/v0.ts b/packages/api-client/src/modules/kyros/files/v0.ts index 4712c08379..fb8f4d19b7 100644 --- a/packages/api-client/src/modules/kyros/files/v0.ts +++ b/packages/api-client/src/modules/kyros/files/v0.ts @@ -1,12 +1,19 @@ import { AbstractModule } from '../../../core/abstract-module' import type { UploadHandle, UploadProgress } from '../../../types/upload' +import type { Archon } from '../../archon/types' import type { Kyros } from '../types' +type NodeFsAuth = Pick + export class KyrosFilesV0Module extends AbstractModule { public getModuleID(): string { return 'kyros_files_v0' } + private getNodeBaseUrl(auth: NodeFsAuth): string { + return `https://${auth.url.replace(/\/modrinth\/v\d+\/fs\/?$/, '')}` + } + /** * List directory contents with pagination * @@ -62,6 +69,24 @@ export class KyrosFilesV0Module extends AbstractModule { }) } + /** + * Download a file using explicit filesystem auth credentials. + * + * @param auth - Filesystem auth (url + token) from Archon + * @param path - File path (e.g., "/server-icon.png") + * @returns Promise resolving to file Blob + */ + public async downloadFileWithAuth(auth: NodeFsAuth, path: string): Promise { + return this.client.request('/fs/download', { + api: this.getNodeBaseUrl(auth), + version: 'modrinth/v0', + method: 'GET', + params: { path }, + headers: { Authorization: `Bearer ${auth.token}` }, + skipAuth: true, + }) + } + /** * Upload a file to a server's filesystem with progress tracking * @@ -89,6 +114,36 @@ export class KyrosFilesV0Module extends AbstractModule { }) } + /** + * Upload a file using explicit filesystem auth credentials. + * + * @param auth - Filesystem auth (url + token) from Archon + * @param path - Destination path (e.g., "/server-icon.png") + * @param file - File to upload + * @param options - Optional progress callback and feature overrides + * @returns UploadHandle with promise, onProgress, and cancel + */ + public uploadFileWithAuth( + auth: NodeFsAuth, + path: string, + file: File | Blob, + options?: { + onProgress?: (progress: UploadProgress) => void + retry?: boolean | number + }, + ): UploadHandle { + return this.client.upload('/fs/create', { + api: this.getNodeBaseUrl(auth), + version: 'modrinth/v0', + file, + params: { path, type: 'file' }, + headers: { Authorization: `Bearer ${auth.token}` }, + onProgress: options?.onProgress, + retry: options?.retry, + skipAuth: true, + }) + } + /** * Update file contents * @@ -152,6 +207,28 @@ export class KyrosFilesV0Module extends AbstractModule { }) } + /** + * Delete a file or folder using explicit filesystem auth credentials. + * + * @param auth - Filesystem auth (url + token) from Archon + * @param path - Path to delete + * @param recursive - If true, delete directory contents recursively + */ + public async deleteFileOrFolderWithAuth( + auth: NodeFsAuth, + path: string, + recursive: boolean, + ): Promise { + return this.client.request('/fs/delete', { + api: this.getNodeBaseUrl(auth), + version: 'modrinth/v0', + method: 'DELETE', + params: { path, recursive }, + headers: { Authorization: `Bearer ${auth.token}` }, + skipAuth: true, + }) + } + /** * Extract an archive file (zip, tar, etc.) * diff --git a/packages/api-client/src/platform/tauri.ts b/packages/api-client/src/platform/tauri.ts index 7e57fc2a53..6b3c2ed616 100644 --- a/packages/api-client/src/platform/tauri.ts +++ b/packages/api-client/src/platform/tauri.ts @@ -103,11 +103,38 @@ export class TauriModrinthClient extends XHRUploadClient { throw error } + // Handle binary downloads (e.g. kyros fs files) before JSON parsing. + const contentType = response.headers.get('content-type')?.toLowerCase() ?? '' + if (fullUrl.includes('/fs/download')) { + return (await response.blob()) as T + } + if ( + contentType.startsWith('image/') || + contentType.startsWith('audio/') || + contentType.startsWith('video/') || + contentType.includes('application/octet-stream') + ) { + return (await response.blob()) as T + } + + if (response.status === 204 || response.status === 205) { + return undefined as T + } + + if (contentType.includes('application/json') || contentType.includes('+json')) { + return (await response.json()) as T + } + const text = await response.text() if (!text) { return undefined as T } - return JSON.parse(text) as T + + try { + return JSON.parse(text) as T + } catch { + return text as T + } } catch (error) { throw this.normalizeError(error) } diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts index ff66853f3d..c63ce31a8f 100644 --- a/packages/assets/generated-icons.ts +++ b/packages/assets/generated-icons.ts @@ -3,8 +3,6 @@ import type { FunctionalComponent, SVGAttributes } from 'vue' -export type IconComponent = FunctionalComponent - import _AffiliateIcon from './icons/affiliate.svg?component' import _AlignLeftIcon from './icons/align-left.svg?component' import _ArchiveIcon from './icons/archive.svg?component' @@ -386,12 +384,15 @@ import _VersionIcon from './icons/version.svg?component' import _WikiIcon from './icons/wiki.svg?component' import _WindowIcon from './icons/window.svg?component' import _WorldIcon from './icons/world.svg?component' +import _WrapTextIcon from './icons/wrap-text.svg?component' import _WrenchIcon from './icons/wrench.svg?component' import _XIcon from './icons/x.svg?component' import _XCircleIcon from './icons/x-circle.svg?component' import _ZoomInIcon from './icons/zoom-in.svg?component' import _ZoomOutIcon from './icons/zoom-out.svg?component' +export type IconComponent = FunctionalComponent + export const AffiliateIcon = _AffiliateIcon export const AlignLeftIcon = _AlignLeftIcon export const ArchiveIcon = _ArchiveIcon @@ -773,6 +774,7 @@ export const VersionIcon = _VersionIcon export const WikiIcon = _WikiIcon export const WindowIcon = _WindowIcon export const WorldIcon = _WorldIcon +export const WrapTextIcon = _WrapTextIcon export const WrenchIcon = _WrenchIcon export const XIcon = _XIcon export const XCircleIcon = _XCircleIcon diff --git a/packages/assets/icons/wrap-text.svg b/packages/assets/icons/wrap-text.svg new file mode 100644 index 0000000000..ed9eb6b325 --- /dev/null +++ b/packages/assets/icons/wrap-text.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/packages/ui/package.json b/packages/ui/package.json index 2132684af4..974aaafc2c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -54,6 +54,7 @@ "@codemirror/language": "^6.9.3", "@codemirror/state": "^6.3.2", "@codemirror/view": "^6.22.1", + "@intercom/messenger-js-sdk": "^0.0.14", "@modrinth/api-client": "workspace:*", "@modrinth/assets": "workspace:*", "@modrinth/blog": "workspace:*", @@ -62,6 +63,7 @@ "@tresjs/cientos": "^4.3.0", "@tresjs/core": "^4.3.4", "@tresjs/post-processing": "^2.4.0", + "@types/dompurify": "^3.0.5", "@types/markdown-it": "^14.1.1", "@types/three": "^0.172.0", "@vintl/how-ago": "^3.0.1", @@ -72,6 +74,7 @@ "ace-builds": "^1.43.5", "apexcharts": "^4.0.0", "dayjs": "^1.11.10", + "dompurify": "^3.1.7", "es-toolkit": "^1.44.0", "floating-vue": "^5.2.2", "fuse.js": "^6.6.2", diff --git a/packages/ui/src/components/base/BaseTerminal.vue b/packages/ui/src/components/base/BaseTerminal.vue index 2f30e0c8ef..88ac95d520 100644 --- a/packages/ui/src/components/base/BaseTerminal.vue +++ b/packages/ui/src/components/base/BaseTerminal.vue @@ -1,12 +1,31 @@