diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 8b4d00f42..a4bc69aaf 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -79,12 +79,11 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' import RunningAppBar from '@/components/ui/RunningAppBar.vue' import SplashScreen from '@/components/ui/SplashScreen.vue' -import URLConfirmModal from '@/components/ui/URLConfirmModal.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js' import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics' 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 { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts' import { create_profile_and_install_from_file } from '@/helpers/pack' @@ -157,8 +156,6 @@ const { const news = ref([]) const availableSurvey = ref(false) -const urlModal = ref(null) - const offline = ref(!navigator.onLine) window.addEventListener('offline', () => { offline.value = true @@ -421,6 +418,7 @@ const { preferredLoader: contentInstallPreferredLoader, preferredGameVersion: contentInstallPreferredGameVersion, releaseGameVersions: contentInstallReleaseGameVersions, + projectInfo: contentInstallProjectInfo, handleInstallToInstance, handleCreateAndInstall, handleNavigate: handleContentInstallNavigate, @@ -436,6 +434,7 @@ const { setInstallToPlayModal: setServerInstallToPlayModal, setUpdateToPlayModal: setServerUpdateToPlayModal, setAddServerToInstanceModal: setServerAddServerToInstanceModal, + playServerProject, } = serverInstall const modInstallModal = ref() @@ -545,9 +544,19 @@ async function handleCommand(e) { } else if (e.event === 'InstallServer') { await router.push(`/project/${e.id}`) 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 { - // Other commands are URL-based (deep linking) - urlModal.value.show(e) + await contentInstall + .install(e.id, null, null, 'URLConfirmModal', undefined, undefined, { showProjectInfo: true }) + .catch(handleError) } } @@ -1265,7 +1274,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) - @@ -1281,6 +1289,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) :preferred-loader="contentInstallPreferredLoader" :preferred-game-version="contentInstallPreferredGameVersion" :release-game-versions="contentInstallReleaseGameVersions" + :project-info="contentInstallProjectInfo" @install="handleInstallToInstance" @create-and-install="handleCreateAndInstall" @navigate="handleContentInstallNavigate" diff --git a/apps/app-frontend/src/providers/content-install.ts b/apps/app-frontend/src/providers/content-install.ts index 36dba7e43..463eafc25 100644 --- a/apps/app-frontend/src/providers/content-install.ts +++ b/apps/app-frontend/src/providers/content-install.ts @@ -1,13 +1,20 @@ 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 { convertFileSrc } from '@tauri-apps/api/core' +import { openUrl } from '@tauri-apps/plugin-opener' import dayjs from 'dayjs' import { nextTick, type Ref, ref } from 'vue' import type { Router } from 'vue-router' 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 { add_project_from_version, @@ -73,6 +80,7 @@ export interface ContentInstallContext { preferredLoader: Ref preferredGameVersion: Ref releaseGameVersions: Ref> + projectInfo: Ref handleInstallToInstance: (instance: ContentInstallInstance) => Promise handleCreateAndInstall: (data: { name: string @@ -93,7 +101,7 @@ export interface ContentInstallContext { source?: string, callback?: (versionId?: string) => void, createInstanceCallback?: (profile: string) => void, - hints?: { preferredLoader?: string; preferredGameVersion?: string }, + hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean }, ) => Promise installingItems: Ref> } @@ -116,6 +124,7 @@ export function createContentInstall(opts: { const preferredGameVersion = ref(null) const releaseGameVersions = ref>(new Set()) + const projectInfo = ref(null) const installingItems = ref>(new Map()) function addInstallingItem( @@ -194,6 +203,58 @@ export function createContentInstall(opts: { instances.value = [] 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() const gameVersionSet = new Set() for (const v of versions) { @@ -402,7 +463,7 @@ export function createContentInstall(opts: { source: string = 'unknown', callback: (versionId?: 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') @@ -508,6 +569,7 @@ export function createContentInstall(opts: { preferredLoader, preferredGameVersion, releaseGameVersions, + projectInfo, handleInstallToInstance, handleCreateAndInstall, handleNavigate, diff --git a/apps/app-frontend/src/providers/server-install.ts b/apps/app-frontend/src/providers/server-install.ts index 2bc155899..ed7f50761 100644 --- a/apps/app-frontend/src/providers/server-install.ts +++ b/apps/app-frontend/src/providers/server-install.ts @@ -46,11 +46,24 @@ export interface ServerInstallContext { showAddServerToInstanceModal: (serverName: string, serverAddress: string) => void } -export const [injectServerInstall, provideServerInstall] = createContext( +let _serverInstallSingleton: ServerInstallContext | null = null + +const [_rawInjectServerInstall, provideServerInstall] = createContext( 'root', '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: { router: Router handleError: (err: unknown) => void @@ -345,7 +358,7 @@ export function createServerInstall(opts: { } } - return { + const context: ServerInstallContext = { installingServerProjects, startInstallingServer, stopInstallingServer, @@ -365,4 +378,7 @@ export function createServerInstall(opts: { addServerToInstanceModalRef?.show(serverName, serverAddress) }, } + + _serverInstallSingleton = context + return context } diff --git a/packages/ui/src/layouts/shared/content-tab/components/modals/ContentInstallModal.vue b/packages/ui/src/layouts/shared/content-tab/components/modals/ContentInstallModal.vue index eae3a6945..3a2de5772 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/modals/ContentInstallModal.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/modals/ContentInstallModal.vue @@ -6,6 +6,45 @@ +
+ +
+ +
+
+
+ + {{ projectInfo.title }} + +
+ + + {{ projectInfo.owner.name }} + +
+
+
+
{{ formatMessage(messages.instanceType) }} @@ -34,8 +73,8 @@ class="!border-surface-4 !border" @click="hideUninstallable = !hideUninstallable" > - - + +
@@ -212,6 +251,7 @@ import { } from '@modrinth/assets' import { computed, ref } from 'vue' +import AutoLink from '#ui/components/base/AutoLink.vue' import Avatar from '#ui/components/base/Avatar.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import Chips from '#ui/components/base/Chips.vue' @@ -314,6 +354,20 @@ export interface ContentInstallInstance { 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<{ instances: ContentInstallInstance[] compatibleLoaders: string[] @@ -323,6 +377,7 @@ const props = defineProps<{ defaultTab?: 'existing' | 'new' preferredLoader?: string | null preferredGameVersion?: string | null + projectInfo?: ContentInstallProjectInfo | null }>() const emit = defineEmits<{ diff --git a/packages/ui/src/layouts/shared/content-tab/index.ts b/packages/ui/src/layouts/shared/content-tab/index.ts index 6fdd2c612..e2085b11d 100644 --- a/packages/ui/src/layouts/shared/content-tab/index.ts +++ b/packages/ui/src/layouts/shared/content-tab/index.ts @@ -9,7 +9,11 @@ export { default as ConfirmModpackUpdateModal } from './components/modals/Confir export { default as ConfirmReinstallModal } from './components/modals/ConfirmReinstallModal.vue' export { default as ConfirmRepairModal } from './components/modals/ConfirmRepairModal.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 ContentUpdaterModal } from './components/modals/ContentUpdaterModal.vue' export type { ModpackContentModalState } from './components/modals/ModpackContentModal.vue' diff --git a/packages/ui/src/providers/create-context.ts b/packages/ui/src/providers/create-context.ts index e93f24d4c..befc1b74d 100644 --- a/packages/ui/src/providers/create-context.ts +++ b/packages/ui/src/providers/create-context.ts @@ -43,7 +43,9 @@ export function createContext( ? `${providerComponentName}Context` : contextName - const injectionKey: InjectionKey = Symbol(symbolDescription) + const injectionKey: InjectionKey = Symbol.for( + `modrinth:${symbolDescription}`, + ) /** * @param fallback The context value to return if the injection fails.