diff --git a/.github/workflows/theseus-build.yml b/.github/workflows/theseus-build.yml
index d4903a4cbbd..6e8aa5a298f 100644
--- a/.github/workflows/theseus-build.yml
+++ b/.github/workflows/theseus-build.yml
@@ -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
@@ -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 }}
@@ -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')
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index e9b449e7253..283e3f05788 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -27,6 +27,8 @@ import {
commonSettingsMessages,
ContentInstallModal,
ContentUpdaterModal,
+ createServerInviteHandoff,
+ createServerOnboardingFlow,
CreationFlowModal,
defineMessages,
I18nDebugPanel,
@@ -34,12 +36,18 @@ import {
LoadingBar,
NotificationPanel,
PopupNotificationPanel,
+ provideFileDownload,
+ provideIconCache,
provideLoadingState,
provideModalBehavior,
provideModrinthClient,
provideNotificationManager,
providePageContext,
providePopupNotificationManager,
+ provideServerInviteHandoff,
+ provideServerOnboardingFlow,
+ provideServerPlay,
+ ServerOnboardingModal,
TeleportOverflowMenu,
TooltipDirective,
useDebugLogger,
@@ -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'
@@ -88,6 +97,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 AppSettingsModal from '@/features/settings/app-settings-modal/index.vue'
import { provideSettingsActions } from '@/features/settings/settings-actions'
@@ -157,6 +172,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,
@@ -283,6 +300,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(),
@@ -804,8 +827,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))
@@ -829,42 +858,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()
@@ -954,6 +960,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()
@@ -1539,6 +1553,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') {
@@ -2005,6 +2021,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@create="handleCreate"
@browse-modpacks="handleBrowseModpacks"
/>
+
+
diff --git a/apps/app-frontend/src/app/runtime/use-route-loading.ts b/apps/app-frontend/src/app/runtime/use-route-loading.ts
index b4af0504bc2..9e21984a0f6 100644
--- a/apps/app-frontend/src/app/runtime/use-route-loading.ts
+++ b/apps/app-frontend/src/app/runtime/use-route-loading.ts
@@ -17,6 +17,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
@@ -83,6 +84,7 @@ export function useRouteLoading({ stateInitialized, stateFailed }: RouteLoadingO
})
function onSuspensePending() {
+ onboardingPageReady.value = false
debugStartup('Route Suspense pending', { route: route.path })
suspensePending = true
viewportScroll.onRoutePending()
@@ -91,6 +93,7 @@ export function useRouteLoading({ stateInitialized, stateFailed }: RouteLoadingO
}
function onSuspenseResolve() {
+ onboardingPageReady.value = true
debugStartup('Route Suspense resolved', { route: route.path })
viewportScroll.onRouteResolved()
if (suspenseToken) {
@@ -120,5 +123,12 @@ export function useRouteLoading({ stateInitialized, stateFailed }: RouteLoadingO
}
})
- return { loading, canNavigateBack, canNavigateForward, onSuspensePending, onSuspenseResolve }
+ return {
+ loading,
+ canNavigateBack,
+ canNavigateForward,
+ onboardingPageReady,
+ onSuspensePending,
+ onSuspenseResolve,
+ }
}
diff --git a/apps/app-frontend/src/components/ui/HostingUpdateRequired.vue b/apps/app-frontend/src/components/ui/HostingUpdateRequired.vue
index 465b8b8221c..5f94713dd4c 100644
--- a/apps/app-frontend/src/components/ui/HostingUpdateRequired.vue
+++ b/apps/app-frontend/src/components/ui/HostingUpdateRequired.vue
@@ -1,6 +1,6 @@
-
-
-
-
-
-
-
- {{ formatMessage(messages.background) }}
-
-
-
-
-
-
- {{ formatMessage(messages.symbol) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ formatMessage(messages.description) }}
-
-
-
-
-
-
-
-
+
-
-
diff --git a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
index 068e33338be..19c8fd955a3 100644
--- a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
+++ b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue
@@ -7,10 +7,6 @@
width="544px"
>
-
- {{ formatMessage(messages.inviteWarning) }}
-
-
@@ -401,11 +397,6 @@ const messages = defineMessages({
id: 'app.modal.install-to-play.header',
defaultMessage: 'Install to play',
},
- inviteWarning: {
- id: 'app.modal.install-to-play.invite-warning',
- defaultMessage:
- 'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.',
- },
sharedInstance: {
id: 'app.modal.install-to-play.shared-instance',
defaultMessage: 'Shared instance',
diff --git a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue
index e391474d311..4d0ea51af13 100644
--- a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue
+++ b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue
@@ -6,7 +6,7 @@
? formatMessage(messages.reportSharedInstance)
: formatMessage(messages.installToPlay)
"
- :closable="!submitLoading"
+ :closable="!submitLoading && !installing"
:on-hide="handleHide"
:max-width="reportMode ? '816px' : '544px'"
:width="reportMode ? '816px' : '544px'"
@@ -193,23 +193,23 @@
{{ formatMessage(messages.reviewedFiles) }}
-