Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
72238fa
feat: server play frontend draft
IMB11 Aug 3, 2026
cdbca34
fix: build error
IMB11 Sep 4, 2026
5fabd04
feat: start on content tab env focused design (#7386)
IMB11 Sep 4, 2026
ebe105f
fix: remove feature flag
IMB11 Sep 4, 2026
5631ba4
fix: build
IMB11 Sep 4, 2026
cc5405c
dev: notes
IMB11 Sep 4, 2026
32d7f84
feat: impl endpoints into panel
IMB11 Sep 10, 2026
fabe216
qa: 1
IMB11 Sep 10, 2026
4b865e6
fix:
IMB11 Sep 16, 2026
cce48a8
fix: csp
IMB11 Sep 16, 2026
613872e
fix: archon
IMB11 Sep 16, 2026
0ef817d
fix: missing features
IMB11 Sep 17, 2026
140faa9
fix: qa
IMB11 Sep 20, 2026
fd1dd7e
fix: qa
IMB11 Sep 21, 2026
06bc60e
fix: qa
IMB11 Sep 21, 2026
3a62e84
fix: qa
IMB11 Sep 22, 2026
843c7fb
chore: qa
IMB11 Sep 22, 2026
90496e5
fix: qa
IMB11 Sep 22, 2026
6467739
feat: icon editor in server panel
IMB11 Sep 22, 2026
3e23445
fix: qa
IMB11 Sep 22, 2026
a314d7a
fix: prepr + lint
IMB11 Sep 22, 2026
859ebe5
chore: cleanup
IMB11 Sep 22, 2026
907ed63
fix: qa
IMB11 Sep 22, 2026
51970a9
fix: qa
IMB11 Sep 24, 2026
ccb0b14
fix: lint
IMB11 Sep 24, 2026
f30e813
fix: flags
IMB11 Sep 24, 2026
d2ac190
fix: transitions for onboarding
IMB11 Sep 24, 2026
05e4256
feat: fix fetch issues
IMB11 Sep 24, 2026
7b22be1
feat: inviter field support
IMB11 Sep 24, 2026
f5383f3
fix: qa
IMB11 Sep 24, 2026
7b53697
feat: icon api
IMB11 Sep 28, 2026
a6878bb
fix: qa
IMB11 Sep 28, 2026
0501880
fix: lint
IMB11 Sep 29, 2026
cd5cbe6
feat: qa
IMB11 Sep 30, 2026
3ff5ea0
fix: qa
IMB11 Sep 30, 2026
b9d2df7
fix: qa
IMB11 Sep 30, 2026
551172b
fix: qa
IMB11 Sep 30, 2026
6d34958
fix: qa
IMB11 Sep 30, 2026
36fd647
fix: lint
IMB11 Sep 30, 2026
72f746c
chore(ci): make macos signing optional
fetchfern Oct 2, 2026
9bf53fd
fix apple signing disabled
fetchfern Oct 2, 2026
e0a7872
fix: fmt after rebase
IMB11 Oct 5, 2026
28e96d8
fix: remove tokens from max use calc
IMB11 Oct 6, 2026
7638f54
fix: panel freeze
IMB11 Oct 6, 2026
96a8444
fix: bug
IMB11 Oct 6, 2026
c2cecf8
fix: o11y
IMB11 Oct 6, 2026
36d23f3
feat: update toast + modal
IMB11 Oct 7, 2026
f2fcb74
fix: qa
IMB11 Oct 7, 2026
83f8bda
fix: new update qa
IMB11 Oct 8, 2026
80ef886
fix: icon mismatch qa
IMB11 Oct 8, 2026
86e24e0
fix: qa
IMB11 Oct 8, 2026
6aab93a
feat: config file push update modal
IMB11 Oct 8, 2026
d89537e
fix: qa
IMB11 Oct 8, 2026
ea8d042
chore: align app-frontend impl with standards
IMB11 Oct 9, 2026
b2e9baa
fix: qa
IMB11 Oct 9, 2026
cfbd538
feat: curseforge zip modpack import flow for server onboarding (#7814)
IMB11 Oct 9, 2026
49b40e6
feat: side type review
fetchfern Oct 12, 2026
059baae
fix: qa
IMB11 Oct 13, 2026
72311e3
fix: qa
IMB11 Oct 13, 2026
f3921c9
fix: use original query param
IMB11 Oct 13, 2026
e51bd50
feat: release channels for server panel
IMB11 Oct 13, 2026
e5ebbf4
fix: install flows
IMB11 Oct 13, 2026
5ae8222
fix: qa
IMB11 Oct 13, 2026
781a42d
fix: softOverride: false for onboarding
fetchfern Oct 14, 2026
5755038
fix: prepr
IMB11 Oct 14, 2026
1487ba4
workaround: raise config bundle size limits
fetchfern Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 15 additions & 2 deletions .github/workflows/theseus-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ on:
- 'packages/utils/**'
workflow_dispatch:
inputs:
sign-macos-binaries:
description: Sign macOS binaries
type: boolean
default: false
required: false
sign-windows-binaries:
description: Sign Windows binaries
type: boolean
Expand Down Expand Up @@ -170,9 +175,9 @@ jobs:
dasel delete -f apps/app/tauri-release.conf.json 'bundle.windows.signCommand'
fi

- name: Build macOS app
- name: Build signed macOS app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
if: contains(matrix.artifact-target-name, 'darwin')
if: contains(matrix.artifact-target-name, 'darwin') && (github.event_name != 'workflow_dispatch' || inputs.sign-macos-binaries)
env:
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
Expand All @@ -185,6 +190,14 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}

- name: Build unsigned macOS app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
if: contains(matrix.artifact-target-name, 'darwin') && github.event_name == 'workflow_dispatch' && !inputs.sign-macos-binaries
env:
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}

- name: Build Linux app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
if: contains(matrix.artifact-target-name, 'linux')
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 61 additions & 38 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,27 @@ import {
commonSettingsMessages,
ContentInstallModal,
ContentUpdaterModal,
createServerInviteHandoff,
createServerOnboardingFlow,
CreationFlowModal,
defineMessages,
I18nDebugPanel,
IconButton,
LoadingBar,
NotificationPanel,
PopupNotificationPanel,
provideFileDownload,
provideIconCache,
provideLoadingState,
provideModalBehavior,
provideModrinthClient,
provideNotificationManager,
providePageContext,
providePopupNotificationManager,
provideServerInviteHandoff,
provideServerOnboardingFlow,
provideServerPlay,
ServerOnboardingModal,
TeleportOverflowMenu,
TooltipDirective,
useDebugLogger,
Expand All @@ -64,6 +72,7 @@ import { RouterView, useRoute, useRouter } from 'vue-router'
import { useRouteLoading } from '@/app/runtime/use-route-loading'
import AppSidebar from '@/app/shell/sidebar/index.vue'
import AppTitleBar from '@/app/shell/title-bar/index.vue'
import { trackExternalFileDownload } from '@/components/ui/download-manager/external-file-downloads'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
Expand All @@ -89,6 +98,12 @@ import { useInstanceMetadataRefresh } from '@/composables/use-instance-metadata-
import { useQuickInstanceLimit } from '@/composables/use-quick-instance-limit.ts'
import { isDarkTheme, useTheme } from '@/composables/use-theme.ts'
import { config } from '@/config'
import ServerPlayHandler from '@/features/hosting/play-handler.vue'
import { prefetchHostingData } from '@/features/hosting/prefetch'
import {
createInstanceLaunchState,
provideInstanceLaunchState,
} from '@/features/instances/launch-state'
import { provideMinecraftAccounts } from '@/features/minecraft-accounts/context'
import NewUpdateModal from '@/features/updates/new-update-modal/index.vue'
import { getAccountAppearance, rememberAccountAppearance } from '@/helpers/account-appearance.ts'
Expand Down Expand Up @@ -156,6 +171,8 @@ import {
instanceListQueryOptions,
screenshotKeys,
} from '@/pages/instance/query-options'
import { createFileDownload } from '@/platform/adapters/file-download'
import { createIconCache } from '@/platform/adapters/icon-cache'
import { createAppClient } from '@/platform/modrinth-client'
import {
appUpdateState,
Expand Down Expand Up @@ -286,6 +303,12 @@ useAppEvent('ads_consent_required', handleAdsConsentRequired, appEvents)

const tauriApiClient = createAppClient()
provideModrinthClient(tauriApiClient)
provideServerOnboardingFlow(createServerOnboardingFlow())
provideServerInviteHandoff(createServerInviteHandoff())
provideFileDownload(createFileDownload(tauriApiClient, trackExternalFileDownload))
const iconCache = createIconCache()
provideIconCache(iconCache)
provideInstanceLaunchState(createInstanceLaunchState())
const { data: authenticatedModrinthUser } = useQuery({
queryKey: computed(() => ['authenticated-user', 'campaigns', credentials.value?.user?.id]),
queryFn: () => tauriApiClient.labrinth.users_v3.getAuthenticated(),
Expand Down Expand Up @@ -807,8 +830,14 @@ async function setupApp() {
}

const stateFailed = ref(false)
const { loading, canNavigateBack, canNavigateForward, onSuspensePending, onSuspenseResolve } =
useRouteLoading({ stateInitialized, stateFailed })
const {
loading,
canNavigateBack,
canNavigateForward,
onboardingPageReady,
onSuspensePending,
onSuspenseResolve,
} = useRouteLoading({ stateInitialized, stateFailed })
provideLoadingState(loading)

traceStartupStep('Initialize backend state', () => initialize_state(appEventChannel))
Expand All @@ -832,42 +861,19 @@ const handleClose = async () => {

const queryClient = useQueryClient()

watch(stateInitialized, (ready) => {
if (ready) {
queryClient.prefetchQuery({
queryKey: ['servers'],
queryFn: async () => {
const response = await tauriApiClient.archon.servers_v0.list({ limit: 100 })
const hasMedalServers = response.servers.some((s) => s.is_medal)
if (hasMedalServers) {
const subscriptions = await tauriApiClient.labrinth.billing_internal.getSubscriptions()
for (const server of response.servers) {
if (server.is_medal) {
const sub = subscriptions.find((s) => s.metadata?.id === server.server_id)
if (sub) {
server.medal_expires = new Date(
new Date(sub.created).getTime() + 5 * 86400000,
).toISOString()
}
}
}
}
return response
},
staleTime: 30_000,
})
queryClient.prefetchQuery({
queryKey: ['billing', 'subscriptions'],
queryFn: () => tauriApiClient.labrinth.billing_internal.getSubscriptions(),
staleTime: 30_000,
})
queryClient.prefetchQuery({
queryKey: ['billing', 'payments'],
queryFn: () => tauriApiClient.labrinth.billing_internal.getPayments(),
staleTime: 30_000,
})
}
})
watch(
() => stateInitialized.value && credentials.value?.session,
(session) => {
if (!session) return

void prefetchHostingData(
queryClient,
tauriApiClient,
iconCache,
() => credentials.value?.session === session,
)
},
)

const error = useError()
const errorModal = ref()
Expand Down Expand Up @@ -957,6 +963,14 @@ const contentInstallModpackAlreadyInstalledModal = ref()
const addServerToInstanceModal = ref()
const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const serverPlayHandler = ref()
const serverPlay = {
async play(target) {
if (!serverPlayHandler.value) throw new Error('Server play handler is not ready.')
await serverPlayHandler.value.play(target)
},
}
provideServerPlay(serverPlay)
const sharedInstanceInviteHandler = ref()
const updateToPlayModal = ref()

Expand Down Expand Up @@ -1537,6 +1551,8 @@ async function handleCommand(e) {
} else {
await run(e.id).catch(handleError)
}
} else if (e.event === 'PlayHostingServer') {
await serverPlay.play({ serverId: e.server_id, worldId: e.world_id }).catch(handleError)
} else if (e.event === 'InstallSharedInstanceInvite') {
await sharedInstanceInviteHandler.value?.installFromInviteId(e.invite_id)
} else if (e.event === 'InstallServer') {
Expand Down Expand Up @@ -2003,6 +2019,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@create="handleCreate"
@browse-modpacks="handleBrowseModpacks"
/>
<ServerOnboardingModal
browse-path="/browse/modpack"
:get-loader-manifest="getLoaderManifest"
:navigate="(to) => router.push(to)"
:page-ready="onboardingPageReady"
/>
<IconEditorModal
ref="creationIconEditorModal"
:config="creationGeneratedIcon?.config"
Expand Down Expand Up @@ -2272,6 +2294,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@go-to-instance="handleContentInstallModpackDuplicateGoToInstance"
/>
<ServerPlayHandler ref="serverPlayHandler" />
<SharedInstanceInviteHandler ref="sharedInstanceInviteHandler" />
<InstallToPlayModal ref="installToPlayModal" :show-external-warnings="false" />
<UpdateToPlayModal ref="updateToPlayModal" :show-external-warnings="false" />
Expand Down
12 changes: 11 additions & 1 deletion apps/app-frontend/src/app/runtime/use-route-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function useRouteLoading({ stateInitialized, stateFailed }: RouteLoadingO
const route = useRoute()
const canNavigateBack = ref(false)
const canNavigateForward = ref(false)
const onboardingPageReady = ref(true)
let initialLoadToken: symbol | null = null
let routerToken: symbol | null = null
let suspenseToken: symbol | null = null
Expand Down Expand Up @@ -81,13 +82,15 @@ export function useRouteLoading({ stateInitialized, stateFailed }: RouteLoadingO
})

function onSuspensePending() {
onboardingPageReady.value = false
debugStartup('Route Suspense pending', { route: route.path })
suspensePending = true
if (suspenseToken) loading.end(suspenseToken)
suspenseToken = loading.begin(`Route Suspense: ${route.path}`)
}

function onSuspenseResolve() {
onboardingPageReady.value = true
debugStartup('Route Suspense resolved', { route: route.path })
if (suspenseToken) {
loading.end(suspenseToken)
Expand Down Expand Up @@ -116,5 +119,12 @@ export function useRouteLoading({ stateInitialized, stateFailed }: RouteLoadingO
}
})

return { loading, canNavigateBack, canNavigateForward, onSuspensePending, onSuspenseResolve }
return {
loading,
canNavigateBack,
canNavigateForward,
onboardingPageReady,
onSuspensePending,
onSuspenseResolve,
}
}
4 changes: 2 additions & 2 deletions apps/app-frontend/src/components/ui/HostingUpdateRequired.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { DownloadIcon, ExcitedRinthbot, RefreshCwIcon, ServerStackIcon } from '@modrinth/assets'
import { Button, commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { Button, defineMessages, useVIntl } from '@modrinth/ui'
import { computed } from 'vue'

import {
Expand Down Expand Up @@ -42,7 +42,7 @@ const messages = defineMessages({
useRootBreadcrumb({
slot: 'root',
id: 'servers',
label: () => formatMessage(commonMessages.serversLabel),
label: 'Hosting',
to: '/hosting/manage/',
visual: { type: 'icon', component: ServerStackIcon },
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ defineEmits<{ close: [] }>()

const messages = defineMessages({
title: { id: 'app.download-manager.title', defaultMessage: 'Download manager' },
show: { id: 'app.download-manager.show', defaultMessage: 'Show installation tasks' },
hide: { id: 'app.download-manager.hide', defaultMessage: 'Hide installation tasks' },
show: { id: 'app.download-manager.show-tasks', defaultMessage: 'Show tasks' },
hide: { id: 'app.download-manager.hide-tasks', defaultMessage: 'Hide tasks' },
attention: { id: 'app.download-manager.attention', defaultMessage: 'Needs attention' },
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CheckIcon,
CopyIcon,
DownloadIcon,
MinecraftServerIcon,
PauseIcon,
PlayIcon,
TrashIcon,
Expand All @@ -20,6 +21,7 @@ import {
ProgressBar,
truncatedTooltip,
useRelativeTime,
useServerIcon,
useVIntl,
} from '@modrinth/ui'
import { computed, useTemplateRef } from 'vue'
Expand All @@ -28,6 +30,10 @@ import { RouterLink } from 'vue-router'
import type { DownloadManagerJob } from './use-download-manager'

const props = defineProps<{ job: DownloadManagerJob }>()
const { icon: serverIcon } = useServerIcon(() => props.job.serverId ?? '')
const displayIcon = computed(() =>
props.job.serverId ? (serverIcon.value ?? MinecraftServerIcon) : props.job.iconUrl,
)
defineEmits<{
retry: [id: string]
cancel: [id: string]
Expand Down Expand Up @@ -65,7 +71,7 @@ const cancelLabel = computed(() =>
formatMessage(
needsAttention.value
? messages.dismiss
: props.job.kind === 'debug-export'
: props.job.kind === 'external-file' || props.job.kind === 'debug-export'
? commonMessages.cancelButton
: messages.cancel,
),
Expand Down Expand Up @@ -94,8 +100,8 @@ const instanceLink = computed(() =>
@click="instanceLink && $emit('open')"
>
<Avatar
v-if="job.iconUrl"
:src="job.iconUrl"
v-if="displayIcon"
:src="displayIcon"
size="36px"
no-shadow
class="!rounded-xl border border-solid border-surface-5"
Expand All @@ -105,7 +111,9 @@ const instanceLink = computed(() =>
class="flex size-9 shrink-0 items-center justify-center rounded-xl border border-solid border-surface-5 bg-purple/10"
>
<component
:is="job.kind === 'debug-export' ? DownloadIcon : BoxIcon"
:is="
job.kind === 'external-file' || job.kind === 'debug-export' ? DownloadIcon : BoxIcon
"
class="size-6 text-primary"
aria-hidden="true"
/>
Expand All @@ -119,7 +127,7 @@ const instanceLink = computed(() =>
{{ job.title }}
</span>
<span
v-if="job.taskType && job.taskType !== job.text"
v-if="job.kind !== 'external-file' && job.taskType && job.taskType !== job.text"
class="truncate text-xs font-medium leading-4 text-primary"
>
{{ job.taskType }}
Expand All @@ -134,6 +142,12 @@ const instanceLink = computed(() =>
: 'text-primary'
"
>
<template
v-if="job.kind === 'external-file' && job.taskType && job.taskType !== job.text"
>
<span class="min-w-0 truncate text-primary">{{ job.taskType }}</span>
<BulletDivider class="shrink-0 text-primary" />
</template>
<template v-if="complete && job.finishedAt">
<time :datetime="job.finishedAt" class="shrink-0">
{{ formatRelativeTime(job.finishedAt) }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const messages = defineMessages({
defaultMessage: 'Clear all completed tasks',
},
complete: { id: 'app.download-manager.complete', defaultMessage: 'Complete' },
empty: { id: 'app.download-manager.empty', defaultMessage: 'No installation tasks' },
empty: { id: 'app.download-manager.no-tasks', defaultMessage: 'No tasks' },
rate: { id: 'app.download-manager.estimated-rate', defaultMessage: 'Estimated download speed' },
})
const panel = useTemplateRef('panel')
Expand Down
Loading
Loading