fix: deeplink modal use new modal & DI stability (#5577)

* fix: deeplink

* feat: DI stability

* fix: lint

* fix: play server project deep link

* switch toggle icons

* pnpm prepr

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
This commit is contained in:
Calum H.
2026-03-16 17:10:55 +00:00
committed by GitHub
parent 7d3935a38d
commit d9c7608ade
6 changed files with 165 additions and 17 deletions

View File

@@ -79,12 +79,11 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue' import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue' import SplashScreen from '@/components/ui/SplashScreen.vue'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js' import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics' import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js' import { check_reachable } from '@/helpers/auth.js'
import { get_user } from '@/helpers/cache.js' import { get_user, get_version } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js' import { command_listener, warning_listener } from '@/helpers/events.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts' import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack' import { create_profile_and_install_from_file } from '@/helpers/pack'
@@ -157,8 +156,6 @@ const {
const news = ref([]) const news = ref([])
const availableSurvey = ref(false) const availableSurvey = ref(false)
const urlModal = ref(null)
const offline = ref(!navigator.onLine) const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => { window.addEventListener('offline', () => {
offline.value = true offline.value = true
@@ -421,6 +418,7 @@ const {
preferredLoader: contentInstallPreferredLoader, preferredLoader: contentInstallPreferredLoader,
preferredGameVersion: contentInstallPreferredGameVersion, preferredGameVersion: contentInstallPreferredGameVersion,
releaseGameVersions: contentInstallReleaseGameVersions, releaseGameVersions: contentInstallReleaseGameVersions,
projectInfo: contentInstallProjectInfo,
handleInstallToInstance, handleInstallToInstance,
handleCreateAndInstall, handleCreateAndInstall,
handleNavigate: handleContentInstallNavigate, handleNavigate: handleContentInstallNavigate,
@@ -436,6 +434,7 @@ const {
setInstallToPlayModal: setServerInstallToPlayModal, setInstallToPlayModal: setServerInstallToPlayModal,
setUpdateToPlayModal: setServerUpdateToPlayModal, setUpdateToPlayModal: setServerUpdateToPlayModal,
setAddServerToInstanceModal: setServerAddServerToInstanceModal, setAddServerToInstanceModal: setServerAddServerToInstanceModal,
playServerProject,
} = serverInstall } = serverInstall
const modInstallModal = ref() const modInstallModal = ref()
@@ -545,9 +544,19 @@ async function handleCommand(e) {
} else if (e.event === 'InstallServer') { } else if (e.event === 'InstallServer') {
await router.push(`/project/${e.id}`) await router.push(`/project/${e.id}`)
await playServerProject(e.id).catch(handleError) await playServerProject(e.id).catch(handleError)
} else if (e.event === 'InstallVersion') {
const version = await get_version(e.id, 'must_revalidate').catch(handleError)
if (version) {
await contentInstall
.install(version.project_id, version.id, null, 'URLConfirmModal', undefined, undefined, {
showProjectInfo: true,
})
.catch(handleError)
}
} else { } else {
// Other commands are URL-based (deep linking) await contentInstall
urlModal.value.show(e) .install(e.id, null, null, 'URLConfirmModal', undefined, undefined, { showProjectInfo: true })
.catch(handleError)
} }
} }
@@ -1265,7 +1274,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
</template> </template>
</div> </div>
</div> </div>
<URLConfirmModal ref="urlModal" />
<I18nDebugPanel /> <I18nDebugPanel />
<NotificationPanel has-sidebar /> <NotificationPanel has-sidebar />
<PopupNotificationPanel has-sidebar /> <PopupNotificationPanel has-sidebar />
@@ -1281,6 +1289,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
:preferred-loader="contentInstallPreferredLoader" :preferred-loader="contentInstallPreferredLoader"
:preferred-game-version="contentInstallPreferredGameVersion" :preferred-game-version="contentInstallPreferredGameVersion"
:release-game-versions="contentInstallReleaseGameVersions" :release-game-versions="contentInstallReleaseGameVersions"
:project-info="contentInstallProjectInfo"
@install="handleInstallToInstance" @install="handleInstallToInstance"
@create-and-install="handleCreateAndInstall" @create-and-install="handleCreateAndInstall"
@navigate="handleContentInstallNavigate" @navigate="handleContentInstallNavigate"

View File

@@ -1,13 +1,20 @@
import type { Labrinth } from '@modrinth/api-client' import type { Labrinth } from '@modrinth/api-client'
import type { ContentInstallInstance, ContentItem } from '@modrinth/ui' import type { ContentInstallInstance, ContentInstallProjectInfo, ContentItem } from '@modrinth/ui'
import { createContext } from '@modrinth/ui' import { createContext } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { nextTick, type Ref, ref } from 'vue' import { nextTick, type Ref, ref } from 'vue'
import type { Router } from 'vue-router' import type { Router } from 'vue-router'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { get_project, get_project_v3_many, get_version_many } from '@/helpers/cache.js' import {
get_organization,
get_project,
get_project_v3_many,
get_team,
get_version_many,
} from '@/helpers/cache.js'
import { create_profile_and_install as packInstall } from '@/helpers/pack' import { create_profile_and_install as packInstall } from '@/helpers/pack'
import { import {
add_project_from_version, add_project_from_version,
@@ -73,6 +80,7 @@ export interface ContentInstallContext {
preferredLoader: Ref<string | null> preferredLoader: Ref<string | null>
preferredGameVersion: Ref<string | null> preferredGameVersion: Ref<string | null>
releaseGameVersions: Ref<Set<string>> releaseGameVersions: Ref<Set<string>>
projectInfo: Ref<ContentInstallProjectInfo | null>
handleInstallToInstance: (instance: ContentInstallInstance) => Promise<void> handleInstallToInstance: (instance: ContentInstallInstance) => Promise<void>
handleCreateAndInstall: (data: { handleCreateAndInstall: (data: {
name: string name: string
@@ -93,7 +101,7 @@ export interface ContentInstallContext {
source?: string, source?: string,
callback?: (versionId?: string) => void, callback?: (versionId?: string) => void,
createInstanceCallback?: (profile: string) => void, createInstanceCallback?: (profile: string) => void,
hints?: { preferredLoader?: string; preferredGameVersion?: string }, hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean },
) => Promise<void> ) => Promise<void>
installingItems: Ref<Map<string, ContentItem[]>> installingItems: Ref<Map<string, ContentItem[]>>
} }
@@ -116,6 +124,7 @@ export function createContentInstall(opts: {
const preferredGameVersion = ref<string | null>(null) const preferredGameVersion = ref<string | null>(null)
const releaseGameVersions = ref<Set<string>>(new Set()) const releaseGameVersions = ref<Set<string>>(new Set())
const projectInfo = ref<ContentInstallProjectInfo | null>(null)
const installingItems = ref<Map<string, ContentItem[]>>(new Map()) const installingItems = ref<Map<string, ContentItem[]>>(new Map())
function addInstallingItem( function addInstallingItem(
@@ -194,6 +203,58 @@ export function createContentInstall(opts: {
instances.value = [] instances.value = []
defaultTab.value = 'existing' defaultTab.value = 'existing'
if (hints?.showProjectInfo) {
projectInfo.value = {
title: project.title,
iconUrl: project.icon_url,
link: `/project/${project.slug ?? project.id}`,
}
if (project.organization) {
get_organization(project.organization)
.then((org: { id: string; slug: string; name: string; icon_url?: string }) => {
if (projectInfo.value) {
const orgSlug = org.slug ?? org.id
projectInfo.value = {
...projectInfo.value,
owner: {
name: org.name,
iconUrl: org.icon_url,
circle: false,
link: () => openUrl(`https://modrinth.com/organization/${orgSlug}`),
},
}
}
})
.catch(() => {})
} else if (project.team) {
get_team(project.team)
.then(
(
members: {
user: { id: string; username: string; avatar_url?: string }
is_owner: boolean
}[],
) => {
const owner = members.find((m) => m.is_owner)
if (owner && projectInfo.value) {
projectInfo.value = {
...projectInfo.value,
owner: {
name: owner.user.username,
iconUrl: owner.user.avatar_url,
circle: true,
link: () => openUrl(`https://modrinth.com/user/${owner.user.username}`),
},
}
}
},
)
.catch(() => {})
}
} else {
projectInfo.value = null
}
const loaderSet = new Set<string>() const loaderSet = new Set<string>()
const gameVersionSet = new Set<string>() const gameVersionSet = new Set<string>()
for (const v of versions) { for (const v of versions) {
@@ -402,7 +463,7 @@ export function createContentInstall(opts: {
source: string = 'unknown', source: string = 'unknown',
callback: (versionId?: string) => void = () => {}, callback: (versionId?: string) => void = () => {},
createInstanceCallback: (profile: string) => void = () => {}, createInstanceCallback: (profile: string) => void = () => {},
hints?: { preferredLoader?: string; preferredGameVersion?: string }, hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean },
) { ) {
const project: Labrinth.Projects.v2.Project = await get_project(projectId, 'must_revalidate') const project: Labrinth.Projects.v2.Project = await get_project(projectId, 'must_revalidate')
@@ -508,6 +569,7 @@ export function createContentInstall(opts: {
preferredLoader, preferredLoader,
preferredGameVersion, preferredGameVersion,
releaseGameVersions, releaseGameVersions,
projectInfo,
handleInstallToInstance, handleInstallToInstance,
handleCreateAndInstall, handleCreateAndInstall,
handleNavigate, handleNavigate,

View File

@@ -46,11 +46,24 @@ export interface ServerInstallContext {
showAddServerToInstanceModal: (serverName: string, serverAddress: string) => void showAddServerToInstanceModal: (serverName: string, serverAddress: string) => void
} }
export const [injectServerInstall, provideServerInstall] = createContext<ServerInstallContext>( let _serverInstallSingleton: ServerInstallContext | null = null
const [_rawInjectServerInstall, provideServerInstall] = createContext<ServerInstallContext>(
'root', 'root',
'serverInstall', 'serverInstall',
) )
export { provideServerInstall }
export function injectServerInstall(): ServerInstallContext {
try {
return _rawInjectServerInstall()
} catch {
if (_serverInstallSingleton) return _serverInstallSingleton
throw new Error('ServerInstall context not available')
}
}
export function createServerInstall(opts: { export function createServerInstall(opts: {
router: Router router: Router
handleError: (err: unknown) => void handleError: (err: unknown) => void
@@ -345,7 +358,7 @@ export function createServerInstall(opts: {
} }
} }
return { const context: ServerInstallContext = {
installingServerProjects, installingServerProjects,
startInstallingServer, startInstallingServer,
stopInstallingServer, stopInstallingServer,
@@ -365,4 +378,7 @@ export function createServerInstall(opts: {
addServerToInstanceModalRef?.show(serverName, serverAddress) addServerToInstanceModalRef?.show(serverName, serverAddress)
}, },
} }
_serverInstallSingleton = context
return context
} }

View File

@@ -6,6 +6,45 @@
</span> </span>
</template> </template>
<div
v-if="projectInfo"
class="flex items-center gap-2.5 rounded-[20px] bg-surface-2 mx-6 mt-6 p-3"
>
<AutoLink :to="projectInfo.link" class="shrink-0">
<div
class="size-14 shrink-0 overflow-hidden rounded-2xl border border-solid border-surface-5"
>
<Avatar
v-if="projectInfo.iconUrl"
:src="projectInfo.iconUrl"
:alt="projectInfo.title"
size="100%"
no-shadow
/>
</div>
</AutoLink>
<div class="flex flex-col gap-1">
<AutoLink :to="projectInfo.link" class="font-semibold text-contrast hover:underline">
{{ projectInfo.title }}
</AutoLink>
<div v-if="projectInfo.owner" class="flex items-center gap-2 text-sm text-secondary">
<AutoLink
:to="projectInfo.owner.link"
class="flex items-center gap-1.5 text-inherit no-underline hover:underline"
>
<Avatar
:src="projectInfo.owner.iconUrl"
:alt="projectInfo.owner.name"
size="1.25rem"
:circle="projectInfo.owner.circle"
no-shadow
/>
<span class="font-medium">{{ projectInfo.owner.name }}</span>
</AutoLink>
</div>
</div>
</div>
<div class="flex flex-col gap-2.5 p-6"> <div class="flex flex-col gap-2.5 p-6">
<span class="font-semibold text-contrast"> <span class="font-semibold text-contrast">
{{ formatMessage(messages.instanceType) }} {{ formatMessage(messages.instanceType) }}
@@ -34,8 +73,8 @@
class="!border-surface-4 !border" class="!border-surface-4 !border"
@click="hideUninstallable = !hideUninstallable" @click="hideUninstallable = !hideUninstallable"
> >
<EyeIcon v-if="hideUninstallable" /> <EyeOffIcon v-if="hideUninstallable" />
<EyeOffIcon v-else /> <EyeIcon v-else />
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </div>
@@ -212,6 +251,7 @@ import {
} from '@modrinth/assets' } from '@modrinth/assets'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import AutoLink from '#ui/components/base/AutoLink.vue'
import Avatar from '#ui/components/base/Avatar.vue' import Avatar from '#ui/components/base/Avatar.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import Chips from '#ui/components/base/Chips.vue' import Chips from '#ui/components/base/Chips.vue'
@@ -314,6 +354,20 @@ export interface ContentInstallInstance {
installing?: boolean installing?: boolean
} }
export interface ContentInstallProjectOwner {
name: string
iconUrl?: string
circle?: boolean
link: string | (() => void)
}
export interface ContentInstallProjectInfo {
title: string
iconUrl?: string | null
link: string
owner?: ContentInstallProjectOwner | null
}
const props = defineProps<{ const props = defineProps<{
instances: ContentInstallInstance[] instances: ContentInstallInstance[]
compatibleLoaders: string[] compatibleLoaders: string[]
@@ -323,6 +377,7 @@ const props = defineProps<{
defaultTab?: 'existing' | 'new' defaultTab?: 'existing' | 'new'
preferredLoader?: string | null preferredLoader?: string | null
preferredGameVersion?: string | null preferredGameVersion?: string | null
projectInfo?: ContentInstallProjectInfo | null
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -9,7 +9,11 @@ export { default as ConfirmModpackUpdateModal } from './components/modals/Confir
export { default as ConfirmReinstallModal } from './components/modals/ConfirmReinstallModal.vue' export { default as ConfirmReinstallModal } from './components/modals/ConfirmReinstallModal.vue'
export { default as ConfirmRepairModal } from './components/modals/ConfirmRepairModal.vue' export { default as ConfirmRepairModal } from './components/modals/ConfirmRepairModal.vue'
export { default as ConfirmUnlinkModal } from './components/modals/ConfirmUnlinkModal.vue' export { default as ConfirmUnlinkModal } from './components/modals/ConfirmUnlinkModal.vue'
export type { ContentInstallInstance } from './components/modals/ContentInstallModal.vue' export type {
ContentInstallInstance,
ContentInstallProjectInfo,
ContentInstallProjectOwner,
} from './components/modals/ContentInstallModal.vue'
export { default as ContentInstallModal } from './components/modals/ContentInstallModal.vue' export { default as ContentInstallModal } from './components/modals/ContentInstallModal.vue'
export { default as ContentUpdaterModal } from './components/modals/ContentUpdaterModal.vue' export { default as ContentUpdaterModal } from './components/modals/ContentUpdaterModal.vue'
export type { ModpackContentModalState } from './components/modals/ModpackContentModal.vue' export type { ModpackContentModalState } from './components/modals/ModpackContentModal.vue'

View File

@@ -43,7 +43,9 @@ export function createContext<ContextValue>(
? `${providerComponentName}Context` ? `${providerComponentName}Context`
: contextName : contextName
const injectionKey: InjectionKey<ContextValue | null> = Symbol(symbolDescription) const injectionKey: InjectionKey<ContextValue | null> = Symbol.for(
`modrinth:${symbolDescription}`,
)
/** /**
* @param fallback The context value to return if the injection fails. * @param fallback The context value to return if the injection fails.