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:
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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>>()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user