You've already forked AstralRinth
7d92e4ec7f
* feat: base content card component * fix: tooltips + colors * feat: fix orgs * feat: base content tab internals rewrite * feat: fix invalidmodal * feat: add ContentModpackCard * fix: extract types * draft: layout * feat: unlink modal * feat: impl content tab * fix: lint * fix: toggling * temp: disable updating stuff * feat: selection v-model * feat: bulk selection * feat: mods tab rough draft * feat: use fuse.js * feat: add project combobox * clean up project combobox * feat: start install to play modal * fix: events * feat: use v-on * feat: bulk actions + fix floating action bar width * feat: figma alignments * feat: migrate toggle to tailwind * fix: row borders * feat: disabled state * feat: virtual list impl for card table based on window scroll * fix: lint * feat: virtualization + smaller contentcard items * feat: use ContentCardTable + ContentCardItems * feat: fix gap + border issues on last elm * feat: cleanup + use proper searching * fix: use TeleportOverflowMenu * fix: fallback to svg if src is invalid on avatar component * fix: storybook * feat: start on updater modal * feat: finish content updater modal * feat: i18n pass * feat: impl modal * feat(app): backend changes for content tab refactor (#5237) * feat: include_changelog=false for updater modal * fix: hash overrides * feat: update checking for modpack * feat: qa * feat: modpack content modal * fix: padding in table to match modals + tightness * fix: lint * feat: delete modal * feat: fix toggle bugs * fix: prepr * fix: duplicate messages * qa: full width search * qa: use bg-surface-1.5 * qa: animation for filter pills * qa: standardize hover colors * fix: border-[1px] is border * qa: mass de-select actually mass selecting * qa: match figma designs for floating action bar * qa: modal fixes * q: modal fixes x2 * fix: table border * qa: confirm modals * qa: modal alignment * qa: re-add stuck heading + dedupe logic * qa: dedupe virtual scrolling + remove dead components * qa: responsiveness for content table + link fixes * qa: version column link, tooltips + lint fixes * qa: instance busy protections * fix: installation freeze bug * chore: remove old mods page * refactor: deduplicate layout * chore: delete old content page(s) * qa * qa * qa * feat: sort btn - to iterate * fix: ml * feat: date added * fix: lint * fix: formatting.ts removal * feat: get_dependencies_as_content_items * qa: final QA changes * refactor: deduplicate + polish content.rs * feat: hook up content.vue with v1 * feat: hide v1 content api behind frontend feature flag * fix: query keys + copy on empty state * chore: i18n pass * feat: reimpl unlink + upload endpoint * feat: use bulk endpoints v1 * fix: lint * fix: flags * fix: responsiveness via container queries * fix: lint * qa: 1 * qa: fixes * qa: fix ssr issues with browse content * qa: header page divider * qa: modals * fix: prepr * fix: issues * fix: lint * fix: toggle v1 ff * qa: 5 * qa: delete modal copy * feat: creation flow modals (#5383) * refactor: delete content v0 usages + impl * feat: qa + fixes * feat: installing banner using state event * feat: fix modpack card bugs + filtering issues * refactor: delete backups v0 api module * feat: v1 servers GET endpoint * fix: backups * feat: swap to kyros upload v1 addon * fix: use tanstack for loader.vue * feat: finish install from discovery modal * qa: bug fixes * feat: set up installation settings * fix: lint * fix: typos * fix: bugs * fix: disable inline content * feat: content tab improvements — upload UX, installation settings, and client-only indicators Upload cancellation and navigation guard: - Add ConfirmLeaveModal that prompts when navigating away during upload - Cancel in-flight XHR uploads when user confirms leaving the page - Add beforeunload handler to warn on browser/tab close during upload - Track uploadedBytes/totalBytes in UploadState for progress display - Replace Collapsible with Transition for upload progress admonition - Show byte progress and percentage in upload banner - Clamp upload progress to prevent exceeding 100% Installation settings (server.properties): - Add KnownPropertiesFields and PropertiesFields types to Archon types - Add buildProperties() to creation flow context to collect gamemode, difficulty, seed, world type, structures, and generator settings - Pass properties through installContent on onboarding, discovery, and ServerSetupModal flows Server setup and discovery flow improvements: - Migrate ServerSetupModal from servers_v0.reinstall to content_v1.installContent - Replace loaderApiNames lookup with toApiLoader() helper - Remove eraseDataOnInstall toggle — always use soft_override: false - Simplify modpack install on discovery page to use first available version and route through creation flow modal for both onboarding and non-onboarding - Differentiate post-install navigation: content page for onboarding, loader options for existing servers Modpack update flow: - Replace updateModpack() call with installContent() using soft_override: true to support version selection in the content updater modal Client-only mod indicators: - Add environment field to AddonVersion (reuses Labrinth.Projects.v3.Environment) - Add environment to ContentItem and isClientOnly to ContentCardTableItem - Show orange TriangleAlertIcon with tooltip on client-only mods in content table - Add "Client-only" filter pill to content filtering (controlled via showClientOnlyFilter on ContentManagerContext) - Apply client-only indicators in both ContentPageLayout and ModpackContentModal Misc: - Add CLAUDE.md note about using prepr commands for lint checks - Export ConfirmLeaveModal from instances barrel * fix: piping * fix: switch content disable for linked server instances * feat: client only filter * fix: prepr * feat: hasUpdate shape update * feat: bulk update endpoint impl for content in panel * feat: websocket state impl again with new phases * fix: ws * fix: use timeout fn for sync admon + fix content card layout scroll for browsers with overflow anchor bug * fix: qa bugs * fix: lint, a11y and i18n * refactor: set up layouts folder properly * fix: linked data cache stuff + lint * feat: move installationsettings to shared layout * fix: lint * fix: issues * feat: temp fuck staging up * fix: lockfile * fix: data sync issues on loader.vue * fix: lint * Hide shader configuration files from content list (#5499) * feat: workaround search problem + split out reset * fix: qa * fix: changelog not showing on first open * fix: qa + optimistic updating improvements * fix: prepr+lint * fix: qa * feat: qa * fix: lint * fix: lint * fix: build * fix: build * fix: type errors * fix: fade and JAVA_HOME passthrough * feat: qa * feat: impl diff shit * fix: qa * fix: app qa * feat: update diff modal * fix: endpoint * fix: qa * fix: qa * fix: use bulk in modpack modal * feat: abort signal impl + fix issues * fix: diff modal trunc * feat: qa * fix: qa * feat: tooltip content tab * fix: prepr * fix: dismiss on settings btn * feat: qa * feat: dont clear handlers on disconnect * fix: lint * fix: wrangler + introduce staging-archon env file --------- Signed-off-by: Calum H. <calum@modrinth.com> Co-authored-by: tdgao <mr.trumgao@gmail.com> Co-authored-by: Artyom Ezri <61311568+Artezon@users.noreply.github.com>
799 lines
23 KiB
Vue
799 lines
23 KiB
Vue
<template>
|
|
<ContentPageLayout>
|
|
<template #modals>
|
|
<ShareModalWrapper
|
|
ref="shareModal"
|
|
:share-title="formatMessage(messages.shareTitle)"
|
|
:share-text="formatMessage(messages.shareText)"
|
|
:open-in-new-tab="false"
|
|
/>
|
|
<ModpackContentModal
|
|
ref="modpackContentModal"
|
|
:modpack-name="linkedModpackProject?.title"
|
|
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
|
|
:enable-toggle="!props.isServerInstance"
|
|
:get-overflow-options="getOverflowOptions"
|
|
@update:enabled="handleModpackContentToggle"
|
|
@bulk:enable="handleModpackContentBulkToggle"
|
|
@bulk:disable="handleModpackContentBulkToggle"
|
|
/>
|
|
<ConfirmModpackUpdateModal
|
|
ref="modpackUpdateConfirmModal"
|
|
:downgrade="isModpackUpdateDowngrade"
|
|
@confirm="handleModpackUpdateConfirm"
|
|
@cancel="handleModpackUpdateCancel"
|
|
/>
|
|
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
|
<ContentUpdaterModal
|
|
v-if="updatingProject || updatingModpack"
|
|
ref="contentUpdaterModal"
|
|
:versions="updatingProjectVersions"
|
|
:current-game-version="instance.game_version"
|
|
:current-loader="instance.loader"
|
|
:current-version-id="
|
|
updatingModpack
|
|
? (instance.linked_data?.version_id ?? '')
|
|
: (updatingProject?.version?.id ?? '')
|
|
"
|
|
:is-app="true"
|
|
:is-modpack="updatingModpack"
|
|
:project-icon-url="
|
|
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
|
"
|
|
:project-name="
|
|
updatingModpack
|
|
? (linkedModpackProject?.title ?? formatMessage(messages.modpackFallback))
|
|
: (updatingProject?.project?.title ?? updatingProject?.file_name)
|
|
"
|
|
:loading="loadingVersions"
|
|
:loading-changelog="loadingChangelog"
|
|
@update="handleModalUpdate"
|
|
@cancel="resetUpdateState"
|
|
@version-select="handleVersionSelect"
|
|
@version-hover="handleVersionHover"
|
|
/>
|
|
</template>
|
|
</ContentPageLayout>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Labrinth } from '@modrinth/api-client'
|
|
import {
|
|
ConfirmModpackUpdateModal,
|
|
type ContentItem,
|
|
type ContentModpackCardCategory,
|
|
type ContentModpackCardProject,
|
|
type ContentModpackCardVersion,
|
|
type ContentOwner,
|
|
ContentUpdaterModal,
|
|
defineMessages,
|
|
injectNotificationManager,
|
|
ModpackContentModal,
|
|
type ModpackContentModalState,
|
|
type OverflowMenuOption,
|
|
provideAppBackup,
|
|
provideContentManager,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
import { ContentCardLayout as ContentPageLayout } from '@modrinth/ui'
|
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
|
import { open } from '@tauri-apps/plugin-dialog'
|
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
|
|
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 } from '@/helpers/cache.js'
|
|
import { profile_listener } from '@/helpers/events.js'
|
|
import {
|
|
add_project_from_path,
|
|
duplicate,
|
|
edit,
|
|
get,
|
|
get_content_items,
|
|
get_linked_modpack_content,
|
|
get_linked_modpack_info,
|
|
list,
|
|
remove_project,
|
|
toggle_disable_project,
|
|
update_managed_modrinth_version,
|
|
update_project,
|
|
} from '@/helpers/profile'
|
|
import { get_categories } from '@/helpers/tags.js'
|
|
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
|
|
import { highlightModInProfile } from '@/helpers/utils.js'
|
|
import { injectContentInstall } from '@/providers/content-install'
|
|
import { installVersionDependencies } from '@/store/install'
|
|
|
|
const messages = defineMessages({
|
|
shareTitle: {
|
|
id: 'app.instance.mods.share-title',
|
|
defaultMessage: 'Sharing modpack content',
|
|
},
|
|
shareText: {
|
|
id: 'app.instance.mods.share-text',
|
|
defaultMessage: "Check out the projects I'm using in my modpack!",
|
|
},
|
|
modpackFallback: {
|
|
id: 'app.instance.mods.modpack-fallback',
|
|
defaultMessage: 'Modpack',
|
|
},
|
|
successfullyUploaded: {
|
|
id: 'app.instance.mods.successfully-uploaded',
|
|
defaultMessage: 'Successfully uploaded',
|
|
},
|
|
projectWasAdded: {
|
|
id: 'app.instance.mods.project-was-added',
|
|
defaultMessage: '"{name}" was added',
|
|
},
|
|
projectsWereAdded: {
|
|
id: 'app.instance.mods.projects-were-added',
|
|
defaultMessage: '{count} projects were added',
|
|
},
|
|
updating: {
|
|
id: 'app.instance.mods.updating',
|
|
defaultMessage: 'Updating...',
|
|
},
|
|
installing: {
|
|
id: 'app.instance.mods.installing',
|
|
defaultMessage: 'Installing...',
|
|
},
|
|
contentTypeProject: {
|
|
id: 'app.instance.mods.content-type-project',
|
|
defaultMessage: 'project',
|
|
},
|
|
unknownVersion: {
|
|
id: 'app.instance.mods.unknown-version',
|
|
defaultMessage: 'Unknown',
|
|
},
|
|
showFile: {
|
|
id: 'app.instance.mods.show-file',
|
|
defaultMessage: 'Show file',
|
|
},
|
|
copyLink: {
|
|
id: 'app.instance.mods.copy-link',
|
|
defaultMessage: 'Copy link',
|
|
},
|
|
})
|
|
|
|
let savedModalState: ModpackContentModalState | null = null
|
|
|
|
const { formatMessage } = useVIntl()
|
|
const { handleError, addNotification } = injectNotificationManager()
|
|
const { installingItems } = injectContentInstall()
|
|
const router = useRouter()
|
|
|
|
const props = defineProps<{
|
|
instance: GameInstance
|
|
versions: Labrinth.Versions.v2.Version[]
|
|
isServerInstance?: boolean
|
|
openSettings?: () => void
|
|
}>()
|
|
|
|
const loading = ref(true)
|
|
const projects = ref<ContentItem[]>([])
|
|
const mergedProjects = computed<ContentItem[]>(() => {
|
|
const pending = installingItems.value.get(props.instance.path) ?? []
|
|
if (pending.length === 0) return projects.value
|
|
const realProjectIds = new Set(projects.value.map((p) => p.project?.id).filter(Boolean))
|
|
const placeholders = pending.filter((item) => !realProjectIds.has(item.project?.id))
|
|
return [...projects.value, ...placeholders]
|
|
})
|
|
|
|
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 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 shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
|
|
const exportModal = ref(null)
|
|
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>()
|
|
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
|
|
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
|
|
|
|
const updatingProject = ref<ContentItem | null>(null)
|
|
const updatingProjectVersions = ref<Labrinth.Versions.v2.Version[]>([])
|
|
const loadingVersions = ref(false)
|
|
const loadingChangelog = ref(false)
|
|
const updatingModpack = ref(false)
|
|
const pendingModpackUpdateVersion = ref<Labrinth.Versions.v2.Version | null>(null)
|
|
const isModpackUpdateDowngrade = ref(false)
|
|
|
|
async function handleBrowseContent() {
|
|
if (!props.instance) return
|
|
await router.push({
|
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
|
query: { i: props.instance.path },
|
|
})
|
|
}
|
|
|
|
async function handleUploadFiles() {
|
|
if (!props.instance) return
|
|
const files = await open({ multiple: true })
|
|
if (!files) return
|
|
|
|
const addedFiles: string[] = []
|
|
for (const file of files) {
|
|
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)
|
|
addedFiles.push(fileName)
|
|
} catch (e) {
|
|
handleError(e as Error)
|
|
}
|
|
}
|
|
await initProjects()
|
|
|
|
if (addedFiles.length > 0) {
|
|
const names = addedFiles.map((f) => {
|
|
const item = projects.value.find(
|
|
(p) => p.file_name === f || p.file_name === f.replace('.zip', '.jar'),
|
|
)
|
|
return item?.project?.title ?? f
|
|
})
|
|
addNotification({
|
|
type: 'success',
|
|
title: formatMessage(messages.successfullyUploaded),
|
|
text:
|
|
names.length === 1
|
|
? formatMessage(messages.projectWasAdded, { name: names[0] })
|
|
: formatMessage(messages.projectsWereAdded, { count: names.length }),
|
|
})
|
|
}
|
|
}
|
|
|
|
async function toggleDisableMod(mod: ContentItem) {
|
|
try {
|
|
mod.file_path = await toggle_disable_project(props.instance.path, mod.file_path!)
|
|
mod.enabled = !mod.enabled
|
|
|
|
trackEvent('InstanceProjectDisable', {
|
|
loader: props.instance.loader,
|
|
game_version: props.instance.game_version,
|
|
id: mod.project?.id,
|
|
name: mod.project?.title ?? mod.file_name,
|
|
project_type: mod.project_type,
|
|
disabled: !mod.enabled,
|
|
})
|
|
} catch (err) {
|
|
handleError(err as Error)
|
|
}
|
|
}
|
|
|
|
async function removeMod(mod: ContentItem) {
|
|
await remove_project(props.instance.path, mod.file_path!).catch(handleError)
|
|
projects.value = projects.value.filter((x) => mod.file_path !== x.file_path)
|
|
|
|
trackEvent('InstanceProjectRemove', {
|
|
loader: props.instance.loader,
|
|
game_version: props.instance.game_version,
|
|
id: mod.project?.id,
|
|
name: mod.project?.title ?? mod.file_name,
|
|
project_type: mod.project_type,
|
|
})
|
|
}
|
|
|
|
async function updateProject(mod: ContentItem) {
|
|
try {
|
|
const newPath = await update_project(props.instance.path, mod.file_path!)
|
|
mod.file_path = newPath
|
|
|
|
if (mod.update_version_id) {
|
|
const versionData = await get_version(mod.update_version_id, 'must_revalidate').catch(
|
|
handleError,
|
|
)
|
|
|
|
if (versionData) {
|
|
const profile = await get(props.instance.path).catch(handleError)
|
|
|
|
if (profile) {
|
|
await installVersionDependencies(profile, versionData).catch(handleError)
|
|
}
|
|
}
|
|
}
|
|
|
|
mod.has_update = false
|
|
if (mod.version && mod.update_version_id) {
|
|
mod.version.id = mod.update_version_id
|
|
}
|
|
mod.update_version_id = null
|
|
|
|
trackEvent('InstanceProjectUpdate', {
|
|
loader: props.instance.loader,
|
|
game_version: props.instance.game_version,
|
|
id: mod.project?.id,
|
|
name: mod.project?.title ?? mod.file_name,
|
|
project_type: mod.project_type,
|
|
})
|
|
} catch (err) {
|
|
handleError(err as Error)
|
|
}
|
|
}
|
|
|
|
async function handleUpdate(id: string) {
|
|
const item = projects.value.find((p) => p.file_name === id)
|
|
if (!item?.has_update || !item.project?.id || !item.version?.id) return
|
|
|
|
updatingModpack.value = false
|
|
updatingProject.value = item
|
|
updatingProjectVersions.value = []
|
|
loadingVersions.value = true
|
|
loadingChangelog.value = false
|
|
|
|
await nextTick()
|
|
|
|
contentUpdaterModal.value?.show(item.update_version_id ?? undefined)
|
|
|
|
const versions = (await get_project_versions(item.project.id).catch((e) => {
|
|
return handleError(e)
|
|
})) as Labrinth.Versions.v2.Version[] | null
|
|
|
|
loadingVersions.value = false
|
|
|
|
if (!versions) return
|
|
|
|
versions.sort(
|
|
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
|
|
)
|
|
|
|
updatingProjectVersions.value = versions
|
|
}
|
|
|
|
async function handleModpackContentToggle(item: ContentItem) {
|
|
await toggleDisableMod(item)
|
|
}
|
|
|
|
async function handleModpackContentBulkToggle(items: ContentItem[]) {
|
|
for (const item of items) {
|
|
await toggleDisableMod(item)
|
|
}
|
|
}
|
|
|
|
async function handleModpackContent() {
|
|
if (!props.instance?.path) return
|
|
|
|
modpackContentModal.value?.showLoading()
|
|
|
|
const contentItems = await get_linked_modpack_content(props.instance.path).catch(handleError)
|
|
|
|
if (contentItems) {
|
|
modpackContentModal.value?.show(contentItems)
|
|
} else {
|
|
modpackContentModal.value?.hide()
|
|
}
|
|
}
|
|
|
|
async function handleModpackUpdate() {
|
|
if (!props.instance?.linked_data?.project_id) return
|
|
|
|
updatingModpack.value = true
|
|
updatingProject.value = null
|
|
updatingProjectVersions.value = []
|
|
loadingVersions.value = true
|
|
loadingChangelog.value = false
|
|
|
|
await nextTick()
|
|
|
|
contentUpdaterModal.value?.show(
|
|
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined,
|
|
)
|
|
|
|
const versions = (await get_project_versions(props.instance.linked_data.project_id).catch(
|
|
handleError,
|
|
)) as Labrinth.Versions.v2.Version[] | null
|
|
|
|
loadingVersions.value = false
|
|
|
|
if (!versions) return
|
|
|
|
versions.sort(
|
|
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
|
|
)
|
|
|
|
updatingProjectVersions.value = versions
|
|
}
|
|
|
|
async function fetchAndSpliceVersion(
|
|
versionId: string,
|
|
cacheBehaviour?: Parameters<typeof get_version>[1],
|
|
onError?: (err: unknown) => void,
|
|
) {
|
|
const fullVersion = (await get_version(versionId, cacheBehaviour).catch(
|
|
onError ?? (() => null),
|
|
)) as Labrinth.Versions.v2.Version | null
|
|
if (!fullVersion) return
|
|
const index = updatingProjectVersions.value.findIndex((v) => v.id === versionId)
|
|
if (index !== -1) {
|
|
const newVersions = [...updatingProjectVersions.value]
|
|
newVersions[index] = fullVersion
|
|
updatingProjectVersions.value = newVersions
|
|
}
|
|
}
|
|
|
|
async function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
|
|
if (version.changelog != null) return
|
|
loadingChangelog.value = true
|
|
await fetchAndSpliceVersion(version.id, 'must_revalidate', handleError)
|
|
loadingChangelog.value = false
|
|
}
|
|
|
|
async function handleVersionHover(version: Labrinth.Versions.v2.Version) {
|
|
if (version.changelog != null) return
|
|
await fetchAndSpliceVersion(version.id)
|
|
}
|
|
|
|
function resetUpdateState() {
|
|
updatingModpack.value = false
|
|
updatingProject.value = null
|
|
updatingProjectVersions.value = []
|
|
loadingVersions.value = false
|
|
loadingChangelog.value = false
|
|
}
|
|
|
|
function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
|
|
pendingModpackUpdateVersion.value = selectedVersion
|
|
const currentVersionId = props.instance?.linked_data?.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
|
|
modpackUpdateConfirmModal.value?.show()
|
|
}
|
|
|
|
async function handleModpackUpdateConfirm() {
|
|
if (!pendingModpackUpdateVersion.value || !props.instance?.path) return
|
|
|
|
const version = pendingModpackUpdateVersion.value
|
|
pendingModpackUpdateVersion.value = null
|
|
|
|
isModpackUpdating.value = true
|
|
try {
|
|
await update_managed_modrinth_version(props.instance.path, version.id)
|
|
await initProjects()
|
|
} finally {
|
|
isModpackUpdating.value = false
|
|
resetUpdateState()
|
|
}
|
|
}
|
|
|
|
function handleModpackUpdateCancel() {
|
|
pendingModpackUpdateVersion.value = null
|
|
}
|
|
|
|
async function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version) {
|
|
if (updatingModpack.value) {
|
|
handleModpackUpdateRequest(selectedVersion)
|
|
} else if (updatingProject.value) {
|
|
const mod = updatingProject.value
|
|
|
|
mod.update_version_id = selectedVersion.id
|
|
|
|
await updateProject(mod)
|
|
|
|
resetUpdateState()
|
|
}
|
|
}
|
|
|
|
async function unpairProfile() {
|
|
await edit(props.instance.path, {
|
|
linked_data: null as unknown as undefined,
|
|
})
|
|
linkedModpackProject.value = null
|
|
linkedModpackVersion.value = null
|
|
linkedModpackOwner.value = null
|
|
linkedModpackHasUpdate.value = false
|
|
linkedModpackUpdateVersionId.value = null
|
|
await initProjects()
|
|
}
|
|
|
|
async function handleShareItems(
|
|
items: ContentItem[],
|
|
format: 'names' | 'file-names' | 'urls' | 'markdown',
|
|
) {
|
|
const source = items.length > 0 ? items : projects.value
|
|
let text: string
|
|
switch (format) {
|
|
case 'names':
|
|
text = source.map((x) => x.project?.title ?? x.file_name).join('\n')
|
|
break
|
|
case 'file-names':
|
|
text = source.map((x) => x.file_name).join('\n')
|
|
break
|
|
case 'urls':
|
|
text = source
|
|
.filter((x) => x.project?.slug)
|
|
.map((x) => `https://modrinth.com/${x.project_type}/${x.project?.slug}`)
|
|
.join('\n')
|
|
break
|
|
case 'markdown':
|
|
text = source
|
|
.map((x) => {
|
|
const name = x.project?.title ?? x.file_name
|
|
if (x.project?.slug) {
|
|
return `[${name}](https://modrinth.com/${x.project_type}/${x.project.slug})`
|
|
}
|
|
return name
|
|
})
|
|
.join('\n')
|
|
break
|
|
}
|
|
await shareModal.value?.show(text)
|
|
}
|
|
|
|
function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
|
const options: OverflowMenuOption[] = [
|
|
{
|
|
id: formatMessage(messages.showFile),
|
|
action: () => highlightModInProfile(props.instance.path, item.file_path),
|
|
},
|
|
]
|
|
|
|
if (item.project?.slug) {
|
|
options.push({
|
|
id: formatMessage(messages.copyLink),
|
|
action: async () => {
|
|
await navigator.clipboard.writeText(
|
|
`https://modrinth.com/${item.project_type}/${item.project?.slug}`,
|
|
)
|
|
},
|
|
})
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
async function initProjects(cacheBehaviour?: CacheBehaviour) {
|
|
if (!props.instance) return
|
|
|
|
const [contentItems, modpackInfo, allCategories] = await Promise.all([
|
|
get_content_items(props.instance.path, cacheBehaviour).catch(handleError),
|
|
get_linked_modpack_info(props.instance.path, cacheBehaviour).catch(handleError),
|
|
get_categories().catch(handleError),
|
|
])
|
|
|
|
if (!contentItems) {
|
|
loading.value = false
|
|
return
|
|
}
|
|
|
|
projects.value = contentItems
|
|
|
|
if (modpackInfo) {
|
|
linkedModpackProject.value = {
|
|
...modpackInfo.project,
|
|
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
|
|
icon_url: modpackInfo.project.icon_url ?? undefined,
|
|
}
|
|
linkedModpackVersion.value = {
|
|
...modpackInfo.version,
|
|
date_published: modpackInfo.version.date_published.toString(),
|
|
}
|
|
linkedModpackOwner.value = modpackInfo.owner
|
|
? {
|
|
...modpackInfo.owner,
|
|
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
|
|
}
|
|
: null
|
|
|
|
linkedModpackHasUpdate.value = modpackInfo.has_update
|
|
linkedModpackUpdateVersionId.value = modpackInfo.update_version_id
|
|
|
|
if (allCategories && modpackInfo.project.categories) {
|
|
const seen = new Set<string>()
|
|
linkedModpackCategories.value = allCategories
|
|
.filter((cat: { name: string }) => {
|
|
if (modpackInfo.project.categories.includes(cat.name) && !seen.has(cat.name)) {
|
|
seen.add(cat.name)
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
.map((cat: { name: string }) => ({
|
|
...cat,
|
|
name: cat.name.charAt(0).toUpperCase() + cat.name.slice(1),
|
|
}))
|
|
} else {
|
|
linkedModpackCategories.value = []
|
|
}
|
|
} else {
|
|
linkedModpackProject.value = null
|
|
linkedModpackVersion.value = null
|
|
linkedModpackOwner.value = null
|
|
linkedModpackCategories.value = []
|
|
linkedModpackHasUpdate.value = false
|
|
linkedModpackUpdateVersionId.value = null
|
|
}
|
|
|
|
loading.value = false
|
|
}
|
|
|
|
provideAppBackup({
|
|
async createBackup() {
|
|
const allProfiles = await list()
|
|
const prefix = `${props.instance.name} - Backup #`
|
|
const existingNums = allProfiles
|
|
.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 CONTENT_HINT_KEY = 'content-tab-modpack-hint-dismissed'
|
|
const showContentHint = ref(localStorage.getItem(CONTENT_HINT_KEY) === null)
|
|
function dismissContentHint() {
|
|
showContentHint.value = false
|
|
localStorage.setItem(CONTENT_HINT_KEY, 'true')
|
|
}
|
|
|
|
provideContentManager({
|
|
items: mergedProjects,
|
|
loading,
|
|
error: ref(null),
|
|
modpack: computed(() =>
|
|
linkedModpackProject.value
|
|
? {
|
|
project: linkedModpackProject.value,
|
|
projectLink: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
|
|
version: linkedModpackVersion.value ?? undefined,
|
|
versionLink:
|
|
linkedModpackProject.value && linkedModpackVersion.value
|
|
? `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`
|
|
: undefined,
|
|
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(messages.updating)
|
|
: formatMessage(messages.installing),
|
|
}
|
|
: null,
|
|
),
|
|
isPackLocked,
|
|
isBusy: isInstanceBusy,
|
|
isBulkOperating,
|
|
getItemId: (item) => item.file_name,
|
|
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
|
|
toggleEnabled: toggleDisableMod,
|
|
deleteItem: removeMod,
|
|
refresh: () => initProjects('must_revalidate'),
|
|
browse: handleBrowseContent,
|
|
uploadFiles: handleUploadFiles,
|
|
hasUpdateSupport: true,
|
|
updateItem: handleUpdate,
|
|
bulkUpdateItem: updateProject,
|
|
updateModpack: props.isServerInstance ? undefined : handleModpackUpdate,
|
|
viewModpackContent: handleModpackContent,
|
|
unlinkModpack: unpairProfile,
|
|
openSettings: props.openSettings,
|
|
getOverflowOptions,
|
|
showContentHint,
|
|
dismissContentHint,
|
|
shareItems: handleShareItems,
|
|
mapToTableItem: (item) => ({
|
|
id: item.file_name,
|
|
project: item.project ?? {
|
|
id: item.file_name,
|
|
slug: null,
|
|
title: item.file_name.replace('.disabled', ''),
|
|
icon_url: null,
|
|
},
|
|
projectLink: item.installing
|
|
? undefined
|
|
: item.project?.id
|
|
? `/project/${item.project.id}`
|
|
: undefined,
|
|
version: item.installing
|
|
? {
|
|
id: item.file_name,
|
|
version_number: formatMessage(messages.installing),
|
|
file_name: '',
|
|
}
|
|
: (item.version ?? {
|
|
id: item.file_name,
|
|
version_number: formatMessage(messages.unknownVersion),
|
|
file_name: item.file_name,
|
|
}),
|
|
versionLink: item.installing
|
|
? undefined
|
|
: item.project?.id && item.version?.id
|
|
? `/project/${item.project.id}/version/${item.version.id}`
|
|
: undefined,
|
|
owner: item.owner
|
|
? {
|
|
...item.owner,
|
|
link: () => openUrl(`https://modrinth.com/${item.owner!.type}/${item.owner!.id}`),
|
|
}
|
|
: undefined,
|
|
enabled: item.enabled,
|
|
}),
|
|
})
|
|
|
|
await initProjects()
|
|
|
|
// Restore modpack content modal state if returning via back navigation
|
|
if (savedModalState) {
|
|
const stateToRestore = savedModalState
|
|
savedModalState = null
|
|
await nextTick()
|
|
modpackContentModal.value?.restore(stateToRestore)
|
|
}
|
|
|
|
// Save modal state when navigating away so it can be restored on back
|
|
const removeBeforeEach = router.beforeEach(() => {
|
|
const state = modpackContentModal.value?.getState()
|
|
savedModalState = state ?? null
|
|
})
|
|
|
|
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
|
|
if (event.payload.type !== 'drop' || !props.instance) return
|
|
|
|
for (const file of event.payload.paths) {
|
|
if (file.endsWith('.mrpack')) continue
|
|
await add_project_from_path(props.instance.path, file).catch(handleError)
|
|
}
|
|
await initProjects()
|
|
})
|
|
|
|
const unlistenProfiles = await profile_listener(
|
|
async (event: { event: string; profile_path_id: string }) => {
|
|
if (
|
|
props.instance &&
|
|
event.profile_path_id === props.instance.path &&
|
|
event.event === 'synced' &&
|
|
props.instance.install_stage !== 'pack_installing' &&
|
|
!isBulkOperating.value
|
|
) {
|
|
await initProjects()
|
|
}
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.instance?.install_stage,
|
|
async (newStage, oldStage) => {
|
|
if (oldStage !== 'installed' && newStage === 'installed') {
|
|
await initProjects('must_revalidate')
|
|
} else if (oldStage === 'not_installed' && newStage === 'pack_installing') {
|
|
await initProjects()
|
|
}
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.instance?.linked_data,
|
|
async (newLinkedData, oldLinkedData) => {
|
|
if (oldLinkedData && !newLinkedData) {
|
|
await initProjects('must_revalidate')
|
|
}
|
|
},
|
|
)
|
|
|
|
onUnmounted(() => {
|
|
removeBeforeEach()
|
|
unlisten()
|
|
unlistenProfiles()
|
|
})
|
|
</script>
|