From fe0c97190aa937c99cca487d6594edec49079d35 Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Tue, 4 Aug 2026 17:52:00 +0100 Subject: [PATCH] refactor: use DI for instance page + subpages (#6987) * refactor: use DI for instance page + subpages * fix: qa * refactor: layout.vue bring up a layer * refactor: rename folders * import order * refactor: move settings into instance page * fix: lint --------- Co-authored-by: tdgao --- apps/app-frontend/src/App.vue | 3 +- .../components/ui/QuickInstanceSwitcher.vue | 3 +- .../install_flow/AddServerToInstanceModal.vue | 3 +- .../browse/use-app-server-browse.ts | 5 +- .../src/helpers/instance-content.ts | 3 +- apps/app-frontend/src/pages/Browse.vue | 111 +-- .../src/pages/instance/Overview.vue | 13 - .../components/admonitions}/index.vue | 10 +- .../components/admonitions/messages.ts} | 0 .../admonitions/shared-instance-stale.vue} | 2 +- .../shared-instance-unavailable.vue} | 0 .../shared-instance-update-available.vue} | 2 +- .../shared-instance-wrong-account.vue} | 2 +- .../instance/components/admonitions}/types.ts | 0 .../components/page-header}/index.vue | 0 .../instance-page-header-server-metadata.vue | 0 ...m-revoke-shared-instance-invite-modal.vue} | 0 .../settings-modal/general-settings.vue} | 4 +- .../settings-modal/hooks-settings.vue} | 4 +- .../components/settings-modal/index.vue} | 16 +- .../settings-modal/installation-settings.vue} | 11 +- .../instance-settings-context.ts} | 0 .../settings-modal/java-settings.vue} | 4 +- ...stance-installation-settings-controls.vue} | 0 .../settings-modal/sharing-settings.vue} | 10 +- .../settings-modal/window-settings.vue} | 4 +- .../instance/{Mods.vue => content/index.vue} | 231 +++--- .../instance/{Files.vue => files/index.vue} | 101 ++- apps/app-frontend/src/pages/instance/index.js | 9 - apps/app-frontend/src/pages/instance/index.ts | 8 + .../src/pages/instance/instance-context.ts | 27 + .../pages/instance/{Index.vue => layout.vue} | 753 +++++++----------- .../instance/{Logs.vue => logs/index.vue} | 99 ++- .../src/pages/instance/query-options.ts | 79 ++ .../src/pages/instance/share/index.vue | 89 +-- .../shared-instance-management-context.ts | 19 + .../share/shared-instance-members-table.vue | 35 +- .../share/use-shared-instance-members.ts | 5 +- .../pages/instance/shared-instance-context.ts | 181 +++++ .../instance/use-shared-instance-state.ts | 228 ------ .../instance/{Worlds.vue => worlds/index.vue} | 64 +- apps/app-frontend/src/routes.js | 13 +- 42 files changed, 1009 insertions(+), 1142 deletions(-) delete mode 100644 apps/app-frontend/src/pages/instance/Overview.vue rename apps/app-frontend/src/{components/ui/instance/instance-admonitions => pages/instance/components/admonitions}/index.vue (91%) rename apps/app-frontend/src/{components/ui/instance/instance-admonitions/instance-admonitions-messages.ts => pages/instance/components/admonitions/messages.ts} (100%) rename apps/app-frontend/src/{components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-stale.vue => pages/instance/components/admonitions/shared-instance-stale.vue} (95%) rename apps/app-frontend/src/{components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-unavailable.vue => pages/instance/components/admonitions/shared-instance-unavailable.vue} (100%) rename apps/app-frontend/src/{components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue => pages/instance/components/admonitions/shared-instance-update-available.vue} (89%) rename apps/app-frontend/src/{components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-wrong-account.vue => pages/instance/components/admonitions/shared-instance-wrong-account.vue} (96%) rename apps/app-frontend/src/{components/ui/instance/instance-admonitions => pages/instance/components/admonitions}/types.ts (100%) rename apps/app-frontend/src/{components/ui/instance-page-header => pages/instance/components/page-header}/index.vue (100%) rename apps/app-frontend/src/{components/ui/instance-page-header => pages/instance/components/page-header}/instance-page-header-server-metadata.vue (100%) rename apps/app-frontend/src/{components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue => pages/instance/components/settings-modal/confirm-revoke-shared-instance-invite-modal.vue} (100%) rename apps/app-frontend/src/{components/ui/instance_settings/GeneralSettings.vue => pages/instance/components/settings-modal/general-settings.vue} (99%) rename apps/app-frontend/src/{components/ui/instance_settings/HooksSettings.vue => pages/instance/components/settings-modal/hooks-settings.vue} (96%) rename apps/app-frontend/src/{components/ui/modal/InstanceSettingsModal.vue => pages/instance/components/settings-modal/index.vue} (89%) rename apps/app-frontend/src/{components/ui/instance_settings/InstallationSettings.vue => pages/instance/components/settings-modal/installation-settings.vue} (97%) rename apps/app-frontend/src/{providers/instance-settings.ts => pages/instance/components/settings-modal/instance-settings-context.ts} (100%) rename apps/app-frontend/src/{components/ui/instance_settings/JavaSettings.vue => pages/instance/components/settings-modal/java-settings.vue} (98%) rename apps/app-frontend/src/{components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue => pages/instance/components/settings-modal/shared-instance-installation-settings-controls.vue} (100%) rename apps/app-frontend/src/{components/ui/instance_settings/SharingSettings.vue => pages/instance/components/settings-modal/sharing-settings.vue} (94%) rename apps/app-frontend/src/{components/ui/instance_settings/WindowSettings.vue => pages/instance/components/settings-modal/window-settings.vue} (97%) rename apps/app-frontend/src/pages/instance/{Mods.vue => content/index.vue} (89%) rename apps/app-frontend/src/pages/instance/{Files.vue => files/index.vue} (82%) delete mode 100644 apps/app-frontend/src/pages/instance/index.js create mode 100644 apps/app-frontend/src/pages/instance/index.ts create mode 100644 apps/app-frontend/src/pages/instance/instance-context.ts rename apps/app-frontend/src/pages/instance/{Index.vue => layout.vue} (57%) rename apps/app-frontend/src/pages/instance/{Logs.vue => logs/index.vue} (73%) create mode 100644 apps/app-frontend/src/pages/instance/query-options.ts create mode 100644 apps/app-frontend/src/pages/instance/share/shared-instance-management-context.ts create mode 100644 apps/app-frontend/src/pages/instance/shared-instance-context.ts delete mode 100644 apps/app-frontend/src/pages/instance/use-shared-instance-state.ts rename apps/app-frontend/src/pages/instance/{Worlds.vue => worlds/index.vue} (94%) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index e2f78fe965..3e766fcc8e 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -120,6 +120,7 @@ import { } from '@/helpers/utils.js' import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts' import i18n from '@/i18n.config' +import { instanceKeys } from '@/pages/instance/query-options' import { appUpdateState, downloadAvailableAppUpdate, @@ -242,7 +243,7 @@ const { data: authenticatedModrinthUser } = useQuery({ retry: false, }) useQuery({ - queryKey: computed(() => ['shared-instance-eligibility', credentials.value?.user?.id]), + queryKey: computed(() => instanceKeys.sharedEligibility(credentials.value?.user?.id)), queryFn: can_current_user_use_shared_instances, enabled: () => !!credentials.value?.session && !!credentials.value?.user?.id, retry: false, diff --git a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue index c1868332eb..9a60c3d435 100644 --- a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue +++ b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue @@ -9,6 +9,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import NavButton from '@/components/ui/NavButton.vue' import { instance_listener } from '@/helpers/events.js' import { list } from '@/helpers/instance' +import { instanceKeys } from '@/pages/instance/query-options' const ITEM_SIZE = 52 const APPROX_USED_VERTICAL_SPACE = 513 // doesn't need to be exact lol just close enough so there's a little gap and no overflow @@ -123,7 +124,7 @@ const getInstances = async () => { const instances = await list().catch(handleError) for (const instance of instances) { - queryClient.setQueryData(['instances', 'summary', instance.id], instance) + queryClient.setQueryData(instanceKeys.detail(instance.id), instance) } allInstances.value = instances.sort((a, b) => { diff --git a/apps/app-frontend/src/components/ui/install_flow/AddServerToInstanceModal.vue b/apps/app-frontend/src/components/ui/install_flow/AddServerToInstanceModal.vue index e3c523c8a1..17114ed83a 100644 --- a/apps/app-frontend/src/components/ui/install_flow/AddServerToInstanceModal.vue +++ b/apps/app-frontend/src/components/ui/install_flow/AddServerToInstanceModal.vue @@ -15,6 +15,7 @@ import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { trackEvent } from '@/helpers/analytics' import { list } from '@/helpers/instance' import { add_server_to_instance, get_instance_worlds } from '@/helpers/worlds.ts' +import { instanceKeys } from '@/pages/instance/query-options' const { handleError } = injectNotificationManager() const queryClient = useQueryClient() @@ -67,7 +68,7 @@ async function addServer(instance) { try { await add_server_to_instance(instance.id, serverName.value, serverAddress.value, 'prompt') instance.added = true - await queryClient.invalidateQueries({ queryKey: ['worlds', instance.id] }) + await queryClient.invalidateQueries({ queryKey: instanceKeys.worlds(instance.id) }) trackEvent('AddServerToInstance', { server_name: serverName.value, diff --git a/apps/app-frontend/src/composables/browse/use-app-server-browse.ts b/apps/app-frontend/src/composables/browse/use-app-server-browse.ts index addda23d9c..a789cd1033 100644 --- a/apps/app-frontend/src/composables/browse/use-app-server-browse.ts +++ b/apps/app-frontend/src/composables/browse/use-app-server-browse.ts @@ -17,6 +17,7 @@ import { kill, list as listInstances } from '@/helpers/instance' import { get_by_instance_id } from '@/helpers/process' import type { GameInstance } from '@/helpers/types' import { add_server_to_instance, getServerAddress } from '@/helpers/worlds' +import { instanceKeys } from '@/pages/instance/query-options' interface BrowseServerInstance { id: string @@ -38,7 +39,7 @@ interface ContextMenuOptionClick { } export interface UseAppServerBrowseOptions { - instance: Ref + instance: Readonly> isFromWorlds: ComputedRef allInstalledIds: ComputedRef> newlyInstalled: Ref @@ -131,7 +132,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) { project.minecraft_java_server?.content?.kind, ) options.newlyInstalled.value.push(project.project_id) - await queryClient.invalidateQueries({ queryKey: ['worlds', instanceId] }) + await queryClient.invalidateQueries({ queryKey: instanceKeys.worlds(instanceId) }) } catch (error) { options.handleError(error) } diff --git a/apps/app-frontend/src/helpers/instance-content.ts b/apps/app-frontend/src/helpers/instance-content.ts index f9f386febf..b294603c34 100644 --- a/apps/app-frontend/src/helpers/instance-content.ts +++ b/apps/app-frontend/src/helpers/instance-content.ts @@ -51,7 +51,8 @@ export async function loadInstanceContentData( } function handleLoadError(error: unknown, onError?: (error: Error) => unknown) { - onError?.(error as Error) + if (!onError) throw error + onError(error as Error) return null } diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue index 8f166430fd..d35a247544 100644 --- a/apps/app-frontend/src/pages/Browse.vue +++ b/apps/app-frontend/src/pages/Browse.vue @@ -32,7 +32,7 @@ import { useDebugLogger, useVIntl, } from '@modrinth/ui' -import { useQueryClient } from '@tanstack/vue-query' +import { useQuery, useQueryClient } from '@tanstack/vue-query' import { convertFileSrc } from '@tauri-apps/api/core' import type { Ref } from 'vue' import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue' @@ -41,15 +41,9 @@ import { useRoute, useRouter } from 'vue-router' import ContextMenu from '@/components/ui/ContextMenu.vue' import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse' -import { - get_project, - get_project_v3, - get_search_results_v3, - get_version_many, -} from '@/helpers/cache.js' +import { get_project, get_search_results_v3, get_version_many } from '@/helpers/cache.js' import { instance_listener } from '@/helpers/events.js' import { - get as getInstance, get_installed_project_ids as getInstalledProjectIds, list as listInstances, } from '@/helpers/instance' @@ -57,6 +51,11 @@ import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata' import { get as getSettings, set as setSettings } from '@/helpers/settings.ts' import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags' import { get_instance_worlds } from '@/helpers/worlds' +import { + instanceDetailQueryOptions, + instanceKeys, + instanceLinkedProjectQueryOptions, +} from '@/pages/instance/query-options' import { type BreadcrumbDefinition, useBreadcrumb, @@ -154,30 +153,29 @@ const { markServerProjectInstalled, } = serverInstallContent -type Instance = { - game_version: string - loader: string - path: string - install_stage: string - icon_path?: string - name: string - link?: { - type: string - project_id: string - version_id: string - } -} - -const initialInstanceId = String(route.query.i ?? '') -const instance: Ref = ref( - queryClient.getQueryData(['instances', 'summary', initialInstanceId]) ?? null, +const initialInstanceId = computed(() => String(route.query.i ?? '')) +const instanceQuery = useQuery( + computed(() => ({ + ...instanceDetailQueryOptions(initialInstanceId.value), + enabled: !!initialInstanceId.value, + })), +) +const instance = computed(() => instanceQuery.data.value ?? null) +const linkedInstanceProjectId = computed(() => instance.value?.link?.project_id ?? '') +const linkedInstanceProjectQuery = useQuery( + computed(() => ({ + ...instanceLinkedProjectQueryOptions(linkedInstanceProjectId.value), + enabled: !!linkedInstanceProjectId.value, + })), ) const installedProjectIds: Ref = ref(null) const instanceHideInstalled = ref(route.query.ai === 'true') const newlyInstalled = ref([]) const hiddenInstanceProjectIds = ref>(new Set()) const hiddenInstanceProjectIdsInitialized = ref(false) -const isServerInstance = ref(false) +const isServerInstance = computed( + () => linkedInstanceProjectQuery.data.value?.minecraft_server != null, +) const instanceBreadcrumb = route.query.i ? useBreadcrumb({ @@ -291,7 +289,13 @@ await initInstanceContext() async function refreshInstalledProjectIds() { if (!route.query.i) { - const instances = await listInstances().catch(handleError) + const instances = await queryClient + .fetchQuery({ + queryKey: [...instanceKeys.all, 'installed-project-ids'], + queryFn: listInstances, + staleTime: 0, + }) + .catch(handleError) if (!instances) return const ids = instances @@ -303,7 +307,14 @@ async function refreshInstalledProjectIds() { } if (route.query.from === 'worlds') { - const worlds = await get_instance_worlds(route.query.i as string).catch(handleError) + const targetInstanceId = route.query.i as string + const worlds = await queryClient + .fetchQuery({ + queryKey: instanceKeys.installedProjectIds(targetInstanceId, 'worlds'), + queryFn: () => get_instance_worlds(targetInstanceId), + staleTime: 0, + }) + .catch(handleError) if (!worlds) return const serverProjectIds = worlds @@ -314,7 +325,14 @@ async function refreshInstalledProjectIds() { return } - const ids = await getInstalledProjectIds(route.query.i as string).catch(handleError) + const targetInstanceId = route.query.i as string + const ids = await queryClient + .fetchQuery({ + queryKey: instanceKeys.installedProjectIds(targetInstanceId, 'content'), + queryFn: () => getInstalledProjectIds(targetInstanceId), + staleTime: 0, + }) + .catch(handleError) if (!ids) return debugLog('installedProjectIds loaded', { count: ids.length }) @@ -329,11 +347,13 @@ async function initInstanceContext() { queryWid: route.query.wid, queryFrom: route.query.from, }) - await initServerContext() - await refreshInstalledProjectIds() + await Promise.all([ + initServerContext(), + refreshInstalledProjectIds(), + route.query.i ? instanceQuery.suspense().catch(handleError) : Promise.resolve(), + ]) if (route.query.i) { - instance.value = (await getInstance(route.query.i as string).catch(handleError)) ?? null debugLog('instance loaded', { name: instance.value?.name, loader: instance.value?.loader, @@ -341,15 +361,7 @@ async function initInstanceContext() { }) if (instance.value?.link?.project_id) { - debugLog('checking linked project for server status', instance.value.link.project_id) - const projectV3 = await get_project_v3( - instance.value.link.project_id, - 'must_revalidate', - ).catch(handleError) - if (projectV3?.minecraft_server != null) { - debugLog('instance is a server instance') - isServerInstance.value = true - } + await linkedInstanceProjectQuery.suspense().catch(handleError) } } } @@ -577,16 +589,12 @@ const messages = defineMessages({ const projectType = ref(route.params.projectType as ProjectType) function resetInstanceContext() { - if (!instance.value) return - debugLog('instance context removed, resetting') - instance.value = null installedProjectIds.value = null instanceHideInstalled.value = false newlyInstalled.value = [] hiddenInstanceProjectIds.value = new Set() hiddenInstanceProjectIdsInitialized.value = false - isServerInstance.value = false browseBreadcrumb.reset() void refreshInstalledProjectIds() } @@ -611,9 +619,18 @@ watch( watch( () => route.query.i, - (instanceId) => { - if (!instanceId && route.path.startsWith('/browse')) { + async (nextInstanceId, previousInstanceId) => { + if (!route.path.startsWith('/browse') || nextInstanceId === previousInstanceId) return + if (!nextInstanceId) { resetInstanceContext() + return + } + + installedProjectIds.value = null + hiddenInstanceProjectIdsInitialized.value = false + await Promise.all([instanceQuery.suspense().catch(handleError), refreshInstalledProjectIds()]) + if (instance.value?.link?.project_id) { + await linkedInstanceProjectQuery.suspense().catch(handleError) } }, ) diff --git a/apps/app-frontend/src/pages/instance/Overview.vue b/apps/app-frontend/src/pages/instance/Overview.vue deleted file mode 100644 index 1221d27175..0000000000 --- a/apps/app-frontend/src/pages/instance/Overview.vue +++ /dev/null @@ -1,13 +0,0 @@ - - diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/index.vue b/apps/app-frontend/src/pages/instance/components/admonitions/index.vue similarity index 91% rename from apps/app-frontend/src/components/ui/instance/instance-admonitions/index.vue rename to apps/app-frontend/src/pages/instance/components/admonitions/index.vue index e16171bc5b..5e4d996ad6 100644 --- a/apps/app-frontend/src/components/ui/instance/instance-admonitions/index.vue +++ b/apps/app-frontend/src/pages/instance/components/admonitions/index.vue @@ -36,11 +36,11 @@ import { computed, ref, watch } from 'vue' import type { SharedInstanceUnavailableReason } from '@/helpers/install' import type { GameInstance } from '@/helpers/types' -import InstanceAdmonitionsSharedInstanceStale from './instance-admonitions-shared-instance-stale.vue' -import InstanceAdmonitionsSharedInstanceUnavailable from './instance-admonitions-shared-instance-unavailable.vue' -import InstanceAdmonitionsSharedInstanceUpdateAvailable from './instance-admonitions-shared-instance-update-available.vue' -import InstanceAdmonitionsSharedInstanceWrongAccount from './instance-admonitions-shared-instance-wrong-account.vue' -import type { InstanceAdmonitionItem, SharedInstanceRole } from './types' +import InstanceAdmonitionsSharedInstanceStale from './shared-instance-stale.vue' +import InstanceAdmonitionsSharedInstanceUnavailable from './shared-instance-unavailable.vue' +import InstanceAdmonitionsSharedInstanceUpdateAvailable from './shared-instance-update-available.vue' +import InstanceAdmonitionsSharedInstanceWrongAccount from './shared-instance-wrong-account.vue' +import type { InstanceAdmonitionItem, SharedInstanceRole } from './types.ts' defineOptions({ inheritAttrs: false, diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-messages.ts b/apps/app-frontend/src/pages/instance/components/admonitions/messages.ts similarity index 100% rename from apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-messages.ts rename to apps/app-frontend/src/pages/instance/components/admonitions/messages.ts diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-stale.vue b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-stale.vue similarity index 95% rename from apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-stale.vue rename to apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-stale.vue index e437bfe1cf..30a92c9044 100644 --- a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-stale.vue +++ b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-stale.vue @@ -42,7 +42,7 @@ import { computed, ref } from 'vue' import SharedInstancePublishModal from '@/components/ui/shared-instances/SharedInstancePublishModal.vue' import type { GameInstance } from '@/helpers/types' -import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages' +import { instanceAdmonitionsMessages as messages } from './messages' defineProps<{ instance: GameInstance diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-unavailable.vue b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-unavailable.vue similarity index 100% rename from apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-unavailable.vue rename to apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-unavailable.vue diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-update-available.vue similarity index 89% rename from apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue rename to apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-update-available.vue index ffb96f4329..172b1227c9 100644 --- a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-update-available.vue +++ b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-update-available.vue @@ -20,7 +20,7 @@ import { DownloadIcon } from '@modrinth/assets' import { Admonition, ButtonStyled, useVIntl } from '@modrinth/ui' -import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages' +import { instanceAdmonitionsMessages as messages } from './messages' defineProps<{ instanceName: string diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-wrong-account.vue b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-wrong-account.vue similarity index 96% rename from apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-wrong-account.vue rename to apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-wrong-account.vue index aab1f2fa8f..4582622772 100644 --- a/apps/app-frontend/src/components/ui/instance/instance-admonitions/instance-admonitions-shared-instance-wrong-account.vue +++ b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-wrong-account.vue @@ -29,7 +29,7 @@ import { computed } from 'vue' import { get_user } from '@/helpers/cache' -import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages' +import { instanceAdmonitionsMessages as messages } from './messages' import type { SharedInstanceRole } from './types' const props = defineProps<{ diff --git a/apps/app-frontend/src/components/ui/instance/instance-admonitions/types.ts b/apps/app-frontend/src/pages/instance/components/admonitions/types.ts similarity index 100% rename from apps/app-frontend/src/components/ui/instance/instance-admonitions/types.ts rename to apps/app-frontend/src/pages/instance/components/admonitions/types.ts diff --git a/apps/app-frontend/src/components/ui/instance-page-header/index.vue b/apps/app-frontend/src/pages/instance/components/page-header/index.vue similarity index 100% rename from apps/app-frontend/src/components/ui/instance-page-header/index.vue rename to apps/app-frontend/src/pages/instance/components/page-header/index.vue diff --git a/apps/app-frontend/src/components/ui/instance-page-header/instance-page-header-server-metadata.vue b/apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue similarity index 100% rename from apps/app-frontend/src/components/ui/instance-page-header/instance-page-header-server-metadata.vue rename to apps/app-frontend/src/pages/instance/components/page-header/instance-page-header-server-metadata.vue diff --git a/apps/app-frontend/src/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/confirm-revoke-shared-instance-invite-modal.vue similarity index 100% rename from apps/app-frontend/src/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/confirm-revoke-shared-instance-invite-modal.vue diff --git a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/general-settings.vue similarity index 99% rename from apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/general-settings.vue index 8568ebefa6..ef6bec1319 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/general-settings.vue @@ -21,9 +21,9 @@ import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInsta import { trackEvent } from '@/helpers/analytics' import { install_duplicate_instance } from '@/helpers/install' import { edit, edit_icon, list, remove } from '@/helpers/instance' -import { injectInstanceSettings } from '@/providers/instance-settings' -import type { GameInstance } from '../../../helpers/types' +import type { GameInstance } from '../../../../helpers/types' +import { injectInstanceSettings } from './instance-settings-context' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() diff --git a/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/hooks-settings.vue similarity index 96% rename from apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/hooks-settings.vue index bfd016d940..6af96227b2 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/hooks-settings.vue @@ -10,9 +10,9 @@ import { computed, ref, watch } from 'vue' import { edit } from '@/helpers/instance' import { get } from '@/helpers/settings.ts' -import { injectInstanceSettings } from '@/providers/instance-settings' -import type { AppSettings, Hooks } from '../../../helpers/types' +import type { AppSettings, Hooks } from '../../../../helpers/types' +import { injectInstanceSettings } from './instance-settings-context' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() diff --git a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/index.vue similarity index 89% rename from apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/index.vue index 800cac656b..c979e793c5 100644 --- a/apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/index.vue @@ -22,19 +22,19 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query' import { convertFileSrc } from '@tauri-apps/api/core' import { computed, nextTick, ref, watch } from 'vue' -import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue' -import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue' -import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue' -import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue' -import SharingSettings from '@/components/ui/instance_settings/SharingSettings.vue' -import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue' import { get_project_v3 } from '@/helpers/cache' import { get_linked_modpack_info } from '@/helpers/instance' import { get_loader_versions } from '@/helpers/metadata' import { get_game_versions, get_loaders } from '@/helpers/tags' -import { provideInstanceSettings } from '@/providers/instance-settings' +import type { GameInstance } from '@/helpers/types' -import type { GameInstance } from '../../../helpers/types' +import GeneralSettings from './general-settings.vue' +import HooksSettings from './hooks-settings.vue' +import InstallationSettings from './installation-settings.vue' +import { provideInstanceSettings } from './instance-settings-context.ts' +import JavaSettings from './java-settings.vue' +import SharingSettings from './sharing-settings.vue' +import WindowSettings from './window-settings.vue' const { formatMessage } = useVIntl() const queryClient = useQueryClient() diff --git a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/installation-settings.vue similarity index 97% rename from apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/installation-settings.vue index fe210e165f..b415d92757 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/installation-settings.vue @@ -15,7 +15,6 @@ import type { GameVersionTag, PlatformTag } from '@modrinth/utils' import { useQuery, useQueryClient } from '@tanstack/vue-query' import { computed, ref } from 'vue' -import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue' import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy' import { trackEvent } from '@/helpers/analytics' import { get_project_versions, get_version } from '@/helpers/cache' @@ -34,10 +33,12 @@ import { import { get_loader_versions } from '@/helpers/metadata' import { get_game_versions, get_loaders } from '@/helpers/tags' import { provideInstanceBackup } from '@/providers/instance-backup' -import { injectInstanceSettings } from '@/providers/instance-settings' import { useTheming } from '@/store/state' -import type { Manifest } from '../../../helpers/types' +import type { Manifest } from '../../../../helpers/types' +import { instanceKeys } from '../../query-options.ts' +import { injectInstanceSettings } from './instance-settings-context.ts' +import SharedInstanceInstallationSettingsControls from './shared-instance-installation-settings-controls.vue' const { handleError } = injectNotificationManager() const filePicker = injectFilePicker() @@ -148,7 +149,9 @@ async function unlinkSharedInstance() { unlinkingSharedInstance.value = true try { await unlink_shared_instance(instance.value.id) - await queryClient.invalidateQueries({ queryKey: ['sharedInstanceUsers', instance.value.id] }) + await queryClient.invalidateQueries({ + queryKey: instanceKeys.sharedMembers(instance.value.id), + }) await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] }) onUnlinked() } catch (error) { diff --git a/apps/app-frontend/src/providers/instance-settings.ts b/apps/app-frontend/src/pages/instance/components/settings-modal/instance-settings-context.ts similarity index 100% rename from apps/app-frontend/src/providers/instance-settings.ts rename to apps/app-frontend/src/pages/instance/components/settings-modal/instance-settings-context.ts diff --git a/apps/app-frontend/src/components/ui/instance_settings/JavaSettings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue similarity index 98% rename from apps/app-frontend/src/components/ui/instance_settings/JavaSettings.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue index 71e4449937..b73d848c87 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/JavaSettings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue @@ -25,9 +25,9 @@ import useJavaTest from '@/composables/useJavaTest' import useMemorySlider from '@/composables/useMemorySlider' import { edit, get_optimal_jre_key } from '@/helpers/instance' import { get } from '@/helpers/settings.ts' -import { injectInstanceSettings } from '@/providers/instance-settings' -import type { AppSettings } from '../../../helpers/types' +import type { AppSettings } from '../../../../helpers/types' +import { injectInstanceSettings } from './instance-settings-context' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() diff --git a/apps/app-frontend/src/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/shared-instance-installation-settings-controls.vue similarity index 100% rename from apps/app-frontend/src/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/shared-instance-installation-settings-controls.vue diff --git a/apps/app-frontend/src/components/ui/instance_settings/SharingSettings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/sharing-settings.vue similarity index 94% rename from apps/app-frontend/src/components/ui/instance_settings/SharingSettings.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/sharing-settings.vue index c34a6e0b62..c2c481889f 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/SharingSettings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/sharing-settings.vue @@ -94,8 +94,6 @@ import { import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' import { computed, ref } from 'vue' -import ConfirmRevokeSharedInstanceInviteModal from '@/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue' -import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue' import { config } from '@/config' import { get_shared_instance_invites, @@ -104,7 +102,11 @@ import { unpublish_shared_instance, } from '@/helpers/instance' import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors' -import { injectInstanceSettings } from '@/providers/instance-settings' + +import { instanceKeys } from '../../query-options.ts' +import ConfirmRevokeSharedInstanceInviteModal from './confirm-revoke-shared-instance-invite-modal.vue' +import { injectInstanceSettings } from './instance-settings-context.ts' +import SharedInstanceInstallationSettingsControls from './shared-instance-installation-settings-controls.vue' const { instance, offline, onUnlinked } = injectInstanceSettings() const { notifySharedInstanceError } = useSharedInstanceErrors() @@ -193,7 +195,7 @@ async function unpublishSharedInstance() { unpublishing.value = true try { await unpublish_shared_instance(instance.value.id) - queryClient.setQueryData(['sharedInstanceUsers', instance.value.id], []) + queryClient.setQueryData(instanceKeys.sharedMembers(instance.value.id), []) await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] }) onUnlinked() } catch (error) { diff --git a/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/window-settings.vue similarity index 97% rename from apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue rename to apps/app-frontend/src/pages/instance/components/settings-modal/window-settings.vue index 925a22a3de..5e6a662366 100644 --- a/apps/app-frontend/src/components/ui/instance_settings/WindowSettings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/window-settings.vue @@ -11,9 +11,9 @@ import { computed, type Ref, ref, watch } from 'vue' import { edit } from '@/helpers/instance' import { get } from '@/helpers/settings.ts' -import { injectInstanceSettings } from '@/providers/instance-settings' -import type { AppSettings } from '../../../helpers/types' +import type { AppSettings } from '../../../../helpers/types' +import { injectInstanceSettings } from './instance-settings-context' const { handleError } = injectNotificationManager() const { formatMessage } = useVIntl() diff --git a/apps/app-frontend/src/pages/instance/Mods.vue b/apps/app-frontend/src/pages/instance/content/index.vue similarity index 89% rename from apps/app-frontend/src/pages/instance/Mods.vue rename to apps/app-frontend/src/pages/instance/content/index.vue index f7314efbee..018c27b03e 100644 --- a/apps/app-frontend/src/pages/instance/Mods.vue +++ b/apps/app-frontend/src/pages/instance/content/index.vue @@ -19,13 +19,11 @@ ref="modpackContentModal" :modpack-name="displayedModpackProject?.title" :modpack-icon-url="displayedModpackProject?.icon_url ?? undefined" - :enable-toggle="!props.isServerInstance && !isSharedMember && !isQuarantined" + :enable-toggle="!isServerInstance && !isSharedMember && !isQuarantined" :busy="isBulkOperating" :get-overflow-options="getOverflowOptions" :switch-version=" - props.isServerInstance || isSharedMember || isQuarantined - ? undefined - : handleSwitchVersion + isServerInstance || isSharedMember || isQuarantined ? undefined : handleSwitchVersion " @update:enabled="handleModpackContentToggle" @bulk:enable="(items) => handleModpackContentBulkToggle(items, true)" @@ -146,13 +144,15 @@ import { } from '@/helpers/instance' import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content' import { get as getSettings, set as setSettings } from '@/helpers/settings' -import type { CacheBehaviour, GameInstance } from '@/helpers/types' +import type { CacheBehaviour } from '@/helpers/types' import { highlightModInInstance } from '@/helpers/utils.js' import { injectContentInstall } from '@/providers/content-install' -import { provideInstanceBackup } from '@/providers/instance-backup' import { useTheming } from '@/store/state' import type { FeatureFlag } from '@/store/theme' +import { injectInstancePage } from '../instance-context' +import { instanceContentQueryOptions, instanceKeys } from '../query-options' + const messages = defineMessages({ shareTitle: { id: 'app.instance.mods.share-title', @@ -218,13 +218,11 @@ const skipNonEssentialWarnings = computed(() => themeStore.getFeatureFlag('skip_non_essential_warnings'), ) -const props = defineProps<{ - instance: GameInstance - isServerInstance?: boolean - openSettings?: () => void - preloadedContent?: InstanceContentData | null -}>() -const managedContentPolicy = useManagedContentPolicy(computed(() => props.instance)) +const instancePage = injectInstancePage() +const instance = instancePage.instance +const isServerInstance = instancePage.isServerInstance +const openSettings = () => instancePage.openSettings(1) +const managedContentPolicy = useManagedContentPolicy(computed(() => instance.value)) const { isManagedModpack: isSharedMember, isQuarantined, @@ -232,18 +230,20 @@ const { canUpdateContent: canUpdateProject, } = managedContentPolicy -function hasPreloadedContent(contentData: InstanceContentData | null | undefined) { - return contentData?.path === props.instance.id -} - -const loading = ref(!hasPreloadedContent(props.preloadedContent)) +const contentQuery = useQuery( + computed(() => ({ + ...instanceContentQueryOptions(instancePage.instanceId.value), + enabled: !!instancePage.instanceId.value, + })), +) +const loading = ref(contentQuery.data.value === undefined) const projects = ref([]) const installingBuffer = ref([]) const handledInstallRevision = ref(0) watch( - () => installingItems.value.get(props.instance.id), + () => installingItems.value.get(instance.value.id), (items) => { if (items && items.length > 0) { installingBuffer.value = [...items] @@ -261,7 +261,7 @@ watch(projects, (newProjects) => { }) const mergedProjects = computed(() => { - const active = installingItems.value.get(props.instance.id) + const active = installingItems.value.get(instance.value.id) const pending = active ?? installingBuffer.value if (pending.length === 0) return projects.value const pendingProjectIds = new Set(pending.map((p) => p.project?.id).filter(Boolean)) @@ -276,7 +276,7 @@ const mergedProjects = computed(() => { }) watch( - () => installFailureRevisionByInstance.value.get(props.instance.id) ?? 0, + () => installFailureRevisionByInstance.value.get(instance.value.id) ?? 0, (revision, previousRevision) => { if (revision === previousRevision) return installingBuffer.value = [] @@ -292,14 +292,14 @@ const linkedModpackUpdateVersionId = ref(null) const localImportedModpackUnlinked = ref(false) const localImportedModpackProject = computed(() => { - const link = props.instance.link + const link = instance.value.link if (localImportedModpackUnlinked.value || link?.type !== 'imported_modpack') return null return { - id: link.filename ?? props.instance.id, - slug: link.filename ?? props.instance.id, - title: link.name ?? props.instance.name, - icon_url: props.instance.icon_path ? convertFileSrc(props.instance.icon_path) : undefined, + id: link.filename ?? instance.value.id, + slug: link.filename ?? instance.value.id, + title: link.name ?? instance.value.name, + icon_url: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : undefined, description: '', filename: link.filename ?? undefined, } @@ -310,7 +310,7 @@ const displayedModpackProject = computed( ) watch( - () => props.instance.link, + () => instance.value.link, () => { localImportedModpackUnlinked.value = false }, @@ -318,12 +318,12 @@ watch( const isModpackUpdating = ref(false) const isBulkOperating = ref(false) -const isInstanceBusy = computed(() => props.instance?.install_stage !== 'installed') +const isInstanceBusy = computed(() => instance.value?.install_stage !== 'installed') const isPackLocked = computed( () => - props.instance.quarantined || - props.instance?.link?.type === 'modrinth_modpack' || - props.instance?.link?.type === 'server_project_modpack', + instance.value.quarantined || + instance.value?.link?.type === 'modrinth_modpack' || + instance.value?.link?.type === 'server_project_modpack', ) const shareModal = ref | null>() @@ -337,15 +337,15 @@ const unknownFileWarningModal = ref const unknownFileName = ref('') let resolveUnknownFileConfirmation: ((confirmed: boolean) => void) | null = null -const modpackContentQueryKey = computed(() => ['linkedModpackContent', props.instance.id]) +const modpackContentQueryKey = computed(() => instanceKeys.linkedContent(instance.value.id)) const modpackContentQuery = useQuery({ queryKey: modpackContentQueryKey, - queryFn: () => get_linked_modpack_content(props.instance.id), + queryFn: () => get_linked_modpack_content(instance.value.id), enabled: computed( () => - !!props.instance?.id && - !!props.instance?.link && - props.instance.install_stage === 'installed', + !!instance.value?.id && + !!instance.value?.link && + instance.value.install_stage === 'installed', ), }) @@ -523,15 +523,12 @@ async function getUpdaterProjectVersions(projectId: string, pinnedVersionId?: st } async function handleBrowseContent() { - if (!props.instance || props.instance.quarantined) return - await router.push({ - path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`, - query: { i: props.instance.id }, - }) + if (!instance.value || instance.value.quarantined) return + await instancePage.browseContent(instance.value.loader === 'vanilla' ? 'resourcepack' : 'mod') } async function handleUploadFiles() { - if (!props.instance || props.instance.quarantined) return + if (!instance.value || instance.value.quarantined) return const files = await open({ multiple: true }) if (!files) return const selectedFiles: Array<{ path: string; filename: string }> = [] @@ -566,7 +563,7 @@ async function handleUploadFiles() { await Promise.all( confirmedFiles.map(async ({ path, filename }) => { try { - const installedPath = await add_project_from_path(props.instance.id, path) + const installedPath = await add_project_from_path(instance.value.id, path) return { filename, installedPath } } catch (error) { handleError(error as Error) @@ -637,7 +634,7 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) { const originalFilePath = mod.file_path try { - const newPath = await toggle_disable_project(props.instance.id, mod.file_path, desiredEnabled) + const newPath = await toggle_disable_project(instance.value.id, mod.file_path, desiredEnabled) const newFileName = fileNameFromPath(newPath) const enabled = !newPath.endsWith('.disabled') mod.file_path = newPath @@ -655,8 +652,8 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) { }) trackEvent('InstanceProjectDisable', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, id: mod.project?.id, name: mod.project?.title ?? mod.file_name, project_type: mod.project_type, @@ -678,12 +675,12 @@ async function removeMod(mod: ContentItem) { try { const removedPath = mod.file_path - await remove_project(props.instance.id, removedPath) + await remove_project(instance.value.id, removedPath) projects.value = projects.value.filter((x) => removedPath !== x.file_path) trackEvent('InstanceProjectRemove', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, id: mod.project?.id, name: mod.project?.title ?? mod.file_name, project_type: mod.project_type, @@ -709,7 +706,7 @@ function dependencyTargetsItem(dependency: Labrinth.Versions.v2.Dependency, item } async function getDeleteDependencyWarning(items: ContentItem[]) { - if (props.isServerInstance) return null + if (isServerInstance.value) return null const deletingIds = new Set(items.map(getContentItemId)) const remainingItems = projects.value.filter((item) => !deletingIds.has(getContentItemId(item))) @@ -787,12 +784,12 @@ async function bulkUpdateAllProjects(onProgress?: (status: BulkOperationStatus) waiting: true, }) unlisten = await instance_bulk_update_progress_listener((progress) => { - if (progress.instanceId !== props.instance.id) return + if (progress.instanceId !== instance.value.id) return onProgress(formatBulkUpdateProgress(progress)) }) } - await update_all(props.instance.id) + await update_all(instance.value.id) await refreshContentState('must_revalidate') } catch (err) { handleError(err as Error) @@ -810,14 +807,14 @@ async function updateProject(mod: ContentItem) { try { const updateVersionId = mod.update_version_id! await switch_project_version_with_dependencies( - props.instance.id, + instance.value.id, mod.file_path, updateVersionId, ) trackEvent('InstanceProjectUpdate', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, id: mod.project?.id, name: mod.project?.title ?? mod.file_name, project_type: mod.project_type, @@ -840,11 +837,11 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions const oldPath = mod.file_path try { - await switch_project_version_with_dependencies(props.instance.id, oldPath, version.id) + await switch_project_version_with_dependencies(instance.value.id, oldPath, version.id) trackEvent('InstanceProjectUpdate', { - loader: props.instance.loader, - game_version: props.instance.game_version, + loader: instance.value.loader, + game_version: instance.value.game_version, id: mod.project?.id, name: mod.project?.title ?? mod.file_name, project_type: mod.project_type, @@ -872,8 +869,8 @@ async function handleUpdate(id: string) { currentVersionId: item.version.id, currentVersionNumber: item.version.version_number, updateVersionId: item.update_version_id, - instanceGameVersion: props.instance.game_version, - instanceLoader: props.instance.loader, + instanceGameVersion: instance.value.game_version, + instanceLoader: instance.value.loader, }) updatingModpack.value = false @@ -899,11 +896,11 @@ async function handleUpdate(id: string) { updateVersionId: item.update_version_id, }, instance: { - path: props.instance.id, - name: props.instance.name, - gameVersion: props.instance.game_version, - loader: props.instance.loader, - link: props.instance.link, + path: instance.value.id, + name: instance.value.name, + gameVersion: instance.value.game_version, + loader: instance.value.loader, + link: instance.value.link, }, modalStateBeforeFetch: { updatingModpack: updatingModpack.value, @@ -1027,7 +1024,7 @@ async function setModpackContentEnabled(items: ContentItem[], enabled: boolean) } async function handleModpackContent() { - if (!props.instance?.id) return + if (!instance.value?.id) return if (modpackContentQuery.data.value?.length) { modpackContentModal.value?.show(modpackContentQuery.data.value) @@ -1047,12 +1044,12 @@ async function handleModpackContent() { } async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) { - if (!props.instance?.id) return + if (!instance.value?.id) return const contentItems = await queryClient .fetchQuery({ queryKey: modpackContentQueryKey.value, - queryFn: () => get_linked_modpack_content(props.instance.id, cacheBehaviour), + queryFn: () => get_linked_modpack_content(instance.value.id, cacheBehaviour), }) .catch(handleError) @@ -1067,7 +1064,7 @@ async function refreshContentState(cacheBehaviour?: CacheBehaviour) { } watch( - () => installRevisionByInstance.value.get(props.instance.id) ?? 0, + () => installRevisionByInstance.value.get(instance.value.id) ?? 0, async (revision) => { if (revision <= handledInstallRevision.value) return handledInstallRevision.value = revision @@ -1076,7 +1073,7 @@ watch( ) async function handleModpackUpdate() { - if (!props.instance?.link?.project_id) return + if (!instance.value?.link?.project_id) return const requestId = beginUpdateRequest() @@ -1089,7 +1086,7 @@ async function handleModpackUpdate() { await nextTick() const initialVersionId = - linkedModpackUpdateVersionId.value ?? props.instance?.link?.version_id ?? undefined + linkedModpackUpdateVersionId.value ?? instance.value?.link?.version_id ?? undefined debug('handleModpackUpdate: opening modpack updater modal', { type: 'modpack', initialVersionId, @@ -1098,11 +1095,11 @@ async function handleModpackUpdate() { linkedModpackVersion: linkedModpackVersion.value, linkedModpackHasUpdate: linkedModpackHasUpdate.value, instance: { - path: props.instance.id, - name: props.instance.name, - gameVersion: props.instance.game_version, - loader: props.instance.loader, - link: props.instance.link, + path: instance.value.id, + name: instance.value.name, + gameVersion: instance.value.game_version, + loader: instance.value.loader, + link: instance.value.link, }, modalStateBeforeFetch: { updatingModpack: updatingModpack.value, @@ -1118,7 +1115,7 @@ async function handleModpackUpdate() { }) contentUpdaterModal.value?.show(initialVersionId) - const versions = await getUpdaterProjectVersions(props.instance.link.project_id, initialVersionId) + const versions = await getUpdaterProjectVersions(instance.value.link.project_id, initialVersionId) if (!isActiveUpdateRequest(requestId) || !updatingModpack.value) return @@ -1143,7 +1140,7 @@ async function handleModpackUpdate() { : null, versionCount: versions.length, linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value, - currentLinkedVersionId: props.instance.link.version_id, + currentLinkedVersionId: instance.value.link.version_id, }) updatingProjectVersions.value = versions @@ -1195,14 +1192,14 @@ function resetUpdateState() { async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) { pendingModpackUpdateVersion.value = selectedVersion - const currentVersionId = props.instance?.link?.version_id + const currentVersionId = instance.value?.link?.version_id const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId) isModpackUpdateDowngrade.value = currentVersion ? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published) : false const shouldShowWarning = isModpackUpdateDowngrade.value || - versionChangesGameVersion(selectedVersion, props.instance.game_version) + versionChangesGameVersion(selectedVersion, instance.value.game_version) if (skipNonEssentialWarnings.value || !shouldShowWarning) { await handleModpackUpdateConfirm() @@ -1213,7 +1210,7 @@ async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2. } async function handleModpackUpdateConfirm() { - if (!pendingModpackUpdateVersion.value || !props.instance?.id) return + if (!pendingModpackUpdateVersion.value || !instance.value?.id) return const version = pendingModpackUpdateVersion.value pendingModpackUpdateVersion.value = null @@ -1221,7 +1218,7 @@ async function handleModpackUpdateConfirm() { contentUpdaterModal.value?.hide() isModpackUpdating.value = true try { - await update_managed_modrinth_version(props.instance.id, version.id) + await update_managed_modrinth_version(instance.value.id, version.id) await initProjects() } finally { isModpackUpdating.value = false @@ -1260,7 +1257,7 @@ async function handleModalUpdate( } async function unpairInstance() { - await edit(props.instance.id, { + await edit(instance.value.id, { link: null as unknown as undefined, }) linkedModpackProject.value = null @@ -1312,7 +1309,7 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] { options.push({ id: formatMessage(commonMessages.showFileButton), icon: FolderOpenIcon, - action: () => highlightModInInstance(props.instance.id, item.file_path), + action: () => highlightModInInstance(instance.value.id, item.file_path), }) if (item.project?.slug) { @@ -1330,15 +1327,19 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] { return options } -async function initProjects(cacheBehaviour?: CacheBehaviour) { - if (!props.instance) return +async function initProjects(cacheBehaviour?: CacheBehaviour, staleTime = 0) { + if (!instance.value) return - const contentData = await loadInstanceContentData(props.instance.id, cacheBehaviour, handleError) + const contentData = await queryClient.fetchQuery({ + ...instanceContentQueryOptions(instance.value.id), + queryFn: () => loadInstanceContentData(instance.value.id, cacheBehaviour, handleError), + staleTime, + }) applyContentData(contentData) } function applyContentData(contentData: InstanceContentData) { - if (contentData.path !== props.instance.id) { + if (contentData.path !== instance.value.id) { return false } @@ -1372,8 +1373,6 @@ function applyContentData(contentData: InstanceContentData) { return true } -provideInstanceBackup(() => props.instance) - provideContentManager({ items: mergedProjects, loading, @@ -1384,14 +1383,14 @@ provideContentManager({ project: linkedModpackProject.value, projectLink: { path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`, - query: { i: props.instance.id }, + query: { i: instance.value.id }, }, version: linkedModpackVersion.value ?? undefined, versionLink: linkedModpackProject.value && linkedModpackVersion.value ? { path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`, - query: { i: props.instance.id }, + query: { i: instance.value.id }, } : undefined, owner: linkedModpackOwner.value @@ -1459,12 +1458,12 @@ provideContentManager({ bulkUpdateAll: bulkUpdateAllProjects, bulkUpdateItem: updateProject, updateModpack: - props.isServerInstance || isSharedMember.value || isQuarantined.value + isServerInstance.value || isSharedMember.value || isQuarantined.value ? undefined : handleModpackUpdate, viewModpackContent: handleModpackContent, unlinkModpack: unpairInstance, - openSettings: props.openSettings, + openSettings: openSettings, switchVersion: handleSwitchVersion, getOverflowOptions, shareItems: handleShareItems, @@ -1478,7 +1477,7 @@ provideContentManager({ icon_url: null, }, projectLink: item.project?.id - ? { path: `/project/${item.project.id}`, query: { i: props.instance.id } } + ? { path: `/project/${item.project.id}`, query: { i: instance.value.id } } : undefined, version: item.version ?? { id: item.file_name, @@ -1489,7 +1488,7 @@ provideContentManager({ item.project?.id && item.version?.id ? { path: `/project/${item.project.id}/version/${item.version.id}`, - query: { i: props.instance.id }, + query: { i: instance.value.id }, } : undefined, owner: item.owner @@ -1504,7 +1503,7 @@ provideContentManager({ hideSwitchVersion: !canMutateContent(item) || !item.project?.id || !item.version?.id, hasUpdate: canUpdateProject(item), }), - filterPersistKey: props.instance.id, + filterPersistKey: instance.value.id, }) type UnlistenFn = () => void @@ -1513,7 +1512,7 @@ const initialContentReady = loadInitialContent() void initialContentReady.then(restoreModpackContentModalState).catch(handleError) function getInstallRevision() { - return installRevisionByInstance.value.get(props.instance.id) ?? 0 + return installRevisionByInstance.value.get(instance.value.id) ?? 0 } function loadInitialContent() { @@ -1523,13 +1522,23 @@ function loadInitialContent() { return initProjects('must_revalidate') } - if (props.preloadedContent && applyContentData(props.preloadedContent)) { - return Promise.resolve() - } - - return initProjects() + return initProjects(undefined, 30_000) } +watch( + contentQuery.data, + (data) => { + if (data) applyContentData(data) + }, + { immediate: true }, +) +watch(contentQuery.error, (error) => { + if (error) { + loading.value = false + handleError(error) + } +}) + async function restoreModpackContentModalState() { if (!savedModalState) return @@ -1552,11 +1561,11 @@ let unlistenInstances: UnlistenFn | null = null onMounted(() => { void getCurrentWebview() .onDragDropEvent(async (event) => { - if (event.payload.type !== 'drop' || !props.instance) return + if (event.payload.type !== 'drop' || !instance.value) return for (const file of event.payload.paths) { if (file.endsWith('.mrpack')) continue - await add_project_from_path(props.instance.id, file).catch(handleError) + await add_project_from_path(instance.value.id, file).catch(handleError) } await initProjects() }) @@ -1572,10 +1581,10 @@ onMounted(() => { void instance_listener(async (event: { event: string; instance_id: string }) => { if ( - props.instance && - event.instance_id === props.instance.id && + instance.value && + event.instance_id === instance.value.id && event.event === 'synced' && - props.instance.install_stage === 'installed' && + instance.value.install_stage === 'installed' && !isBulkOperating.value ) { await initProjects() @@ -1593,7 +1602,7 @@ onMounted(() => { }) watch( - () => props.instance?.install_stage, + () => instance.value?.install_stage, async (newStage, oldStage) => { if (oldStage !== 'installed' && newStage === 'installed') { await refreshContentState('must_revalidate') @@ -1604,7 +1613,7 @@ watch( ) watch( - () => props.instance?.link, + () => instance.value?.link, async (newInstanceLink, oldInstanceLink) => { if (oldInstanceLink && !newInstanceLink) { await initProjects('must_revalidate') @@ -1613,7 +1622,7 @@ watch( ) watch( - () => props.instance?.update_channel, + () => instance.value?.update_channel, async (newValue, oldValue) => { if (newValue !== oldValue) { await initProjects('must_revalidate') diff --git a/apps/app-frontend/src/pages/instance/Files.vue b/apps/app-frontend/src/pages/instance/files/index.vue similarity index 82% rename from apps/app-frontend/src/pages/instance/Files.vue rename to apps/app-frontend/src/pages/instance/files/index.vue index 79020b38ac..0786bb0488 100644 --- a/apps/app-frontend/src/pages/instance/Files.vue +++ b/apps/app-frontend/src/pages/instance/files/index.vue @@ -10,6 +10,7 @@ import { useDebugLogger, useVIntl, } from '@modrinth/ui' +import { useQuery } from '@tanstack/vue-query' import { invoke } from '@tauri-apps/api/core' import { mkdir, @@ -22,21 +23,17 @@ import { writeFile as writeFileBytes, writeTextFile, } from '@tauri-apps/plugin-fs' -import { onUnmounted, ref, watch } from 'vue' +import { computed, onUnmounted, ref, watch } from 'vue' import { instance_listener } from '@/helpers/events' import { get_full_path } from '@/helpers/instance' -import type { GameInstance } from '@/helpers/types' import { highlightInFolder } from '@/helpers/utils' -const props = defineProps<{ - instance: GameInstance - options: unknown - offline: boolean - playing: boolean - installed: boolean - isServerInstance: boolean -}>() +import { injectInstancePage } from '../instance-context' +import { instanceKeys } from '../query-options' + +const instancePage = injectInstancePage() +const instanceId = instancePage.instanceId const { formatMessage } = useVIntl() const { addNotification } = injectNotificationManager() @@ -53,7 +50,15 @@ const messages = defineMessages({ }, }) -const instanceRoot = ref('') +const instanceRootQuery = useQuery( + computed(() => ({ + queryKey: instanceKeys.rootPath(instancePage.instanceId.value), + queryFn: () => get_full_path(instancePage.instanceId.value), + enabled: !!instancePage.instanceId.value, + staleTime: Infinity, + })), +) +const instanceRoot = computed(() => instanceRootQuery.data.value ?? '') const items = ref([]) /** True until the first directory read for the current instance path finishes (initial load only). */ const firstPaintPending = ref(true) @@ -62,12 +67,7 @@ const error = ref(null) const currentPath = ref('') const editingFile = ref(null) -debug('setup: start, instance.id =', props.instance.id) - -instanceRoot.value = await get_full_path(props.instance.id) -debug('setup: instanceRoot =', instanceRoot.value) -await refresh() -debug('setup: refresh complete, items =', items.value.length, 'error =', error.value) +debug('setup: start, instance.id =', instanceId.value) function resolvePath(relativePath: string): string { return relativePath ? `${instanceRoot.value}/${relativePath}` : instanceRoot.value @@ -113,21 +113,39 @@ async function listDirectory(dirPath: string): Promise { return results.filter((item): item is FileItem => item !== null) } +const directoryQuery = useQuery( + computed(() => ({ + queryKey: instanceKeys.files(instancePage.instanceId.value, currentPath.value), + queryFn: () => listDirectory(currentPath.value), + enabled: !!instanceRoot.value, + staleTime: 30_000, + })), +) + +watch( + directoryQuery.data, + (data) => { + if (!data) return + items.value = data + firstPaintPending.value = false + }, + { immediate: true }, +) +watch(directoryQuery.isFetching, (fetching) => { + loading.value = fetching +}) +watch(directoryQuery.error, (queryError) => { + error.value = queryError + if (queryError) items.value = [] +}) + +await instanceRootQuery.suspense() +await directoryQuery.refetch() +firstPaintPending.value = false + async function refresh() { debug('refresh: called, currentPath =', currentPath.value, 'instanceRoot =', instanceRoot.value) - loading.value = true - error.value = null - try { - items.value = await listDirectory(currentPath.value) - debug('refresh: success, items =', items.value.length) - } catch (e) { - debug('refresh: error =', e) - error.value = e instanceof Error ? e : new Error(String(e)) - items.value = [] - } finally { - loading.value = false - firstPaintPending.value = false - } + await directoryQuery.refetch() } function navigateTo(path: string) { @@ -221,7 +239,7 @@ async function handleWriteFile(path: string, content: string) { async function handleDownloadFile(path: string, _fileName: string) { await invoke('plugin:files|file_save_as', { - instanceId: props.instance.id, + instanceId: instanceId.value, filePath: path, }) } @@ -275,7 +293,7 @@ async function handleUploadFiles(files: File[]) { async function handleExtractFile(path: string, override: boolean, dry: boolean) { try { return await invoke('plugin:files|file_extract_zip', { - instanceId: props.instance.id, + instanceId: instanceId.value, filePath: path, overrideConflicts: override, dryRun: dry, @@ -293,7 +311,7 @@ debug('setup: registering instance_listener') const unlistenInstances = await instance_listener( async (event: { event: string; instance_id: string }) => { debug('instance_listener: event =', event.event, 'path =', event.instance_id) - if (event.instance_id === props.instance.id && event.event === 'synced') { + if (event.instance_id === instanceId.value && event.event === 'synced') { debug('instance_listener: synced event matched, calling refresh') await refresh() } @@ -305,16 +323,13 @@ onUnmounted(() => { unlistenInstances() }) -watch( - () => props.instance.id, - async () => { - debug('watch instance.id: changed to', props.instance.id) - firstPaintPending.value = true - instanceRoot.value = await get_full_path(props.instance.id) - currentPath.value = '' - await refresh() - }, -) +watch(instanceId, async () => { + debug('watch instance.id: changed to', instanceId.value) + firstPaintPending.value = true + currentPath.value = '' + await instanceRootQuery.refetch() + await refresh() +}) provideFileManager({ items, diff --git a/apps/app-frontend/src/pages/instance/index.js b/apps/app-frontend/src/pages/instance/index.js deleted file mode 100644 index 0e6a0e7ec2..0000000000 --- a/apps/app-frontend/src/pages/instance/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import Files from './Files.vue' -import Index from './Index.vue' -import Logs from './Logs.vue' -import Mods from './Mods.vue' -import Overview from './Overview.vue' -import Share from './share/index.vue' -import Worlds from './Worlds.vue' - -export { Files, Index, Logs, Mods, Overview, Share, Worlds } diff --git a/apps/app-frontend/src/pages/instance/index.ts b/apps/app-frontend/src/pages/instance/index.ts new file mode 100644 index 0000000000..a4cfdec8da --- /dev/null +++ b/apps/app-frontend/src/pages/instance/index.ts @@ -0,0 +1,8 @@ +import Content from './content/index.vue' +import Files from './files/index.vue' +import Index from './layout.vue' +import Logs from './logs/index.vue' +import Share from './share/index.vue' +import Worlds from './worlds/index.vue' + +export { Content, Files, Index, Logs, Share, Worlds } diff --git a/apps/app-frontend/src/pages/instance/instance-context.ts b/apps/app-frontend/src/pages/instance/instance-context.ts new file mode 100644 index 0000000000..19223b9664 --- /dev/null +++ b/apps/app-frontend/src/pages/instance/instance-context.ts @@ -0,0 +1,27 @@ +import type { Labrinth } from '@modrinth/api-client' +import { createContext } from '@modrinth/ui' +import type { ComputedRef, Ref } from 'vue' + +import type { GameInstance } from '@/helpers/types' + +export interface InstancePageContext { + readonly instanceId: ComputedRef + readonly instance: ComputedRef + readonly linkedProject: ComputedRef + readonly isServerInstance: ComputedRef + readonly offline: Readonly> + readonly playing: ComputedRef + readonly loading: Readonly> + readonly stopping: Readonly> + refreshInstance: () => Promise + refreshPlayState: () => Promise + play: (source: string) => Promise + stop: (source: string) => Promise + playServer: () => Promise + openSettings: (tab?: number) => void + browseContent: (projectType?: string) => Promise + browseServers: () => Promise +} + +export const [injectInstancePage, provideInstancePage] = + createContext('InstancePage') diff --git a/apps/app-frontend/src/pages/instance/Index.vue b/apps/app-frontend/src/pages/instance/layout.vue similarity index 57% rename from apps/app-frontend/src/pages/instance/Index.vue rename to apps/app-frontend/src/pages/instance/layout.vue index 6cc08714f1..c2dd0ede78 100644 --- a/apps/app-frontend/src/pages/instance/Index.vue +++ b/apps/app-frontend/src/pages/instance/layout.vue @@ -11,7 +11,7 @@ ref="settingsModal" :instance="instance" :offline="offline" - @unlinked="fetchInstance" + @unlinked="refreshInstance" />
- + @@ -102,45 +90,24 @@ - - - - - - - - - - -
- - diff --git a/apps/app-frontend/src/pages/instance/Logs.vue b/apps/app-frontend/src/pages/instance/logs/index.vue similarity index 73% rename from apps/app-frontend/src/pages/instance/Logs.vue rename to apps/app-frontend/src/pages/instance/logs/index.vue index 6a08117dd7..fd8add17f7 100644 --- a/apps/app-frontend/src/pages/instance/Logs.vue +++ b/apps/app-frontend/src/pages/instance/logs/index.vue @@ -11,51 +11,20 @@ import { injectNotificationManager, provideConsoleManager, } from '@modrinth/ui' +import { useQuery } from '@tanstack/vue-query' import { computed, onUnmounted, ref, shallowRef, triggerRef, watch, watchEffect } from 'vue' -import { useRoute } from 'vue-router' import { useInstanceConsole } from '@/composables/useInstanceConsole' import { log_listener, process_listener } from '@/helpers/events.js' import { delete_logs_by_filename, get_output_by_filename } from '@/helpers/logs.js' +import { injectInstancePage } from '../instance-context' +import { instanceKeys } from '../query-options' + const client = injectModrinthClient() const { handleError } = injectNotificationManager() -const route = useRoute() - -const props = defineProps({ - instance: { - type: Object, - default() { - return {} - }, - }, - options: { - type: Object, - default() { - return {} - }, - }, - offline: { - type: Boolean, - default() { - return false - }, - }, - playing: { - type: Boolean, - default() { - return false - }, - }, - installed: { - type: Boolean, - default() { - return false - }, - }, -}) - -const instanceId = computed(() => route.params.id) +const instancePage = injectInstancePage() +const instanceId = instancePage.instanceId const { liveConsole, historicalConsole, @@ -66,7 +35,17 @@ const { clearLive, } = useInstanceConsole(instanceId.value) -await hydrate() +const consoleHydrationQuery = useQuery({ + queryKey: computed(() => instanceKeys.console(instanceId.value)), + queryFn: async () => { + await hydrate() + return true + }, + staleTime: 0, + refetchOnMount: 'always', +}) + +await consoleHydrationQuery.suspense() function buildLogList(rawLogs) { return [ @@ -88,18 +67,29 @@ function buildLogList(rawLogs) { } const logs = ref(buildLogList([])) - -void getHistoricalLogs() - .then((allLogs) => { - logs.value = buildLogList(allLogs) - }) - .catch(handleError) +const historicalLogsQuery = useQuery({ + queryKey: computed(() => instanceKeys.logs(instanceId.value)), + queryFn: getHistoricalLogs, + staleTime: 0, +}) +watch( + historicalLogsQuery.data, + (allLogs) => { + if (allLogs) logs.value = buildLogList(allLogs) + }, + { immediate: true }, +) +watch(historicalLogsQuery.error, (error) => { + if (error) handleError(error) +}) const selectedLogIndex = ref(0) const isLive = computed(() => selectedLogIndex.value === 0) const filteredLogs = computed(() => - props.playing ? logs.value.filter((l) => l.live || l.name !== 'latest.log') : logs.value, + instancePage.playing.value + ? logs.value.filter((l) => l.live || l.name !== 'latest.log') + : logs.value, ) const logSources = computed(() => @@ -140,16 +130,16 @@ const selectedLog = computed(() => filteredLogs.value[selectedLogIndex.value]) const deleteDisabled = computed(() => { const log = selectedLog.value if (!log || log.live) return true - return log.filename === 'latest.log' && props.playing + return log.filename === 'latest.log' && instancePage.playing.value }) async function deleteSelectedLog() { const log = selectedLog.value if (!log || log.live) return - await delete_logs_by_filename(props.instance.id, log.log_type, log.filename) + await delete_logs_by_filename(instanceId.value, log.log_type, log.filename) invalidate() - const freshLogs = await getHistoricalLogs() - logs.value = buildLogList(freshLogs) + const { data } = await historicalLogsQuery.refetch() + if (data) logs.value = buildLogList(data) selectedLogIndex.value = 0 } @@ -166,7 +156,7 @@ provideConsoleManager({ onDelete: deleteSelectedLog, deleteDisabled, deleteDisabledTooltip: 'Cannot delete latest.log while the instance is running', - shareDisabled: computed(() => props.offline), + shareDisabled: instancePage.offline, emptyStateType: 'instance', crashAnalysis, onDismissCrash: () => { @@ -186,7 +176,7 @@ watch(selectedLogIndex, async (newIndex) => { return } - const output = await get_output_by_filename(props.instance.id, log.log_type, log.filename).catch( + const output = await get_output_by_filename(instanceId.value, log.log_type, log.filename).catch( handleError, ) if (output) { @@ -197,7 +187,7 @@ watch(selectedLogIndex, async (newIndex) => { selectedLogIndex.value = 0 -if (!props.playing) { +if (!instancePage.playing.value) { void analyseForCrash() } @@ -216,12 +206,13 @@ const unlistenProcesses = await process_listener(async (e) => { if (e.event === 'launched') { liveConsole.clear() invalidate() + void historicalLogsQuery.refetch() selectedLogIndex.value = 0 } if (e.event === 'finished') { invalidate() - const freshLogs = await getHistoricalLogs() - logs.value = buildLogList(freshLogs) + const { data } = await historicalLogsQuery.refetch() + if (data) logs.value = buildLogList(data) void analyseForCrash() } }) diff --git a/apps/app-frontend/src/pages/instance/query-options.ts b/apps/app-frontend/src/pages/instance/query-options.ts new file mode 100644 index 0000000000..93f4ee81c8 --- /dev/null +++ b/apps/app-frontend/src/pages/instance/query-options.ts @@ -0,0 +1,79 @@ +import { queryOptions } from '@tanstack/vue-query' + +import { get_project_v3 } from '@/helpers/cache.js' +import { get as getInstance } from '@/helpers/instance' +import { loadInstanceContentData } from '@/helpers/instance-content' +import { get_by_instance_id } from '@/helpers/process' +import { refreshWorlds } from '@/helpers/worlds' + +export const instanceKeys = { + all: ['instances'] as const, + detail: (instanceId: string) => [...instanceKeys.all, 'summary', instanceId] as const, + processes: (instanceId: string) => [...instanceKeys.all, 'processes', instanceId] as const, + content: (instanceId: string) => [...instanceKeys.all, 'content', instanceId] as const, + rootPath: (instanceId: string) => [...instanceKeys.detail(instanceId), 'root-path'] as const, + files: (instanceId: string, path: string) => + [...instanceKeys.detail(instanceId), 'files', path] as const, + console: (instanceId: string) => [...instanceKeys.detail(instanceId), 'console'] as const, + logs: (instanceId: string) => [...instanceKeys.detail(instanceId), 'logs'] as const, + installedProjectIds: (instanceId: string, source: 'content' | 'worlds') => + [...instanceKeys.detail(instanceId), 'installed-project-ids', source] as const, + linkedContent: (instanceId: string) => ['linkedModpackContent', instanceId] as const, + worlds: (instanceId: string) => ['worlds', instanceId] as const, + linkedProject: (projectId: string) => ['project', 'v3', projectId] as const, + sharedEligibility: (userId: string | null | undefined) => + ['shared-instance-eligibility', userId] as const, + sharedUpdatePreview: (instanceId: string, userId: string | null | undefined) => + [...instanceKeys.detail(instanceId), 'shared-update-preview', userId] as const, + sharedMembers: (instanceId: string) => ['sharedInstanceUsers', instanceId] as const, +} + +export function instanceDetailQueryOptions(instanceId: string) { + return queryOptions({ + queryKey: instanceKeys.detail(instanceId), + queryFn: async () => { + const instance = await getInstance(instanceId) + if (!instance) throw new Error(`Instance ${instanceId} is not managed`) + return instance + }, + staleTime: 30_000, + }) +} + +export function instanceProcessesQueryOptions(instanceId: string) { + return queryOptions({ + queryKey: instanceKeys.processes(instanceId), + queryFn: async () => { + const processes = await get_by_instance_id(instanceId) + return Array.isArray(processes) ? processes : [] + }, + staleTime: 0, + }) +} + +export function instanceLinkedProjectQueryOptions(projectId: string) { + return queryOptions({ + queryKey: instanceKeys.linkedProject(projectId), + queryFn: () => get_project_v3(projectId, 'must_revalidate'), + staleTime: 30_000, + }) +} + +export function instanceContentQueryOptions( + instanceId: string, + onError?: (error: Error) => unknown, +) { + return queryOptions({ + queryKey: instanceKeys.content(instanceId), + queryFn: () => loadInstanceContentData(instanceId, undefined, onError), + staleTime: 30_000, + }) +} + +export function instanceWorldsQueryOptions(instanceId: string) { + return queryOptions({ + queryKey: instanceKeys.worlds(instanceId), + queryFn: () => refreshWorlds(instanceId), + staleTime: 0, + }) +} diff --git a/apps/app-frontend/src/pages/instance/share/index.vue b/apps/app-frontend/src/pages/instance/share/index.vue index b6addbe3bf..1c09022319 100644 --- a/apps/app-frontend/src/pages/instance/share/index.vue +++ b/apps/app-frontend/src/pages/instance/share/index.vue @@ -55,20 +55,7 @@