mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 01:26:23 +00:00
feat: instances v2 (#6431)
* feat: base of instances v2 * feat: use old profiles with compat layer * prototype: instances v2 * fix: install_from using profile * fix: skins migration fix * fix: frontend still using profile path * fix: add update proj multiselect guard * fix: cargo fmt * fix: content missing fields * feat: break up app-lib/api/instance.rs * fix: check_content_updates mismatch * fix: updater modal cleanup w/new structure * feat: better update all handling * fix: remove preview_update_all * fix: feedback on bulk update + lint * fix: rem transitions * fix: change to jsonb * feat: app db backup after update * fix: lint * fix: sqlx prepare + use sqlx macros * fix: lint * fix: bugs * feat: defuck the installing process up * fix: bug of hell * fix: shear * fix: fmt * fix: install progress spacing + change mc/content/overrides to bytes * fix: lint * fix: prepr * fix: navtabs anim not working in app * fix: worlds.vue improvements + browse page fixes * feat: optimise queries + adapter fns * fix: lint * fix: lint * feat: shared modrinth-content-management crate (#6469) * feat: disable warnings setting * feat: add instances shortcuts (#6329) * Add modrinth://launch deep link to start a profile Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck. * Change route to /launch/profile/{id} for future extensibility * fix: ensure profile path is url decoded * fix: URL-decode profile path from deep link * fix: use urlencoding crate for URL decoding * feat: implement app instance shortcuts * feat: change windows shortcut creation to use windows api instead * feat: implement creating a shortcut launching world/server * format * fmt * fix multiline inline tables * pnpm prepr * feat: move create shortcut to last item * refactor: split up shortcuts.rs for individual platforms * refactor: turn profile launch url into url type * use string literal and add safety comment * pt2 * refactor: rename anything that's profile into instance * update mac shortcut --------- Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com> --------- Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
This commit is contained in:
co-authored by
DJCheesusReal
Truman Gao
parent
ef4044534f
commit
734720e11e
@@ -24,8 +24,8 @@ import {
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { profile_listener } from '@/helpers/events'
|
||||
import { get_full_path } from '@/helpers/profile'
|
||||
import { instance_listener } from '@/helpers/events'
|
||||
import { get_full_path } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { highlightInFolder } from '@/helpers/utils'
|
||||
|
||||
@@ -62,9 +62,9 @@ const error = ref<Error | null>(null)
|
||||
const currentPath = ref('')
|
||||
const editingFile = ref<EditingFile | null>(null)
|
||||
|
||||
debug('setup: start, instance.path =', props.instance.path)
|
||||
debug('setup: start, instance.id =', props.instance.id)
|
||||
|
||||
instanceRoot.value = await get_full_path(props.instance.path)
|
||||
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)
|
||||
@@ -221,7 +221,7 @@ async function handleWriteFile(path: string, content: string) {
|
||||
|
||||
async function handleDownloadFile(path: string, _fileName: string) {
|
||||
await invoke('plugin:files|file_save_as', {
|
||||
instancePath: props.instance.path,
|
||||
instanceId: props.instance.id,
|
||||
filePath: path,
|
||||
})
|
||||
}
|
||||
@@ -275,7 +275,7 @@ async function handleUploadFiles(files: File[]) {
|
||||
async function handleExtractFile(path: string, override: boolean, dry: boolean) {
|
||||
try {
|
||||
return await invoke('plugin:files|file_extract_zip', {
|
||||
instancePath: props.instance.path,
|
||||
instanceId: props.instance.id,
|
||||
filePath: path,
|
||||
overrideConflicts: override,
|
||||
dryRun: dry,
|
||||
@@ -289,28 +289,28 @@ async function handleExtractFile(path: string, override: boolean, dry: boolean)
|
||||
}
|
||||
}
|
||||
|
||||
debug('setup: registering profile_listener')
|
||||
const unlistenProfiles = await profile_listener(
|
||||
async (event: { event: string; profile_path_id: string }) => {
|
||||
debug('profile_listener: event =', event.event, 'path =', event.profile_path_id)
|
||||
if (event.profile_path_id === props.instance.path && event.event === 'synced') {
|
||||
debug('profile_listener: synced event matched, calling refresh')
|
||||
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') {
|
||||
debug('instance_listener: synced event matched, calling refresh')
|
||||
await refresh()
|
||||
}
|
||||
},
|
||||
)
|
||||
debug('setup: profile_listener registered')
|
||||
debug('setup: instance_listener registered')
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfiles()
|
||||
unlistenInstances()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.instance.path,
|
||||
() => props.instance.id,
|
||||
async () => {
|
||||
debug('watch instance.path: changed to', props.instance.path)
|
||||
debug('watch instance.id: changed to', props.instance.id)
|
||||
firstPaintPending.value = true
|
||||
instanceRoot.value = await get_full_path(props.instance.path)
|
||||
instanceRoot.value = await get_full_path(props.instance.id)
|
||||
currentPath.value = ''
|
||||
await refresh()
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<ExportModal ref="exportModal" :instance="instance" />
|
||||
<InstanceSettingsModal
|
||||
:key="instance.path"
|
||||
:key="instance.id"
|
||||
ref="settingsModal"
|
||||
:instance="instance"
|
||||
:offline="offline"
|
||||
@@ -19,7 +19,7 @@
|
||||
:src="icon ? icon : undefined"
|
||||
:alt="instance.name"
|
||||
size="64px"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
@@ -78,7 +78,7 @@
|
||||
<Avatar
|
||||
:src="linkedProjectV3.icon_url"
|
||||
:alt="linkedProjectV3.name"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
size="24px"
|
||||
/>
|
||||
<router-link
|
||||
@@ -190,13 +190,17 @@
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => {
|
||||
if (instance) showProfileInFolder(instance.path)
|
||||
if (instance) showInstanceInFolder(instance.id)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
action: () => exportModal?.show(),
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
@@ -204,6 +208,7 @@
|
||||
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||
<template #create-shortcut> <ExternalIcon /> Create shortcut </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -214,14 +219,10 @@
|
||||
<NavTabs :links="tabs" />
|
||||
</div>
|
||||
<div :class="['p-6 pt-4', { 'min-h-0 flex-1 overflow-y-auto': isFixedRender }]">
|
||||
<RouterView
|
||||
v-if="route.path.startsWith('/instance')"
|
||||
v-slot="{ Component }"
|
||||
:key="instance.path"
|
||||
>
|
||||
<RouterView v-slot="{ Component }" :key="instance.id" :route="displayedInstanceRoute">
|
||||
<template v-if="Component">
|
||||
<Suspense
|
||||
:key="instance.path"
|
||||
:key="instance.id"
|
||||
@pending="subpagePending = true"
|
||||
@resolve="subpagePending = false"
|
||||
>
|
||||
@@ -309,7 +310,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
@@ -319,12 +320,13 @@ import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_v3 } from '@/helpers/cache.js'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { instance_listener, process_listener } from '@/helpers/events'
|
||||
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
|
||||
import { get, get_full_path, kill, run } from '@/helpers/instance'
|
||||
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
@@ -333,12 +335,13 @@ import { useBreadcrumbs, useTheming } from '@/store/state'
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { addNotification, handleError } = injectNotificationManager()
|
||||
const { playServerProject } = injectServerInstall()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
|
||||
const router = useRouter()
|
||||
const displayedInstanceRoute = shallowRef(router.currentRoute.value)
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
const showInstancePlayTime = computed(() => themeStore.getFeatureFlag('show_instance_play_time'))
|
||||
@@ -377,7 +380,17 @@ const playersOnline = ref<number | undefined>(undefined)
|
||||
const ping = ref<number | undefined>(undefined)
|
||||
const loadingServerPing = ref(false)
|
||||
|
||||
function isContentSubpageRoute(routeName = route.name) {
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(nextRoute) => {
|
||||
if (nextRoute.path.startsWith('/instance')) {
|
||||
displayedInstanceRoute.value = nextRoute
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function isContentSubpageRoute(routeName = displayedInstanceRoute.value.name) {
|
||||
return typeof routeName === 'string' && contentSubpageRouteNames.has(routeName)
|
||||
}
|
||||
|
||||
@@ -395,15 +408,12 @@ async function fetchInstance() {
|
||||
|
||||
const contentPreloadPromise =
|
||||
nextInstance && isContentSubpageRoute()
|
||||
? loadInstanceContentData(nextInstance.path, undefined, handleError)
|
||||
? loadInstanceContentData(nextInstance.id, undefined, handleError)
|
||||
: Promise.resolve(null)
|
||||
|
||||
if (!offline.value && nextInstance?.linked_data && nextInstance.linked_data.project_id) {
|
||||
if (!offline.value && nextInstance?.link && nextInstance.link.project_id) {
|
||||
try {
|
||||
nextLinkedProjectV3 = await get_project_v3(
|
||||
nextInstance.linked_data.project_id,
|
||||
'must_revalidate',
|
||||
)
|
||||
nextLinkedProjectV3 = await get_project_v3(nextInstance.link.project_id, 'must_revalidate')
|
||||
|
||||
if (nextLinkedProjectV3?.minecraft_server != null) {
|
||||
nextIsServerInstance = true
|
||||
@@ -424,8 +434,8 @@ async function fetchInstance() {
|
||||
|
||||
if (nextInstance) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['worlds', nextInstance.path],
|
||||
queryFn: () => refreshWorlds(nextInstance.path),
|
||||
queryKey: ['worlds', nextInstance.id],
|
||||
queryFn: () => refreshWorlds(nextInstance.id),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
@@ -454,7 +464,7 @@ function fetchDeferredData() {
|
||||
|
||||
async function updatePlayState() {
|
||||
if (!route.params.id) return
|
||||
const runningProcesses = await get_by_profile_path(route.params.id as string).catch(handleError)
|
||||
const runningProcesses = await get_by_instance_id(route.params.id as string).catch(handleError)
|
||||
|
||||
playing.value = Array.isArray(runningProcesses) && runningProcesses.length > 0
|
||||
}
|
||||
@@ -469,7 +479,9 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const basePath = computed(() => `/instance/${encodeURIComponent(route.params.id as string)}`)
|
||||
const basePath = computed(
|
||||
() => `/instance/${encodeURIComponent(displayedInstanceRoute.value.params.id as string)}`,
|
||||
)
|
||||
|
||||
/**
|
||||
* Per-route layout mode.
|
||||
@@ -480,7 +492,7 @@ const basePath = computed(() => `/instance/${encodeURIComponent(route.params.id
|
||||
* Used by tabs whose content (e.g. the log console) needs a bounded height to resolve `h-full`.
|
||||
*/
|
||||
const renderMode = computed<'scroll' | 'fixed'>(() =>
|
||||
route.meta.renderMode === 'fixed' ? 'fixed' : 'scroll',
|
||||
displayedInstanceRoute.value.meta.renderMode === 'fixed' ? 'fixed' : 'scroll',
|
||||
)
|
||||
const isFixedRender = computed(() => renderMode.value === 'fixed')
|
||||
const contentSubpageProps = computed(() =>
|
||||
@@ -519,8 +531,8 @@ if (instance.value) {
|
||||
)
|
||||
breadcrumbs.setContext({
|
||||
name: instance.value.name,
|
||||
link: route.path,
|
||||
query: route.query,
|
||||
link: displayedInstanceRoute.value.path,
|
||||
query: displayedInstanceRoute.value.query,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -538,7 +550,7 @@ const startInstance = async (context: string) => {
|
||||
await run(route.params.id as string)
|
||||
playing.value = true
|
||||
} catch (err) {
|
||||
handleSevereError(err, { profilePath: route.params.id as string })
|
||||
handleSevereError(err, { instanceId: route.params.id as string })
|
||||
}
|
||||
loading.value = false
|
||||
|
||||
@@ -564,10 +576,10 @@ const stopInstance = async (context: string) => {
|
||||
}
|
||||
|
||||
const handlePlayServer = async () => {
|
||||
if (!instance.value?.linked_data?.project_id) return
|
||||
if (!instance.value?.link?.project_id) return
|
||||
loading.value = true
|
||||
try {
|
||||
await playServerProject(instance.value.linked_data.project_id)
|
||||
await playServerProject(instance.value.link.project_id)
|
||||
} finally {
|
||||
await updatePlayState()
|
||||
loading.value = false
|
||||
@@ -575,7 +587,39 @@ const handlePlayServer = async () => {
|
||||
}
|
||||
|
||||
const repairInstance = async () => {
|
||||
await finish_install(instance.value).catch(handleError)
|
||||
if (
|
||||
instance.value.install_stage !== 'pack_installed' &&
|
||||
(instance.value.link?.type === 'modrinth_modpack' ||
|
||||
instance.value.link?.type === 'server_project_modpack')
|
||||
) {
|
||||
await install_pack_to_existing_instance(instance.value.id, {
|
||||
type: 'fromVersionId',
|
||||
project_id: instance.value.link.project_id ?? instance.value.link.server_project_id ?? '',
|
||||
version_id: instance.value.link.version_id ?? instance.value.link.content_version_id ?? '',
|
||||
title: instance.value.name,
|
||||
}).catch(handleError)
|
||||
} else {
|
||||
await install_existing_instance(instance.value.id, false).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
const createShortcut = async () => {
|
||||
if (!instance.value) return
|
||||
try {
|
||||
const shortcutPath = await createInstanceShortcut(instance.value.name, instance.value.id)
|
||||
if (!shortcutPath) return
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Shortcut created',
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: `Error creating shortcut`,
|
||||
text: `${error}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRightClick = (event: MouseEvent) => {
|
||||
@@ -628,7 +672,7 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
||||
})
|
||||
break
|
||||
case 'open_folder':
|
||||
if (instance.value) await showProfileInFolder(instance.value.path)
|
||||
if (instance.value) await showInstanceInFolder(instance.value.id)
|
||||
break
|
||||
case 'copy_path': {
|
||||
if (instance.value) {
|
||||
@@ -640,9 +684,9 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const unlistenProfiles = await profile_listener(
|
||||
async (event: { profile_path_id: string; event: string }) => {
|
||||
if (event.profile_path_id !== route.params.id) return
|
||||
const unlistenInstances = await instance_listener(
|
||||
async (event: { instance_id: string; event: string }) => {
|
||||
if (event.instance_id !== route.params.id) return
|
||||
if (event.event === 'removed' || route.path === '/') {
|
||||
if (route.path !== '/') {
|
||||
await router.push({ path: '/' })
|
||||
@@ -656,20 +700,18 @@ const unlistenProfiles = await profile_listener(
|
||||
}
|
||||
return handleError(err)
|
||||
})
|
||||
if (!instance.value?.linked_data?.project_id) {
|
||||
if (!instance.value?.link?.project_id) {
|
||||
linkedProjectV3.value = undefined
|
||||
isServerInstance.value = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const unlistenProcesses = await process_listener(
|
||||
(e: { event: string; profile_path_id: string }) => {
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) {
|
||||
playing.value = false
|
||||
}
|
||||
},
|
||||
)
|
||||
const unlistenProcesses = await process_listener((e: { event: string; instance_id: string }) => {
|
||||
if (e.event === 'finished' && e.instance_id === route.params.id) {
|
||||
playing.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() =>
|
||||
instance.value?.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||
@@ -701,10 +743,10 @@ const timePlayedHumanized = computed(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenProfiles()
|
||||
const profilePath = route.params.id
|
||||
if (profilePath) {
|
||||
const { destroy } = useInstanceConsole(profilePath)
|
||||
unlistenInstances()
|
||||
const instanceId = displayedInstanceRoute.value.params.id
|
||||
if (instanceId) {
|
||||
const { destroy } = useInstanceConsole(instanceId)
|
||||
destroy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const profilePathId = computed(() => route.params.id)
|
||||
const instanceId = computed(() => route.params.id)
|
||||
const {
|
||||
liveConsole,
|
||||
historicalConsole,
|
||||
@@ -64,7 +64,7 @@ const {
|
||||
getHistoricalContent,
|
||||
invalidate,
|
||||
clearLive,
|
||||
} = useInstanceConsole(profilePathId.value)
|
||||
} = useInstanceConsole(instanceId.value)
|
||||
|
||||
await hydrate()
|
||||
|
||||
@@ -89,7 +89,7 @@ function buildLogList(rawLogs) {
|
||||
|
||||
const logs = ref(buildLogList([]))
|
||||
|
||||
void getHistoricalLogs(props.instance.path)
|
||||
void getHistoricalLogs()
|
||||
.then((allLogs) => {
|
||||
logs.value = buildLogList(allLogs)
|
||||
})
|
||||
@@ -146,9 +146,9 @@ const deleteDisabled = computed(() => {
|
||||
async function deleteSelectedLog() {
|
||||
const log = selectedLog.value
|
||||
if (!log || log.live) return
|
||||
await delete_logs_by_filename(props.instance.path, log.log_type, log.filename)
|
||||
await delete_logs_by_filename(props.instance.id, log.log_type, log.filename)
|
||||
invalidate()
|
||||
const freshLogs = await getHistoricalLogs(props.instance.path)
|
||||
const freshLogs = await getHistoricalLogs()
|
||||
logs.value = buildLogList(freshLogs)
|
||||
selectedLogIndex.value = 0
|
||||
}
|
||||
@@ -186,11 +186,9 @@ watch(selectedLogIndex, async (newIndex) => {
|
||||
return
|
||||
}
|
||||
|
||||
const output = await get_output_by_filename(
|
||||
props.instance.path,
|
||||
log.log_type,
|
||||
log.filename,
|
||||
).catch(handleError)
|
||||
const output = await get_output_by_filename(props.instance.id, log.log_type, log.filename).catch(
|
||||
handleError,
|
||||
)
|
||||
if (output) {
|
||||
historicalConsole.clear()
|
||||
historicalConsole.addLegacyLog(output)
|
||||
@@ -204,7 +202,7 @@ if (!props.playing) {
|
||||
}
|
||||
|
||||
const unlistenLog = await log_listener((payload) => {
|
||||
if (payload.profile_path_id !== profilePathId.value) return
|
||||
if (payload.instance_id !== instanceId.value) return
|
||||
|
||||
if (payload.type === 'log4j') {
|
||||
liveConsole.addLog4jEvent(payload)
|
||||
@@ -214,7 +212,7 @@ const unlistenLog = await log_listener((payload) => {
|
||||
})
|
||||
|
||||
const unlistenProcesses = await process_listener(async (e) => {
|
||||
if (e.profile_path_id !== profilePathId.value) return
|
||||
if (e.instance_id !== instanceId.value) return
|
||||
if (e.event === 'launched') {
|
||||
liveConsole.clear()
|
||||
invalidate()
|
||||
@@ -222,7 +220,7 @@ const unlistenProcesses = await process_listener(async (e) => {
|
||||
}
|
||||
if (e.event === 'finished') {
|
||||
invalidate()
|
||||
const freshLogs = await getHistoricalLogs(props.instance.path)
|
||||
const freshLogs = await getHistoricalLogs()
|
||||
logs.value = buildLogList(freshLogs)
|
||||
void analyseForCrash()
|
||||
}
|
||||
|
||||
@@ -10,21 +10,21 @@
|
||||
/>
|
||||
<ModpackContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="linkedModpackProject?.title"
|
||||
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
|
||||
:modpack-name="displayedModpackProject?.title"
|
||||
:modpack-icon-url="displayedModpackProject?.icon_url ?? undefined"
|
||||
:enable-toggle="!props.isServerInstance"
|
||||
:busy="isBulkOperating"
|
||||
:get-overflow-options="getOverflowOptions"
|
||||
:switch-version="handleSwitchVersion"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@bulk:enable="handleModpackContentBulkToggle"
|
||||
@bulk:disable="handleModpackContentBulkToggle"
|
||||
@bulk:enable="(items) => handleModpackContentBulkToggle(items, true)"
|
||||
@bulk:disable="(items) => handleModpackContentBulkToggle(items, false)"
|
||||
/>
|
||||
<ConfirmModpackUpdateModal
|
||||
ref="modpackUpdateConfirmModal"
|
||||
:downgrade="isModpackUpdateDowngrade"
|
||||
:backup-tip="
|
||||
[linkedModpackProject?.title, pendingModpackUpdateVersion?.version_number]
|
||||
[displayedModpackProject?.title, pendingModpackUpdateVersion?.version_number]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
"
|
||||
@@ -40,17 +40,17 @@
|
||||
:current-loader="instance.loader"
|
||||
:current-version-id="
|
||||
updatingModpack
|
||||
? (instance.linked_data?.version_id ?? '')
|
||||
? (instance.link?.version_id ?? '')
|
||||
: (updatingProject?.version?.id ?? '')
|
||||
"
|
||||
:is-app="true"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
||||
updatingModpack ? displayedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
||||
"
|
||||
:project-name="
|
||||
updatingModpack
|
||||
? (linkedModpackProject?.title ?? formatMessage(commonMessages.modpackLabel))
|
||||
? (displayedModpackProject?.title ?? formatMessage(commonMessages.modpackLabel))
|
||||
: (updatingProject?.project?.title ?? updatingProject?.file_name)
|
||||
"
|
||||
:loading="loadingVersions"
|
||||
@@ -69,6 +69,7 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ClipboardCopyIcon, FolderOpenIcon } from '@modrinth/assets'
|
||||
import {
|
||||
type BulkOperationStatus,
|
||||
commonMessages,
|
||||
ConfirmModpackUpdateModal,
|
||||
ContentCardLayout as ContentPageLayout,
|
||||
@@ -91,6 +92,7 @@ import {
|
||||
versionChangesGameVersion,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
@@ -101,25 +103,28 @@ import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_versions, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import { profile_listener } from '@/helpers/events.js'
|
||||
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
|
||||
import {
|
||||
instance_bulk_update_progress_listener,
|
||||
instance_listener,
|
||||
type InstanceBulkUpdateProgress,
|
||||
} from '@/helpers/events.js'
|
||||
import { install_duplicate_instance, installJobInstanceId } from '@/helpers/install'
|
||||
import {
|
||||
add_project_from_path,
|
||||
add_project_from_version,
|
||||
duplicate,
|
||||
edit,
|
||||
get,
|
||||
get_linked_modpack_content,
|
||||
list,
|
||||
remove_project,
|
||||
switch_project_version_with_dependencies,
|
||||
toggle_disable_project,
|
||||
update_all,
|
||||
update_managed_modrinth_version,
|
||||
update_project,
|
||||
} from '@/helpers/profile'
|
||||
} from '@/helpers/instance'
|
||||
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
|
||||
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
|
||||
import { highlightModInProfile } from '@/helpers/utils.js'
|
||||
import { highlightModInInstance } from '@/helpers/utils.js'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { installVersionDependencies } from '@/store/install'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const messages = defineMessages({
|
||||
shareTitle: {
|
||||
@@ -146,16 +151,33 @@ const messages = defineMessages({
|
||||
id: 'app.instance.mods.content-type-project',
|
||||
defaultMessage: 'project',
|
||||
},
|
||||
bulkUpdateResolvingVersions: {
|
||||
id: 'app.instance.mods.bulk-update.resolving-versions',
|
||||
defaultMessage: 'Resolving versions...',
|
||||
},
|
||||
bulkUpdateDownloadingProjects: {
|
||||
id: 'app.instance.mods.bulk-update.downloading-projects',
|
||||
defaultMessage: 'Downloading {current, number}/{total, number} projects...',
|
||||
},
|
||||
bulkUpdateFinishing: {
|
||||
id: 'app.instance.mods.bulk-update.finishing',
|
||||
defaultMessage: 'Finishing update...',
|
||||
},
|
||||
})
|
||||
|
||||
let savedModalState: ModpackContentModalState | null = null
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError, addNotification } = injectNotificationManager()
|
||||
const { installingItems } = injectContentInstall()
|
||||
const { installingItems, installRevisionByInstance, installFailureRevisionByInstance } =
|
||||
injectContentInstall()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const debug = useDebugLogger('Mods:ContentUpdate')
|
||||
const themeStore = useTheming()
|
||||
const skipNonEssentialWarnings = computed(() =>
|
||||
themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
@@ -164,13 +186,18 @@ const props = defineProps<{
|
||||
preloadedContent?: InstanceContentData | null
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
function hasPreloadedContent(contentData: InstanceContentData | null | undefined) {
|
||||
return contentData?.path === props.instance.id
|
||||
}
|
||||
|
||||
const loading = ref(!hasPreloadedContent(props.preloadedContent))
|
||||
const projects = ref<ContentItem[]>([])
|
||||
|
||||
const installingBuffer = ref<ContentItem[]>([])
|
||||
const handledInstallRevision = ref(0)
|
||||
|
||||
watch(
|
||||
() => installingItems.value.get(props.instance.path),
|
||||
() => installingItems.value.get(props.instance.id),
|
||||
(items) => {
|
||||
if (items && items.length > 0) {
|
||||
installingBuffer.value = [...items]
|
||||
@@ -188,25 +215,69 @@ watch(projects, (newProjects) => {
|
||||
})
|
||||
|
||||
const mergedProjects = computed<ContentItem[]>(() => {
|
||||
const active = installingItems.value.get(props.instance.path)
|
||||
const active = installingItems.value.get(props.instance.id)
|
||||
const pending = active ?? installingBuffer.value
|
||||
if (pending.length === 0) return projects.value
|
||||
const realProjectIds = new Set(projects.value.map((p) => p.project?.id).filter(Boolean))
|
||||
const pendingProjectIds = new Set(pending.map((p) => p.project?.id).filter(Boolean))
|
||||
const displayProjects = projects.value.map((project) =>
|
||||
project.project?.id && pendingProjectIds.has(project.project.id)
|
||||
? { ...project, installing: true }
|
||||
: project,
|
||||
)
|
||||
const realProjectIds = new Set(displayProjects.map((p) => p.project?.id).filter(Boolean))
|
||||
const placeholders = pending.filter((item) => !realProjectIds.has(item.project?.id))
|
||||
return placeholders.length > 0 ? [...projects.value, ...placeholders] : projects.value
|
||||
return placeholders.length > 0 ? [...displayProjects, ...placeholders] : displayProjects
|
||||
})
|
||||
|
||||
watch(
|
||||
() => installFailureRevisionByInstance.value.get(props.instance.id) ?? 0,
|
||||
(revision, previousRevision) => {
|
||||
if (revision === previousRevision) return
|
||||
installingBuffer.value = []
|
||||
},
|
||||
)
|
||||
|
||||
const linkedModpackProject = ref<ContentModpackCardProject | null>(null)
|
||||
const linkedModpackVersion = ref<ContentModpackCardVersion | null>(null)
|
||||
const linkedModpackOwner = ref<ContentOwner | null>(null)
|
||||
const linkedModpackCategories = ref<ContentModpackCardCategory[]>([])
|
||||
const linkedModpackHasUpdate = ref(false)
|
||||
const linkedModpackUpdateVersionId = ref<string | null>(null)
|
||||
const localImportedModpackUnlinked = ref(false)
|
||||
|
||||
const localImportedModpackProject = computed<ContentModpackCardProject | null>(() => {
|
||||
const link = props.instance.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,
|
||||
description: '',
|
||||
filename: link.filename ?? undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const displayedModpackProject = computed(
|
||||
() => linkedModpackProject.value ?? localImportedModpackProject.value,
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.instance.link,
|
||||
() => {
|
||||
localImportedModpackUnlinked.value = false
|
||||
},
|
||||
)
|
||||
|
||||
const isModpackUpdating = ref(false)
|
||||
const isBulkOperating = ref(false)
|
||||
const isInstanceBusy = computed(() => props.instance?.install_stage !== 'installed')
|
||||
const isPackLocked = computed(() => props.instance?.linked_data?.locked ?? false)
|
||||
const isPackLocked = computed(
|
||||
() =>
|
||||
props.instance?.link?.type === 'modrinth_modpack' ||
|
||||
props.instance?.link?.type === 'server_project_modpack',
|
||||
)
|
||||
|
||||
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
|
||||
const exportModal = ref(null)
|
||||
@@ -214,11 +285,16 @@ const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>
|
||||
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
|
||||
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
|
||||
|
||||
const modpackContentQueryKey = computed(() => ['linkedModpackContent', props.instance.path])
|
||||
const modpackContentQueryKey = computed(() => ['linkedModpackContent', props.instance.id])
|
||||
const modpackContentQuery = useQuery({
|
||||
queryKey: modpackContentQueryKey,
|
||||
queryFn: () => get_linked_modpack_content(props.instance.path),
|
||||
enabled: computed(() => !!props.instance?.path && !!props.instance?.linked_data),
|
||||
queryFn: () => get_linked_modpack_content(props.instance.id),
|
||||
enabled: computed(
|
||||
() =>
|
||||
!!props.instance?.id &&
|
||||
!!props.instance?.link &&
|
||||
props.instance.install_stage === 'installed',
|
||||
),
|
||||
})
|
||||
|
||||
// TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow.
|
||||
@@ -239,6 +315,38 @@ function fileNameFromPath(path: string) {
|
||||
return path.split('/').pop() ?? path
|
||||
}
|
||||
|
||||
function matchesContentItem(
|
||||
item: ContentItem,
|
||||
target: ContentItem,
|
||||
originalFileName: string,
|
||||
originalFilePath?: string,
|
||||
) {
|
||||
if (item.file_name === originalFileName || item.file_path === originalFilePath) return true
|
||||
|
||||
const projectId = target.project?.id
|
||||
if (!projectId || item.project?.id !== projectId) return false
|
||||
|
||||
const versionId = target.version?.id
|
||||
return !versionId || item.version?.id === versionId
|
||||
}
|
||||
|
||||
function updateLinkedModpackContentCache(
|
||||
target: ContentItem,
|
||||
originalFileName: string,
|
||||
originalFilePath: string | undefined,
|
||||
updates: Partial<ContentItem>,
|
||||
) {
|
||||
queryClient.setQueryData<ContentItem[]>(modpackContentQueryKey.value, (items) => {
|
||||
if (!items) return items
|
||||
|
||||
return items.map((item) =>
|
||||
matchesContentItem(item, target, originalFileName, originalFilePath)
|
||||
? { ...item, ...updates }
|
||||
: item,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function getContentItemId(item: ContentItem | null | undefined) {
|
||||
return item?.file_path ?? item?.file_name ?? item?.id ?? ''
|
||||
}
|
||||
@@ -254,6 +362,10 @@ function hasContentOperation(item: ContentItem) {
|
||||
return keys.some((key) => activeContentOperationKeys.value.has(key))
|
||||
}
|
||||
|
||||
function canUpdateProject(item: ContentItem) {
|
||||
return !!item.file_path && !!item.has_update && !!item.update_version_id
|
||||
}
|
||||
|
||||
function setContentItemBusy(item: ContentItem, busy: boolean, originalFileName = item.file_name) {
|
||||
item.installing = busy
|
||||
modpackContentModal.value?.updateItem(originalFileName, {
|
||||
@@ -362,7 +474,7 @@ async function handleBrowseContent() {
|
||||
if (!props.instance) return
|
||||
await router.push({
|
||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
||||
query: { i: props.instance.path },
|
||||
query: { i: props.instance.id },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -376,7 +488,7 @@ async function handleUploadFiles() {
|
||||
const path = (file as { path?: string }).path ?? file
|
||||
const fileName = typeof path === 'string' ? (path.split('/').pop() ?? path) : String(path)
|
||||
try {
|
||||
await add_project_from_path(props.instance.path, path)
|
||||
await add_project_from_path(props.instance.id, path)
|
||||
addedFiles.push(fileName)
|
||||
} catch (e) {
|
||||
handleError(e as Error)
|
||||
@@ -402,21 +514,28 @@ async function handleUploadFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleDisableMod(mod: ContentItem) {
|
||||
async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
|
||||
if (!mod.file_path) return
|
||||
const operation = beginContentOperation(mod)
|
||||
if (!operation) return
|
||||
const originalFilePath = mod.file_path
|
||||
|
||||
try {
|
||||
const newPath = await toggle_disable_project(props.instance.path, mod.file_path)
|
||||
const newPath = await toggle_disable_project(props.instance.id, mod.file_path, desiredEnabled)
|
||||
const newFileName = fileNameFromPath(newPath)
|
||||
const enabled = !newPath.endsWith('.disabled')
|
||||
mod.file_path = newPath
|
||||
mod.file_name = newFileName
|
||||
mod.enabled = !mod.enabled
|
||||
mod.enabled = enabled
|
||||
modpackContentModal.value?.updateItem(operation.originalFileName, {
|
||||
file_path: newPath,
|
||||
file_name: newFileName,
|
||||
enabled: mod.enabled,
|
||||
enabled,
|
||||
})
|
||||
updateLinkedModpackContentCache(mod, operation.originalFileName, originalFilePath, {
|
||||
file_path: newPath,
|
||||
file_name: newFileName,
|
||||
enabled,
|
||||
})
|
||||
|
||||
trackEvent('InstanceProjectDisable', {
|
||||
@@ -425,7 +544,7 @@ async function toggleDisableMod(mod: ContentItem) {
|
||||
id: mod.project?.id,
|
||||
name: mod.project?.title ?? mod.file_name,
|
||||
project_type: mod.project_type,
|
||||
disabled: !mod.enabled,
|
||||
disabled: !enabled,
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
@@ -443,7 +562,7 @@ async function removeMod(mod: ContentItem) {
|
||||
|
||||
try {
|
||||
const removedPath = mod.file_path
|
||||
await remove_project(props.instance.path, removedPath)
|
||||
await remove_project(props.instance.id, removedPath)
|
||||
projects.value = projects.value.filter((x) => removedPath !== x.file_path)
|
||||
|
||||
trackEvent('InstanceProjectRemove', {
|
||||
@@ -517,26 +636,68 @@ async function getDeleteDependencyWarning(items: ContentItem[]) {
|
||||
return dependents.length > 0 ? { items, dependents } : null
|
||||
}
|
||||
|
||||
function formatBulkUpdateProgress(progress: InstanceBulkUpdateProgress): BulkOperationStatus {
|
||||
if (progress.stage === 'resolving_versions') {
|
||||
return {
|
||||
message: formatMessage(messages.bulkUpdateResolvingVersions),
|
||||
waiting: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (progress.stage === 'finishing') {
|
||||
return {
|
||||
message: formatMessage(messages.bulkUpdateFinishing),
|
||||
progress: progress.current,
|
||||
total: progress.total,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: formatMessage(messages.bulkUpdateDownloadingProjects, {
|
||||
current: progress.current,
|
||||
total: progress.total,
|
||||
}),
|
||||
progress: progress.current,
|
||||
total: progress.total,
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkUpdateAllProjects(onProgress?: (status: BulkOperationStatus) => void) {
|
||||
let unlisten: (() => void) | null = null
|
||||
try {
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
message: formatMessage(messages.bulkUpdateResolvingVersions),
|
||||
waiting: true,
|
||||
})
|
||||
unlisten = await instance_bulk_update_progress_listener((progress) => {
|
||||
if (progress.instanceId !== props.instance.id) return
|
||||
onProgress(formatBulkUpdateProgress(progress))
|
||||
})
|
||||
}
|
||||
|
||||
await update_all(props.instance.id)
|
||||
await refreshContentState('must_revalidate')
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
throw err
|
||||
} finally {
|
||||
unlisten?.()
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProject(mod: ContentItem) {
|
||||
if (!mod.file_path) return
|
||||
if (!canUpdateProject(mod)) return
|
||||
const operation = beginContentOperation(mod)
|
||||
if (!operation) return
|
||||
|
||||
try {
|
||||
const updateVersionId = mod.update_version_id
|
||||
await update_project(props.instance.path, mod.file_path)
|
||||
|
||||
if (updateVersionId) {
|
||||
const versionData = await get_version(updateVersionId, 'must_revalidate').catch(handleError)
|
||||
|
||||
if (versionData) {
|
||||
const profile = await get(props.instance.path).catch(handleError)
|
||||
|
||||
if (profile) {
|
||||
await installVersionDependencies(profile, versionData, 'update').catch(handleError)
|
||||
}
|
||||
}
|
||||
}
|
||||
const updateVersionId = mod.update_version_id!
|
||||
await switch_project_version_with_dependencies(
|
||||
props.instance.id,
|
||||
mod.file_path,
|
||||
updateVersionId,
|
||||
)
|
||||
|
||||
trackEvent('InstanceProjectUpdate', {
|
||||
loader: props.instance.loader,
|
||||
@@ -560,27 +721,9 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
|
||||
if (!operation) return
|
||||
|
||||
const oldPath = mod.file_path
|
||||
const wasDisabled = mod.enabled === false || oldPath.endsWith('.disabled')
|
||||
let newPath: string | null = null
|
||||
let shouldRemoveNewOnError = false
|
||||
|
||||
try {
|
||||
newPath = await add_project_from_version(props.instance.path, version.id, 'update')
|
||||
shouldRemoveNewOnError = newPath !== oldPath
|
||||
|
||||
if (wasDisabled) {
|
||||
newPath = await toggle_disable_project(props.instance.path, newPath)
|
||||
}
|
||||
|
||||
const profile = await get(props.instance.path).catch(handleError)
|
||||
if (profile) {
|
||||
await installVersionDependencies(profile, version, 'update').catch(handleError)
|
||||
}
|
||||
|
||||
shouldRemoveNewOnError = false
|
||||
if (newPath !== oldPath) {
|
||||
await remove_project(props.instance.path, oldPath)
|
||||
}
|
||||
await switch_project_version_with_dependencies(props.instance.id, oldPath, version.id)
|
||||
|
||||
trackEvent('InstanceProjectUpdate', {
|
||||
loader: props.instance.loader,
|
||||
@@ -590,9 +733,6 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
|
||||
project_type: mod.project_type,
|
||||
})
|
||||
} catch (err) {
|
||||
if (shouldRemoveNewOnError && newPath && newPath !== oldPath) {
|
||||
await remove_project(props.instance.path, newPath).catch(() => {})
|
||||
}
|
||||
handleError(err as Error)
|
||||
} finally {
|
||||
await refreshContentState('must_revalidate')
|
||||
@@ -602,7 +742,7 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
|
||||
|
||||
async function handleUpdate(id: string) {
|
||||
const item = projects.value.find((p) => getContentItemId(p) === id)
|
||||
if (!item?.has_update || !item.project?.id || !item.version?.id) return
|
||||
if (!item || !canUpdateProject(item) || !item.project?.id || !item.version?.id) return
|
||||
|
||||
const requestId = beginUpdateRequest()
|
||||
const itemId = getContentItemId(item)
|
||||
@@ -642,11 +782,11 @@ async function handleUpdate(id: string) {
|
||||
updateVersionId: item.update_version_id,
|
||||
},
|
||||
instance: {
|
||||
path: props.instance.path,
|
||||
path: props.instance.id,
|
||||
name: props.instance.name,
|
||||
gameVersion: props.instance.game_version,
|
||||
loader: props.instance.loader,
|
||||
linkedData: props.instance.linked_data,
|
||||
link: props.instance.link,
|
||||
},
|
||||
modalStateBeforeFetch: {
|
||||
updatingModpack: updatingModpack.value,
|
||||
@@ -738,18 +878,18 @@ async function handleSwitchVersion(item: ContentItem) {
|
||||
updatingProjectVersions.value = versions
|
||||
}
|
||||
|
||||
async function handleModpackContentToggle(item: ContentItem) {
|
||||
await toggleDisableDebounced(item)
|
||||
async function handleModpackContentToggle(item: ContentItem, enabled: boolean) {
|
||||
await toggleDisableDebounced(item, enabled)
|
||||
}
|
||||
|
||||
async function handleModpackContentBulkToggle(items: ContentItem[]) {
|
||||
await Promise.all(items.map((item) => toggleDisableMod(item)))
|
||||
async function handleModpackContentBulkToggle(items: ContentItem[], enabled: boolean) {
|
||||
await Promise.all(items.map((item) => toggleDisableMod(item, enabled)))
|
||||
}
|
||||
|
||||
async function handleModpackContent() {
|
||||
if (!props.instance?.path) return
|
||||
if (!props.instance?.id) return
|
||||
|
||||
if (modpackContentQuery.data.value !== undefined) {
|
||||
if (modpackContentQuery.data.value?.length) {
|
||||
modpackContentModal.value?.show(modpackContentQuery.data.value)
|
||||
return
|
||||
}
|
||||
@@ -767,12 +907,12 @@ async function handleModpackContent() {
|
||||
}
|
||||
|
||||
async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
|
||||
if (!props.instance?.path) return
|
||||
if (!props.instance?.id) return
|
||||
|
||||
const contentItems = await queryClient
|
||||
.fetchQuery({
|
||||
queryKey: modpackContentQueryKey.value,
|
||||
queryFn: () => get_linked_modpack_content(props.instance.path, cacheBehaviour),
|
||||
queryFn: () => get_linked_modpack_content(props.instance.id, cacheBehaviour),
|
||||
})
|
||||
.catch(handleError)
|
||||
|
||||
@@ -786,8 +926,17 @@ async function refreshContentState(cacheBehaviour?: CacheBehaviour) {
|
||||
await refreshModpackContentItems(cacheBehaviour)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => installRevisionByInstance.value.get(props.instance.id) ?? 0,
|
||||
async (revision) => {
|
||||
if (revision <= handledInstallRevision.value) return
|
||||
handledInstallRevision.value = revision
|
||||
await refreshContentState('must_revalidate')
|
||||
},
|
||||
)
|
||||
|
||||
async function handleModpackUpdate() {
|
||||
if (!props.instance?.linked_data?.project_id) return
|
||||
if (!props.instance?.link?.project_id) return
|
||||
|
||||
const requestId = beginUpdateRequest()
|
||||
|
||||
@@ -800,7 +949,7 @@ async function handleModpackUpdate() {
|
||||
await nextTick()
|
||||
|
||||
const initialVersionId =
|
||||
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined
|
||||
linkedModpackUpdateVersionId.value ?? props.instance?.link?.version_id ?? undefined
|
||||
debug('handleModpackUpdate: opening modpack updater modal', {
|
||||
type: 'modpack',
|
||||
initialVersionId,
|
||||
@@ -809,11 +958,11 @@ async function handleModpackUpdate() {
|
||||
linkedModpackVersion: linkedModpackVersion.value,
|
||||
linkedModpackHasUpdate: linkedModpackHasUpdate.value,
|
||||
instance: {
|
||||
path: props.instance.path,
|
||||
path: props.instance.id,
|
||||
name: props.instance.name,
|
||||
gameVersion: props.instance.game_version,
|
||||
loader: props.instance.loader,
|
||||
linkedData: props.instance.linked_data,
|
||||
link: props.instance.link,
|
||||
},
|
||||
modalStateBeforeFetch: {
|
||||
updatingModpack: updatingModpack.value,
|
||||
@@ -829,10 +978,7 @@ async function handleModpackUpdate() {
|
||||
})
|
||||
contentUpdaterModal.value?.show(initialVersionId)
|
||||
|
||||
const versions = await getUpdaterProjectVersions(
|
||||
props.instance.linked_data.project_id,
|
||||
initialVersionId,
|
||||
)
|
||||
const versions = await getUpdaterProjectVersions(props.instance.link.project_id, initialVersionId)
|
||||
|
||||
if (!isActiveUpdateRequest(requestId) || !updatingModpack.value) return
|
||||
|
||||
@@ -857,7 +1003,7 @@ async function handleModpackUpdate() {
|
||||
: null,
|
||||
versionCount: versions.length,
|
||||
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
|
||||
currentLinkedVersionId: props.instance.linked_data.version_id,
|
||||
currentLinkedVersionId: props.instance.link.version_id,
|
||||
})
|
||||
|
||||
updatingProjectVersions.value = versions
|
||||
@@ -909,7 +1055,7 @@ function resetUpdateState() {
|
||||
async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
|
||||
pendingModpackUpdateVersion.value = selectedVersion
|
||||
|
||||
const currentVersionId = props.instance?.linked_data?.version_id
|
||||
const currentVersionId = props.instance?.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)
|
||||
@@ -918,7 +1064,7 @@ async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.
|
||||
isModpackUpdateDowngrade.value ||
|
||||
versionChangesGameVersion(selectedVersion, props.instance.game_version)
|
||||
|
||||
if (!shouldShowWarning) {
|
||||
if (skipNonEssentialWarnings.value || !shouldShowWarning) {
|
||||
await handleModpackUpdateConfirm()
|
||||
return
|
||||
}
|
||||
@@ -927,7 +1073,7 @@ async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.
|
||||
}
|
||||
|
||||
async function handleModpackUpdateConfirm() {
|
||||
if (!pendingModpackUpdateVersion.value || !props.instance?.path) return
|
||||
if (!pendingModpackUpdateVersion.value || !props.instance?.id) return
|
||||
|
||||
const version = pendingModpackUpdateVersion.value
|
||||
pendingModpackUpdateVersion.value = null
|
||||
@@ -935,7 +1081,7 @@ async function handleModpackUpdateConfirm() {
|
||||
contentUpdaterModal.value?.hide()
|
||||
isModpackUpdating.value = true
|
||||
try {
|
||||
await update_managed_modrinth_version(props.instance.path, version.id)
|
||||
await update_managed_modrinth_version(props.instance.id, version.id)
|
||||
await initProjects()
|
||||
} finally {
|
||||
isModpackUpdating.value = false
|
||||
@@ -973,15 +1119,16 @@ async function handleModalUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
async function unpairProfile() {
|
||||
await edit(props.instance.path, {
|
||||
linked_data: null as unknown as undefined,
|
||||
async function unpairInstance() {
|
||||
await edit(props.instance.id, {
|
||||
link: null as unknown as undefined,
|
||||
})
|
||||
linkedModpackProject.value = null
|
||||
linkedModpackVersion.value = null
|
||||
linkedModpackOwner.value = null
|
||||
linkedModpackHasUpdate.value = false
|
||||
linkedModpackUpdateVersionId.value = null
|
||||
localImportedModpackUnlinked.value = true
|
||||
await initProjects()
|
||||
}
|
||||
|
||||
@@ -1025,7 +1172,7 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
||||
options.push({
|
||||
id: formatMessage(commonMessages.showFileButton),
|
||||
icon: FolderOpenIcon,
|
||||
action: () => highlightModInProfile(props.instance.path, item.file_path),
|
||||
action: () => highlightModInInstance(props.instance.id, item.file_path),
|
||||
})
|
||||
|
||||
if (item.project?.slug) {
|
||||
@@ -1046,23 +1193,24 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
||||
async function initProjects(cacheBehaviour?: CacheBehaviour) {
|
||||
if (!props.instance) return
|
||||
|
||||
const contentData = await loadInstanceContentData(
|
||||
props.instance.path,
|
||||
cacheBehaviour,
|
||||
handleError,
|
||||
)
|
||||
const contentData = await loadInstanceContentData(props.instance.id, cacheBehaviour, handleError)
|
||||
applyContentData(contentData)
|
||||
}
|
||||
|
||||
function applyContentData(contentData: InstanceContentData) {
|
||||
if (contentData.path !== props.instance.path) return false
|
||||
if (contentData.path !== props.instance.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!contentData.contentItems) {
|
||||
loading.value = false
|
||||
return true
|
||||
}
|
||||
|
||||
projects.value = contentData.contentItems
|
||||
projects.value = contentData.contentItems.map((item) => ({
|
||||
...item,
|
||||
has_update: canUpdateProject(item),
|
||||
}))
|
||||
|
||||
if (contentData.modpack) {
|
||||
linkedModpackProject.value = contentData.modpack.project
|
||||
@@ -1086,15 +1234,18 @@ function applyContentData(contentData: InstanceContentData) {
|
||||
|
||||
provideAppBackup({
|
||||
async createBackup() {
|
||||
const allProfiles = await list()
|
||||
const allInstances = await list()
|
||||
const prefix = `${props.instance.name} - Backup #`
|
||||
const existingNums = allProfiles
|
||||
const existingNums = allInstances
|
||||
.filter((p) => p.name.startsWith(prefix))
|
||||
.map((p) => parseInt(p.name.slice(prefix.length), 10))
|
||||
.filter((n) => !isNaN(n))
|
||||
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
|
||||
const newPath = await duplicate(props.instance.path)
|
||||
await edit(newPath, { name: `${prefix}${nextNum}` })
|
||||
const job = await install_duplicate_instance(props.instance.id)
|
||||
const newInstanceId = installJobInstanceId(job)
|
||||
if (newInstanceId) {
|
||||
await edit(newInstanceId, { name: `${prefix}${nextNum}` })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1109,53 +1260,68 @@ provideContentManager({
|
||||
items: mergedProjects,
|
||||
loading,
|
||||
error: ref(null),
|
||||
modpack: computed(() =>
|
||||
linkedModpackProject.value
|
||||
? {
|
||||
project: linkedModpackProject.value,
|
||||
projectLink: {
|
||||
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
|
||||
query: { i: props.instance.path },
|
||||
},
|
||||
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.path },
|
||||
}
|
||||
: undefined,
|
||||
owner: linkedModpackOwner.value
|
||||
modpack: computed(() => {
|
||||
if (linkedModpackProject.value) {
|
||||
return {
|
||||
project: linkedModpackProject.value,
|
||||
projectLink: {
|
||||
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
|
||||
query: { i: props.instance.id },
|
||||
},
|
||||
version: linkedModpackVersion.value ?? undefined,
|
||||
versionLink:
|
||||
linkedModpackProject.value && linkedModpackVersion.value
|
||||
? {
|
||||
...linkedModpackOwner.value,
|
||||
link: () =>
|
||||
openUrl(
|
||||
`https://modrinth.com/${linkedModpackOwner.value!.type}/${linkedModpackOwner.value!.id}`,
|
||||
),
|
||||
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`,
|
||||
query: { i: props.instance.id },
|
||||
}
|
||||
: undefined,
|
||||
categories: linkedModpackCategories.value,
|
||||
hasUpdate: linkedModpackHasUpdate.value,
|
||||
disabled: isModpackUpdating.value,
|
||||
disabledText: isModpackUpdating.value
|
||||
? formatMessage(commonMessages.updatingLabel)
|
||||
: formatMessage(commonMessages.installingLabel),
|
||||
}
|
||||
: null,
|
||||
),
|
||||
owner: linkedModpackOwner.value
|
||||
? {
|
||||
...linkedModpackOwner.value,
|
||||
link: () =>
|
||||
openUrl(
|
||||
`https://modrinth.com/${linkedModpackOwner.value!.type}/${linkedModpackOwner.value!.id}`,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
categories: linkedModpackCategories.value,
|
||||
hasUpdate: linkedModpackHasUpdate.value,
|
||||
disabled: isModpackUpdating.value,
|
||||
disabledText: isModpackUpdating.value
|
||||
? formatMessage(commonMessages.updatingLabel)
|
||||
: formatMessage(commonMessages.installingLabel),
|
||||
}
|
||||
}
|
||||
|
||||
if (localImportedModpackProject.value) {
|
||||
return {
|
||||
project: localImportedModpackProject.value,
|
||||
categories: [],
|
||||
hasUpdate: false,
|
||||
disabled: isModpackUpdating.value,
|
||||
disabledText: isModpackUpdating.value
|
||||
? formatMessage(commonMessages.updatingLabel)
|
||||
: formatMessage(commonMessages.installingLabel),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}),
|
||||
isPackLocked,
|
||||
isBusy: isInstanceBusy,
|
||||
isBulkOperating,
|
||||
skipNonEssentialWarnings,
|
||||
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
|
||||
toggleEnabled: toggleDisableDebounced,
|
||||
bulkEnableItems: (items: ContentItem[]) =>
|
||||
Promise.all(items.filter((item) => !item.enabled).map((item) => toggleDisableMod(item))).then(
|
||||
() => {},
|
||||
),
|
||||
Promise.all(
|
||||
items.filter((item) => !item.enabled).map((item) => toggleDisableMod(item, true)),
|
||||
).then(() => {}),
|
||||
bulkDisableItems: (items: ContentItem[]) =>
|
||||
Promise.all(items.filter((item) => item.enabled).map((item) => toggleDisableMod(item))).then(
|
||||
() => {},
|
||||
),
|
||||
Promise.all(
|
||||
items.filter((item) => item.enabled).map((item) => toggleDisableMod(item, false)),
|
||||
).then(() => {}),
|
||||
deleteItem: removeMod,
|
||||
bulkDeleteItems: (items: ContentItem[]) =>
|
||||
Promise.all(items.map((item) => removeMod(item))).then(() => {}),
|
||||
@@ -1165,10 +1331,11 @@ provideContentManager({
|
||||
uploadFiles: handleUploadFiles,
|
||||
hasUpdateSupport: true,
|
||||
updateItem: handleUpdate,
|
||||
bulkUpdateAll: bulkUpdateAllProjects,
|
||||
bulkUpdateItem: updateProject,
|
||||
updateModpack: props.isServerInstance ? undefined : handleModpackUpdate,
|
||||
viewModpackContent: handleModpackContent,
|
||||
unlinkModpack: unpairProfile,
|
||||
unlinkModpack: unpairInstance,
|
||||
openSettings: props.openSettings,
|
||||
switchVersion: handleSwitchVersion,
|
||||
getOverflowOptions,
|
||||
@@ -1184,9 +1351,7 @@ provideContentManager({
|
||||
title: item.file_name.replace('.disabled', ''),
|
||||
icon_url: null,
|
||||
},
|
||||
projectLink: item.project?.id
|
||||
? { path: `/project/${item.project.id}`, query: { i: props.instance.path } }
|
||||
: undefined,
|
||||
projectLink: item.project?.id ? { path: `/project/${item.project.id}` } : undefined,
|
||||
version: item.version ?? {
|
||||
id: item.file_name,
|
||||
version_number: formatMessage(commonMessages.unknownLabel),
|
||||
@@ -1196,7 +1361,6 @@ provideContentManager({
|
||||
item.project?.id && item.version?.id
|
||||
? {
|
||||
path: `/project/${item.project.id}/version/${item.version.id}`,
|
||||
query: { i: props.instance.path },
|
||||
}
|
||||
: undefined,
|
||||
owner: item.owner
|
||||
@@ -1208,7 +1372,7 @@ provideContentManager({
|
||||
enabled: item.enabled,
|
||||
installing: item.installing,
|
||||
}),
|
||||
filterPersistKey: props.instance.path,
|
||||
filterPersistKey: props.instance.id,
|
||||
})
|
||||
|
||||
type UnlistenFn = () => void
|
||||
@@ -1216,7 +1380,17 @@ type UnlistenFn = () => void
|
||||
const initialContentReady = loadInitialContent()
|
||||
void initialContentReady.then(restoreModpackContentModalState).catch(handleError)
|
||||
|
||||
function getInstallRevision() {
|
||||
return installRevisionByInstance.value.get(props.instance.id) ?? 0
|
||||
}
|
||||
|
||||
function loadInitialContent() {
|
||||
const installRevision = getInstallRevision()
|
||||
if (installRevision > handledInstallRevision.value) {
|
||||
handledInstallRevision.value = installRevision
|
||||
return initProjects('must_revalidate')
|
||||
}
|
||||
|
||||
if (props.preloadedContent && applyContentData(props.preloadedContent)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -1241,7 +1415,7 @@ const removeBeforeEach = router.beforeEach(() => {
|
||||
|
||||
let isUnmounted = false
|
||||
let unlistenDragDrop: UnlistenFn | null = null
|
||||
let unlistenProfiles: UnlistenFn | null = null
|
||||
let unlistenInstances: UnlistenFn | null = null
|
||||
|
||||
onMounted(() => {
|
||||
void getCurrentWebview()
|
||||
@@ -1250,7 +1424,7 @@ onMounted(() => {
|
||||
|
||||
for (const file of event.payload.paths) {
|
||||
if (file.endsWith('.mrpack')) continue
|
||||
await add_project_from_path(props.instance.path, file).catch(handleError)
|
||||
await add_project_from_path(props.instance.id, file).catch(handleError)
|
||||
}
|
||||
await initProjects()
|
||||
})
|
||||
@@ -1264,10 +1438,10 @@ onMounted(() => {
|
||||
})
|
||||
.catch(handleError)
|
||||
|
||||
void profile_listener(async (event: { event: string; profile_path_id: string }) => {
|
||||
void instance_listener(async (event: { event: string; instance_id: string }) => {
|
||||
if (
|
||||
props.instance &&
|
||||
event.profile_path_id === props.instance.path &&
|
||||
event.instance_id === props.instance.id &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing' &&
|
||||
!isBulkOperating.value
|
||||
@@ -1281,7 +1455,7 @@ onMounted(() => {
|
||||
return
|
||||
}
|
||||
|
||||
unlistenProfiles = unlisten
|
||||
unlistenInstances = unlisten
|
||||
})
|
||||
.catch(handleError)
|
||||
})
|
||||
@@ -1290,7 +1464,7 @@ watch(
|
||||
() => props.instance?.install_stage,
|
||||
async (newStage, oldStage) => {
|
||||
if (oldStage !== 'installed' && newStage === 'installed') {
|
||||
await initProjects('must_revalidate')
|
||||
await refreshContentState('must_revalidate')
|
||||
} else if (oldStage === 'not_installed' && newStage === 'pack_installing') {
|
||||
await initProjects()
|
||||
}
|
||||
@@ -1298,16 +1472,16 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.instance?.linked_data,
|
||||
async (newLinkedData, oldLinkedData) => {
|
||||
if (oldLinkedData && !newLinkedData) {
|
||||
() => props.instance?.link,
|
||||
async (newInstanceLink, oldInstanceLink) => {
|
||||
if (oldInstanceLink && !newInstanceLink) {
|
||||
await initProjects('must_revalidate')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.instance?.preferred_update_channel,
|
||||
() => props.instance?.update_channel,
|
||||
async (newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
await initProjects('must_revalidate')
|
||||
@@ -1319,6 +1493,6 @@ onUnmounted(() => {
|
||||
isUnmounted = true
|
||||
removeBeforeEach()
|
||||
unlistenDragDrop?.()
|
||||
unlistenProfiles?.()
|
||||
unlistenInstances?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -13,29 +13,10 @@
|
||||
/>
|
||||
<EditServerModal ref="editServerModal" :instance="instance" @submit="editServer" />
|
||||
<EditWorldModal ref="editWorldModal" :instance="instance" @submit="editWorld" />
|
||||
<ConfirmModalWrapper
|
||||
ref="removeServerModal"
|
||||
:title="
|
||||
formatMessage(messages.removeServerTitle, {
|
||||
name: serverToRemove?.name ?? formatMessage(messages.thisServer),
|
||||
})
|
||||
"
|
||||
:description="
|
||||
serverToRemove?.address === serverToRemove?.name
|
||||
? formatMessage(messages.removeServerDescription, { name: serverToRemove?.name })
|
||||
: formatMessage(messages.removeServerDescriptionWithAddress, {
|
||||
name: serverToRemove?.name,
|
||||
address: serverToRemove?.address,
|
||||
})
|
||||
"
|
||||
:markdown="false"
|
||||
@proceed="proceedRemoveServer"
|
||||
/>
|
||||
<ConfirmModalWrapper
|
||||
ref="deleteWorldModal"
|
||||
:title="formatMessage(messages.deleteWorldTitle)"
|
||||
:description="formatMessage(messages.deleteWorldDescription, { name: worldToDelete?.name })"
|
||||
@proceed="proceedDeleteWorld"
|
||||
<ConfirmRemoveWorldModal
|
||||
ref="removeWorldModal"
|
||||
:world="worldToRemove"
|
||||
@confirm="proceedRemoveWorld"
|
||||
/>
|
||||
<ReadyTransition :pending="worldsReadyPending">
|
||||
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
|
||||
@@ -64,7 +45,7 @@
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
||||
router.push({ path: '/browse/server', query: { i: instance.id, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
@@ -117,6 +98,7 @@
|
||||
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
|
||||
"
|
||||
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
|
||||
:shortcut-instance-id="instance.id"
|
||||
@play="() => joinWorld(world)"
|
||||
@stop="() => emit('stop')"
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
@@ -129,7 +111,7 @@
|
||||
: editServerModal?.show(world)
|
||||
"
|
||||
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.id, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,7 +132,7 @@
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
||||
router.push({ path: '/browse/server', query: { i: instance.id, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
@@ -182,32 +164,33 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||
import ConfirmRemoveWorldModal from '@/components/ui/world/modal/ConfirmRemoveWorldModal.vue'
|
||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project, get_project_v3 } from '@/helpers/cache.js'
|
||||
import { profile_listener } from '@/helpers/events'
|
||||
import { instance_listener } from '@/helpers/events'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
|
||||
import {
|
||||
delete_world,
|
||||
get_profile_protocol_version,
|
||||
get_instance_protocol_version,
|
||||
getServerDomainKey,
|
||||
getWorldIdentifier,
|
||||
handleDefaultProfileUpdateEvent,
|
||||
handleDefaultInstanceUpdateEvent,
|
||||
hasServerQuickPlaySupport,
|
||||
hasWorldQuickPlaySupport,
|
||||
type InstanceEvent,
|
||||
normalizeServerAddress,
|
||||
type ProfileEvent,
|
||||
type ProtocolVersion,
|
||||
refreshServerData,
|
||||
refreshServers,
|
||||
refreshWorld,
|
||||
refreshWorlds,
|
||||
remove_server_from_profile,
|
||||
remove_server_from_instance,
|
||||
resolveManagedServerWorld,
|
||||
type ServerData,
|
||||
type ServerWorld,
|
||||
@@ -220,32 +203,8 @@ import {
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { ensureManagedServerWorldExists, getServerAddress } from '@/store/install'
|
||||
|
||||
const messages = defineMessages({
|
||||
removeServerTitle: {
|
||||
id: 'app.instance.worlds.remove-server-title',
|
||||
defaultMessage: 'Are you sure you want to remove {name}?',
|
||||
},
|
||||
removeServerDescription: {
|
||||
id: 'app.instance.worlds.remove-server-description',
|
||||
defaultMessage:
|
||||
"'{name}' will be removed from your list, including in-game, and there will be no way to recover it.",
|
||||
},
|
||||
removeServerDescriptionWithAddress: {
|
||||
id: 'app.instance.worlds.remove-server-description-with-address',
|
||||
defaultMessage:
|
||||
"'{name}' ({address}) will be removed from your list, including in-game, and there will be no way to recover it.",
|
||||
},
|
||||
deleteWorldTitle: {
|
||||
id: 'app.instance.worlds.delete-world-title',
|
||||
defaultMessage: 'Are you sure you want to permanently delete this world?',
|
||||
},
|
||||
deleteWorldDescription: {
|
||||
id: 'app.instance.worlds.delete-world-description',
|
||||
defaultMessage:
|
||||
"'{name}' will be **permanently deleted**, and there will be no way to recover it.",
|
||||
},
|
||||
searchWorldsPlaceholder: {
|
||||
id: 'app.instance.worlds.search-worlds-placeholder',
|
||||
defaultMessage: 'Search {count} worlds...',
|
||||
@@ -266,10 +225,6 @@ const messages = defineMessages({
|
||||
id: 'app.instance.worlds.no-worlds-description',
|
||||
defaultMessage: 'Add a server or browse to get started',
|
||||
},
|
||||
thisServer: {
|
||||
id: 'app.instance.worlds.this-server',
|
||||
defaultMessage: 'this server',
|
||||
},
|
||||
vanillaFilter: {
|
||||
id: 'app.instance.worlds.filter-vanilla',
|
||||
defaultMessage: 'Vanilla',
|
||||
@@ -297,11 +252,9 @@ const router = useRouter()
|
||||
const addServerModal = ref<InstanceType<typeof AddServerModal>>()
|
||||
const editServerModal = ref<InstanceType<typeof EditServerModal>>()
|
||||
const editWorldModal = ref<InstanceType<typeof EditWorldModal>>()
|
||||
const removeServerModal = ref<InstanceType<typeof ConfirmModalWrapper>>()
|
||||
const deleteWorldModal = ref<InstanceType<typeof ConfirmModalWrapper>>()
|
||||
const removeWorldModal = ref<InstanceType<typeof ConfirmRemoveWorldModal>>()
|
||||
|
||||
const serverToRemove = ref<ServerWorld>()
|
||||
const worldToDelete = ref<SingleplayerWorld>()
|
||||
const worldToRemove = ref<World | null>(null)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'play', world: World): void
|
||||
@@ -357,8 +310,8 @@ const startingInstance = ref(false)
|
||||
const worldPlaying = ref<World>()
|
||||
|
||||
const worldsQuery = useQuery({
|
||||
queryKey: computed(() => ['worlds', instance.value.path]),
|
||||
queryFn: () => refreshWorlds(instance.value.path),
|
||||
queryKey: computed(() => ['worlds', instance.value.id]),
|
||||
queryFn: () => refreshWorlds(instance.value.id),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
@@ -406,12 +359,12 @@ function isManagedServerWorld(world: World): world is ServerWorld {
|
||||
|
||||
async function refreshManagedServerMetadata() {
|
||||
await ensureManagedServerWorldExists(
|
||||
instance.value.path,
|
||||
instance.value.id,
|
||||
managedServerName.value,
|
||||
managedServerAddress.value,
|
||||
)
|
||||
|
||||
const projectId = instance.value.linked_data?.project_id
|
||||
const projectId = instance.value.link?.project_id
|
||||
if (!projectId) {
|
||||
managedServerName.value = null
|
||||
managedServerAddress.value = null
|
||||
@@ -441,7 +394,7 @@ async function refreshManagedServerMetadata() {
|
||||
managedServerAddress.value = serverAddress
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to resolve managed server metadata for profile: ${instance.value.path}`,
|
||||
`Failed to resolve managed server metadata for instance: ${instance.value.id}`,
|
||||
err,
|
||||
)
|
||||
managedServerName.value = null
|
||||
@@ -450,22 +403,22 @@ async function refreshManagedServerMetadata() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => instance.value.linked_data?.project_id,
|
||||
() => instance.value.link?.project_id,
|
||||
async () => {
|
||||
await refreshManagedServerMetadata()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
let unlistenProfile: (() => void) | null = null
|
||||
let unlistenInstance: (() => void) | null = null
|
||||
let worldsTabAlive = true
|
||||
|
||||
async function initWorldsTab() {
|
||||
const [_unlistenProfile, resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
const [_unlistenInstance, resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
instance_listener(async (e: InstanceEvent) => {
|
||||
if (e.instance_id !== instance.value.id) return
|
||||
|
||||
console.info(`Handling profile event '${e.event}' for profile: ${e.profile_path_id}`)
|
||||
console.info(`Handling instance event '${e.event}' for instance: ${e.instance_id}`)
|
||||
|
||||
if (e.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
@@ -474,18 +427,18 @@ async function initWorldsTab() {
|
||||
await refreshAllWorlds()
|
||||
}
|
||||
|
||||
await handleDefaultProfileUpdateEvent(worlds.value, instance.value.path, e)
|
||||
await handleDefaultInstanceUpdateEvent(worlds.value, instance.value.id, e)
|
||||
}),
|
||||
get_profile_protocol_version(instance.value.path).catch(() => null),
|
||||
get_instance_protocol_version(instance.value.id).catch(() => null),
|
||||
get_game_versions().catch(() => [] as GameVersion[]),
|
||||
])
|
||||
|
||||
if (!worldsTabAlive) {
|
||||
_unlistenProfile()
|
||||
_unlistenInstance()
|
||||
return
|
||||
}
|
||||
|
||||
unlistenProfile = _unlistenProfile
|
||||
unlistenInstance = _unlistenInstance
|
||||
protocolVersion.value = resolvedProtocolVersion
|
||||
gameVersions.value = resolvedGameVersions
|
||||
}
|
||||
@@ -508,7 +461,7 @@ async function refreshAllWorlds() {
|
||||
}
|
||||
|
||||
refreshingAll.value = true
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.value.path] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.value.id] })
|
||||
refreshingAll.value = false
|
||||
}
|
||||
|
||||
@@ -534,7 +487,7 @@ async function editServer(server: ServerWorld) {
|
||||
}
|
||||
|
||||
async function removeServer(server: ServerWorld) {
|
||||
await remove_server_from_profile(instance.value.path, server.index).catch(handleError)
|
||||
await remove_server_from_instance(instance.value.id, server.index).catch(handleError)
|
||||
worlds.value = worlds.value.filter((w) => w.type !== 'server' || w.index !== server.index)
|
||||
let serverIdx = 0
|
||||
for (const w of worlds.value) {
|
||||
@@ -559,12 +512,12 @@ async function editWorld(path: string, name: string, removeIcon: boolean) {
|
||||
}
|
||||
|
||||
async function deleteWorld(world: SingleplayerWorld) {
|
||||
await delete_world(instance.value.path, world.path).catch(handleError)
|
||||
await delete_world(instance.value.id, world.path).catch(handleError)
|
||||
worlds.value = worlds.value.filter((w) => w.type !== 'singleplayer' || w.path !== world.path)
|
||||
}
|
||||
|
||||
function handleJoinError(err: Error) {
|
||||
handleSevereError(err, { profilePath: instance.value.path })
|
||||
handleSevereError(err, { instanceId: instance.value.id })
|
||||
startingInstance.value = false
|
||||
worldPlaying.value = undefined
|
||||
}
|
||||
@@ -574,7 +527,7 @@ async function joinWorld(world: World) {
|
||||
startingInstance.value = true
|
||||
worldPlaying.value = world
|
||||
if (world.type === 'server') {
|
||||
const managedProjectId = instance.value.linked_data?.project_id
|
||||
const managedProjectId = instance.value.link?.project_id
|
||||
if (managedProjectId && isManagedServerWorld(world)) {
|
||||
await playServerProject(managedProjectId).catch(handleJoinError)
|
||||
trackEvent('InstanceStart', {
|
||||
@@ -585,14 +538,14 @@ async function joinWorld(world: World) {
|
||||
startingInstance.value = false
|
||||
return
|
||||
}
|
||||
await start_join_server(instance.value.path, world.address).catch(handleJoinError)
|
||||
await start_join_server(instance.value.id, world.address).catch(handleJoinError)
|
||||
trackEvent('InstanceStart', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
source: 'WorldsPage',
|
||||
})
|
||||
} else if (world.type === 'singleplayer') {
|
||||
await start_join_singleplayer_world(instance.value.path, world.path).catch(handleJoinError)
|
||||
await start_join_singleplayer_world(instance.value.id, world.path).catch(handleJoinError)
|
||||
}
|
||||
play(world)
|
||||
startingInstance.value = false
|
||||
@@ -607,7 +560,7 @@ watch(
|
||||
setTimeout(async () => {
|
||||
for (const world of worlds.value) {
|
||||
if (world.type === 'singleplayer' && world.locked) {
|
||||
await refreshWorld(worlds.value, instance.value.path, world.path)
|
||||
await refreshWorld(worlds.value, instance.value.id, world.path)
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
@@ -744,37 +697,22 @@ const filteredWorlds = computed(() =>
|
||||
const highlightedWorld = ref(route.query.highlight)
|
||||
|
||||
function promptToRemoveWorld(world: World): boolean {
|
||||
worldToRemove.value = world
|
||||
removeWorldModal.value?.show()
|
||||
return !!removeWorldModal.value
|
||||
}
|
||||
|
||||
async function proceedRemoveWorld(world: World) {
|
||||
if (world.type === 'server') {
|
||||
serverToRemove.value = world
|
||||
removeServerModal.value?.show()
|
||||
return !!removeServerModal.value
|
||||
await removeServer(world)
|
||||
} else {
|
||||
worldToDelete.value = world
|
||||
deleteWorldModal.value?.show()
|
||||
return !!deleteWorldModal.value
|
||||
await deleteWorld(world)
|
||||
}
|
||||
}
|
||||
|
||||
async function proceedRemoveServer() {
|
||||
if (!serverToRemove.value) {
|
||||
handleError(new Error(`Error removing server, no server marked for removal.`))
|
||||
return
|
||||
}
|
||||
await removeServer(serverToRemove.value)
|
||||
serverToRemove.value = undefined
|
||||
}
|
||||
|
||||
async function proceedDeleteWorld() {
|
||||
if (!worldToDelete.value) {
|
||||
handleError(new Error(`Error deleting world, no world marked for removal.`))
|
||||
return
|
||||
}
|
||||
await deleteWorld(worldToDelete.value)
|
||||
worldToDelete.value = undefined
|
||||
worldToRemove.value = null
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
worldsTabAlive = false
|
||||
unlistenProfile?.()
|
||||
unlistenInstance?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user