feat: content tab rewrite for worlds (#5136)

* 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>
This commit is contained in:
Calum H.
2026-03-12 20:24:32 +00:00
committed by GitHub
parent f0224dfff7
commit 7d92e4ec7f
302 changed files with 20016 additions and 12142 deletions

View File

@@ -24,6 +24,7 @@ import {
SearchFilterControl,
SearchSidebarFilter,
StyledInput,
useDebugLogger,
useSearch,
useServerSearch,
useVIntl,
@@ -44,27 +45,32 @@ import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import {
get as getInstance,
get_projects as getInstanceProjects,
get_installed_project_ids as getInstalledProjectIds,
kill,
list as listInstances,
} from '@/helpers/profile.js'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import type { GameInstance } from '@/helpers/types'
import { getServerLatency } from '@/helpers/worlds'
import { injectServerInstall } from '@/providers/server-install'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { getServerAddress, playServerProject, useInstall } from '@/store/install.js'
import { getServerAddress } from '@/store/install.js'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const installStore = useInstall()
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
injectServerInstall()
const debugLog = useDebugLogger('Browse')
const router = useRouter()
const route = useRoute()
const projectTypes = computed(() => {
debugLog('projectTypes computed', route.params.projectType)
return [route.params.projectType as ProjectType]
})
debugLog('fetching tags (categories, loaders, gameVersions)')
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories()
.catch(handleError)
@@ -97,61 +103,62 @@ type Instance = {
}
}
type InstanceProject = {
metadata: {
project_id: string
}
}
const instance: Ref<Instance | null> = ref(null)
const instanceProjects: Ref<InstanceProject[] | null> = ref(null)
const installedProjectIds: Ref<string[] | null> = ref(null)
const instanceHideInstalled = ref(false)
const newlyInstalled = ref<string[]>([])
const isServerInstance = ref(false)
const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
await updateInstanceContext()
await initInstanceContext()
watch(
() => [route.query.i, route.query.ai, route.path],
() => {
updateInstanceContext()
},
)
async function updateInstanceContext() {
async function initInstanceContext() {
debugLog('initInstanceContext', { queryI: route.query.i, queryAi: route.query.ai })
if (route.query.i) {
;[instance.value, instanceProjects.value] = await Promise.all([
getInstance(route.query.i).catch(handleError),
getInstanceProjects(route.query.i).catch(handleError),
])
newlyInstalled.value = []
instance.value = await getInstance(route.query.i).catch(handleError)
debugLog('instance loaded', {
name: instance.value?.name,
loader: instance.value?.loader,
gameVersion: instance.value?.game_version,
})
// Load installed project IDs in background — the page and initial search render immediately.
// When this resolves, instanceFilters recomputes and triggers a search refresh
// that applies the "hide installed" negative filters and marks installed badges.
getInstalledProjectIds(route.query.i)
.then((ids) => {
debugLog('installedProjectIds loaded', { count: ids?.length })
installedProjectIds.value = ids
})
.catch(handleError)
isServerInstance.value = false
if (instance.value?.linked_data?.project_id) {
debugLog('checking linked project for server status', instance.value.linked_data.project_id)
const projectV3 = await get_project_v3(
instance.value.linked_data.project_id,
'must_revalidate',
).catch(handleError)
if (projectV3?.minecraft_server != null) {
debugLog('instance is a server instance')
isServerInstance.value = true
}
}
}
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
debugLog('setting instanceHideInstalled from query', route.query.ai)
instanceHideInstalled.value = route.query.ai === 'true'
}
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
instance.value = null
instanceHideInstalled.value = false
}
}
const instanceFilters = computed(() => {
const filters = []
debugLog('instanceFilters recomputing', {
hasInstance: !!instance.value,
isServer: isServerInstance.value,
hideInstalled: instanceHideInstalled.value,
})
if (instance.value) {
const gameVersion = instance.value.game_version
@@ -179,24 +186,9 @@ const instanceFilters = computed(() => {
option: 'client',
})
}
if (instanceHideInstalled.value && instanceProjects.value) {
const installedMods = Object.values(instanceProjects.value)
.filter((x) => x.metadata)
.map((x) => x.metadata.project_id)
installedMods.push(...newlyInstalled.value)
installedMods
?.map((x) => ({
type: 'project_id',
option: `project_id:${x}`,
negative: true,
}))
.forEach((x) => filters.push(x))
}
}
debugLog('instanceFilters result', filters)
return filters
})
@@ -221,11 +213,25 @@ const {
createPageParams,
} = useSearch(projectTypes, tags, instanceFilters)
const activeLoader = computed(() => {
const filter = currentFilters.value.find((f) => f.type === 'mod_loader')
if (filter) return filter.option
if (projectType.value === 'datapack' || projectType.value === 'resourcepack') return 'vanilla'
return instance.value?.loader ?? null
})
const activeGameVersion = computed(() => {
const filter = currentFilters.value.find((f) => f.type === 'game_version')
if (filter) return filter.option
return instance.value?.game_version ?? null
})
const serverHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const serverPings = shallowRef<Record<string, number | undefined>>({})
const runningServerProjects = ref<Record<string, string>>({})
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('checkServerRunningStates', { hitCount: hits.length })
const packs = await listInstances()
const newRunning: Record<string, string> = {}
for (const hit of hits) {
@@ -237,10 +243,12 @@ async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchPro
}
}
}
debugLog('runningServerProjects updated', newRunning)
runningServerProjects.value = newRunning
}
async function handleStopServerProject(projectId: string) {
debugLog('handleStopServerProject', projectId)
const instancePath = runningServerProjects.value[projectId]
if (!instancePath) return
await kill(instancePath).catch(() => {})
@@ -249,18 +257,21 @@ async function handleStopServerProject(projectId: string) {
}
async function handlePlayServerProject(projectId: string) {
debugLog('handlePlayServerProject', projectId)
await playServerProject(projectId)
checkServerRunningStates(serverHits.value)
}
function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearchProject) {
debugLog('handleAddServerToInstance', { projectId: project.project_id, name: project.name })
const address = getServerAddress(project.minecraft_java_server)
if (!address) return
installStore.showAddServerToInstanceModal(project.name, address)
showAddServerToInstanceModal(project.name, address)
}
const unlistenProcesses = await process_listener(
(e: { event: string; profile_path_id: string }) => {
debugLog('process event', e)
if (e.event === 'finished') {
const projectId = Object.entries(runningServerProjects.value).find(
([, path]) => path === e.profile_path_id,
@@ -288,6 +299,7 @@ const {
} = useServerSearch({ tags, query, maxResults, currentPage })
async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('pingServerHits', { hitCount: hits.length })
const pingsToFetch = hits.filter((hit) => hit.minecraft_java_server?.address)
await Promise.all(
pingsToFetch.map(async (hit) => {
@@ -303,13 +315,15 @@ async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
}
const previousFilterState = ref('')
const isRefreshing = ref(false)
let searchVersion = 0
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
debugLog('went offline')
offline.value = true
})
window.addEventListener('online', () => {
debugLog('went online')
offline.value = false
})
@@ -334,29 +348,51 @@ const pageCount = computed(() =>
)
const effectiveRequestParams = computed(() => {
return projectType.value === 'server' ? serverRequestParams.value : requestParams.value
const isServer = projectType.value === 'server'
debugLog('effectiveRequestParams computed', { isServer })
return isServer ? serverRequestParams.value : requestParams.value
})
watch(effectiveRequestParams, async () => {
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
watch(effectiveRequestParams, () => {
if (!route.params.projectType) return
await nextTick()
debugLog('effectiveRequestParams changed, debouncing search')
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
searchDebounceTimer = setTimeout(() => {
refreshSearch()
}, 200)
})
watch(instanceHideInstalled, () => {
debugLog('instanceHideInstalled changed', instanceHideInstalled.value)
refreshSearch()
})
async function refreshSearch() {
if (isRefreshing.value) return
isRefreshing.value = true
const version = ++searchVersion
debugLog('refreshSearch start', { version, projectType: projectType.value })
try {
const isServer = projectType.value === 'server'
if (isServer) {
debugLog('searching v3 (server)', serverRequestParams.value)
const rawResults = (await get_search_results_v3(serverRequestParams.value)) as {
result: Labrinth.Search.v3.SearchResults
} | null
if (version !== searchVersion) {
debugLog('search version stale, discarding', { version, current: searchVersion })
return
}
const searchResults = rawResults?.result ?? { hits: [], total_hits: 0 }
const hits = searchResults.hits ?? []
debugLog('server search results', {
hitCount: hits.length,
totalHits: searchResults.total_hits,
})
serverHits.value = hits
serverPings.value = {}
pingServerHits(hits)
@@ -368,10 +404,16 @@ async function refreshSearch() {
offset: 0,
}
} else {
debugLog('searching v2', requestParams.value)
let rawResults = (await get_search_results(requestParams.value)) as {
result: SearchResults
} | null
if (version !== searchVersion) {
debugLog('search version stale, discarding', { version, current: searchVersion })
return
}
if (!rawResults) {
rawResults = {
result: {
@@ -383,18 +425,24 @@ async function refreshSearch() {
}
}
if (instance.value) {
const installedProjectIds = new Set([
const allInstalledIds = new Set([
...newlyInstalled.value,
...Object.values(instanceProjects.value ?? {})
.filter((x) => x.metadata)
.map((x) => x.metadata.project_id),
...(installedProjectIds.value ?? []),
])
rawResults.result.hits = rawResults.result.hits.map((val) => ({
...val,
installed: installedProjectIds.has(val.project_id),
installed: allInstalledIds.has(val.project_id),
}))
if (instanceHideInstalled.value) {
rawResults.result.hits = rawResults.result.hits.filter((val) => !val.installed)
}
}
debugLog('v2 search results', {
hitCount: rawResults.result.hits.length,
totalHits: rawResults.result.total_hits,
})
results.value = rawResults.result
}
@@ -407,6 +455,7 @@ async function refreshSearch() {
})
if (previousFilterState.value && previousFilterState.value !== currentFilterState) {
debugLog('filters changed, resetting to page 1')
currentPage.value = 1
}
@@ -445,16 +494,21 @@ async function refreshSearch() {
})
.join('&')
const newUrl = `${route.path}${queryString ? '?' + queryString : ''}`
debugLog('updating URL', newUrl)
window.history.replaceState(window.history.state, '', newUrl)
} catch (err) {
console.error('Error refreshing search:', err)
} finally {
loading.value = false
isRefreshing.value = false
debugLog('refreshSearch complete', { version })
} catch (err) {
debugLog('refreshSearch error', err)
if (version === searchVersion) {
loading.value = false
}
}
}
async function setPage(newPageNumber: number) {
debugLog('setPage', newPageNumber)
currentPage.value = newPageNumber
await onSearchChangeToTop()
@@ -469,6 +523,7 @@ async function onSearchChangeToTop() {
}
function clearSearch() {
debugLog('clearSearch')
query.value = ''
currentPage.value = 1
}
@@ -479,6 +534,7 @@ watch(
// Check if the newType is not the same as the current value
if (!newType || newType === projectType.value) return
debugLog('projectType route param changed', { from: projectType.value, to: newType })
projectType.value = newType
currentSortType.value = { display: 'Relevance', name: 'relevance' }
@@ -495,7 +551,8 @@ const selectableProjectTypes = computed(() => {
if (
availableGameVersions.value &&
availableGameVersions.value.findIndex((x) => x.version === instance.value?.game_version) <=
availableGameVersions.value.findIndex((x) => x.version === '1.13')
availableGameVersions.value.findIndex((x) => x.version === '1.13') &&
!isServerInstance.value
) {
dataPacks = true
}
@@ -624,6 +681,7 @@ const handleOptionsClick = (args) => {
}
}
debugLog('performing initial search')
await refreshSearch()
// Initialize previousFilterState after first search
@@ -854,18 +912,12 @@ previousFilterState.value = JSON.stringify({
</ButtonStyled>
<ButtonStyled v-else color="brand" type="outlined">
<button
:disabled="
(installStore.installingServerProjects as string[]).includes(
project.project_id,
)
"
:disabled="(installingServerProjects as string[]).includes(project.project_id)"
@click="() => handlePlayServerProject(project.project_id)"
>
<PlayIcon />
{{
(installStore.installingServerProjects as string[]).includes(
project.project_id,
)
(installingServerProjects as string[]).includes(project.project_id)
? 'Installing...'
: 'Play'
}}
@@ -882,6 +934,19 @@ previousFilterState.value = JSON.stringify({
:project-type="projectType"
:project="result"
:instance="instance ?? undefined"
:active-loader="activeLoader ?? undefined"
:active-game-version="activeGameVersion ?? undefined"
:categories="[
...(categories ?? []).filter(
(cat) =>
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
),
...(loaders ?? []).filter(
(loader) =>
result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes(projectType),
),
]"
:installed="result.installed || newlyInstalled.includes(result.project_id || '')"
@install="
(id) => {

View File

@@ -110,9 +110,13 @@
<div class="flex gap-2">
<ButtonStyled
v-if="
['installing', 'pack_installing', 'minecraft_installing'].includes(
instance.install_stage,
)
[
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
].includes(instance.install_stage)
"
color="brand"
size="large"
@@ -245,6 +249,7 @@
:versions="modrinthVersions"
:installed="instance.install_stage !== 'installed'"
:is-server-instance="isServerInstance"
:open-settings="() => settingsModal?.show(1)"
@play="updatePlayState"
@stop="() => stopInstance('InstanceSubpage')"
></component>
@@ -334,14 +339,15 @@ import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile
import type { GameInstance } from '@/helpers/types'
import { showProfileInFolder } from '@/helpers/utils.js'
import { get_server_status } from '@/helpers/worlds'
import { injectServerInstall } from '@/providers/server-install'
import { handleSevereError } from '@/store/error.js'
import { playServerProject } from '@/store/install.js'
import { useBreadcrumbs, useLoading } from '@/store/state'
dayjs.extend(duration)
dayjs.extend(relativeTime)
const { handleError } = injectNotificationManager()
const { playServerProject } = injectServerInstall()
const route = useRoute()
const router = useRouter()
@@ -607,6 +613,10 @@ const unlistenProfiles = await profile_listener(
return
}
instance.value = await get(route.params.id as string).catch(handleError)
if (!instance.value?.linked_data?.project_id) {
linkedProjectV3.value = undefined
isServerInstance.value = false
}
}
},
)

File diff suppressed because it is too large Load Diff

View File

@@ -176,13 +176,11 @@ import {
start_join_singleplayer_world,
type World,
} from '@/helpers/worlds.ts'
import {
ensureManagedServerWorldExists,
getServerAddress,
playServerProject,
} from '@/store/install'
import { injectServerInstall } from '@/providers/server-install'
import { ensureManagedServerWorldExists, getServerAddress } from '@/store/install'
const { handleError } = injectNotificationManager()
const { playServerProject } = injectServerInstall()
const route = useRoute()
const addServerModal = ref<InstanceType<typeof AddServerModal>>()

View File

@@ -1,17 +1,17 @@
<script setup>
import { PlusIcon } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { onUnmounted, ref, shallowRef } from 'vue'
import { inject, onUnmounted, ref, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
import { NewInstanceImage } from '@/assets/icons'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { profile_listener } from '@/helpers/events.js'
import { list } from '@/helpers/profile.js'
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
const { handleError } = injectNotificationManager()
const showCreationModal = inject('showCreationModal')
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
@@ -55,11 +55,10 @@ onUnmounted(() => {
<NewInstanceImage />
</div>
<h3>No instances found</h3>
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
<Button color="primary" :disabled="offline" @click="showCreationModal?.()">
<PlusIcon />
Create new instance
</Button>
<InstanceCreationModal ref="installationModal" />
</div>
</div>
</template>

View File

@@ -69,15 +69,11 @@
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
<button
:disabled="data && installStore.installingServerProjects.includes(data.id)"
:disabled="data && installingServerProjects.includes(data.id)"
@click="handleClickPlay"
>
<PlayIcon />
{{
data && installStore.installingServerProjects.includes(data.id)
? 'Installing...'
: 'Play'
}}
{{ data && installingServerProjects.includes(data.id) ? 'Installing...' : 'Play' }}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular>
@@ -264,24 +260,23 @@ import {
} from '@/helpers/profile'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { getServerLatency } from '@/helpers/worlds'
import { injectContentInstall } from '@/providers/content-install'
import { injectServerInstall } from '@/providers/server-install'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import {
getServerAddress,
install as installVersion,
playServerProject,
useInstall,
} from '@/store/install.js'
import { getServerAddress } from '@/store/install.js'
import { useTheming } from '@/store/state.js'
dayjs.extend(relativeTime)
const { handleError } = injectNotificationManager()
const { install: installVersion } = injectContentInstall()
const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const themeStore = useTheming()
const installStore = useInstall()
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
injectServerInstall()
const installing = ref(false)
const data = shallowRef(null)
const versions = shallowRef([])
@@ -355,7 +350,7 @@ async function handleStopServer() {
function handleAddServerToInstance() {
const address = getServerAddress(projectV3.value?.minecraft_java_server)
if (!address || !data.value) return
installStore.showAddServerToInstanceModal(data.value.title, address)
showAddServerToInstanceModal(data.value.title, address)
}
async function fetchProjectData() {