mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
* start new server settings tabs
* update properties tab to match design
* better stying in general tab
* feat: add suffix input for hostname field
* implement tables for allocations and DNS records
* add tags for dns record type
* small gap adjustment
* polish advanced page
* adjust properties page hierarchy
* fix searching properties, empty state and projection radius appearing
* pnpm prepr
* update copy to match designs
* fix suffix input component
* style fixes and match heading size
* small fix
* fix search allocations placeholder
* adjust table styles
* move all installation settings helper text to below input
* update icon to use overflow menu buttons
* fix modal to be consistent
* open advanced properties when search
* remove other and custom properties, and update styles
* remove hide/show all java versions
* handle mc 26
* refactor: move server settings pages into /ui and add app ServerSettingsModal
* hook up server pages for app
* add server page header to app
* hook up server settings modal
* use large size
* fix card box shadow style
* fix hostname input for app
* fix app/website card containers
* implement external tabs for billing and admin billing
* fix save banner fixed to parent instead of page body
* remove unused prop to FriendsList causing warning in app
* fix client-only not available for app
* fix bottom cut off
* wire node auth
* implement full copy buttons
* dedup copy button tailwind styles
* fix hover class not working in @apply
* fix spacing
* fix error validation styles
* apply consistent styles and spacing
* feat: update hosting server card (#5609)
* fix type errors
* fix some stylesheets not imported for storybook
* add server listing stories
* add fix for frontend stylesheet imports
* remove props.
* convert copy code to use tailwind
* update server listing component styles
* update server info label styles
* start status/player count info label, more style updates and fixes
* add new server card buttons
* hook up server cards and implement updated styles
* hook up on download button
* fix tauri throwing error when api returns 204 No Content
* hook up purchase server modal in app
* fix upgrading state loading icon
* pnpm prepr
* filter out servers past 30 days after cancellation
* do not apply opacity on lock or spiner icons
* fix disabled server icon background
* update pending change stage
* handle known suspension states
* refactor: reduce code duplication for server listing
* update disabled state text color
* fix loading icon color
* clean up copy
* fix disabled opacity for server card
* update server listing files kept to be countdown
* implement resubscribe modal
* implement proper provisioning state for resubscribe
* fix duplicate attribute and pnpm prepr
* feat: add shared UI package auth DI
* feat: update purchase server flow (#5714)
* implement server list empty state component
* fix stories and adjust spacing
* implement select plan design refresh
* implement auth for empty server list
* use refs instead of reactive
* pnpm prepr
* fix auth usage for empty servers list
* move app auth provider setup to src/providers/setup
* pnpm prepr
* fix max height
* style fix
* fix getCreds no auth is blocking api client
* implement servers guest plan modal and signin which redirects back to modal's next step
* refactor guest plan select logic into provider
* implement sign in or create account popup
* remove force empty serverList
* add download button for suspended mod and generic
* add handling for when user logs out
* QA pass style fixes
* more consistent page styles
* fix duplicate export
* refactor: remove all fallback stuff from resubscribe modal
* implement shared download latest backup util
* i18n pass
* pnpm prepr
* fix region being selected if ping failed
* pnpm prepr
* feat: servers in app finalization (#5744)
* feat: start on shared console implementation into logs and overview pages
* fix: terminal gap issues
* feat: swap word wrap for full screen
* fix: stats cards alignment
* fix: stats
* feat: fix console clear + remove copy
* fix: lint
* fix: use reset not clear
* feat: shared server header & overview page for app and website (#5736)
* feat: implement shared server header for app and website
* feat: implement wrapped overview page with shared composable and hook it up
* pnpm prepr
* fix: bugs
* qa: cleanup
* feat: root.vue shared layout
* feat: delete old options pages + fix discovery frontend
* fix: discovery
* fix: misc style/layout issues
* fix page padding
* fix: modal height jankiness
* feat: implement server install content in app and server setup modal with DI
* fix: spacing
* remove servers in app feature flag
* Revert "remove servers in app feature flag"
This reverts commit 86e284c4bd.
* fix: qa
* feat: remove legacy components from apps/frontend/src/components/ui/servers
---------
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
* qa pass (#5738)
* fix: qa
* feat: qa
* fix: server icon fetch fails due to global node auth race condition overriding each other
* fix: lint
* fix: server icon upload/sync and centralize logic
* fix: server settings modal not closing for server reset
* fix: better server sorting
* feat: copy address in server listing card
* fix: notification panel in modal and when overlapping with action bar
* fix: empty server list empty state flashing when refresh, fixed by adding isReady auth flag
* feat: use floating action bar for save banner
* fix: saving state in save bar
* fix: edit server icon styling
* fix: confirm modal to have consistent buttons
* feat: loading animation for server panel + caching improvements for app
* pnpm prepr
* feat: search page deduplication (#5754)
* fix: action bar behind modal
* fix: remove warning modal for stopping
* fix: server cards states
* we hate webkit we hate webkit
* fix: update allocation creation to not use modal
* fix: properties tab spacing and styles
* feat: add files tab copy
* fix: advanced properties icon
* fix: remove back to all servers link
* feat: add files tab link in copy
* fix: server header styles to be consistent with instance
* fix: add header icons back
* feat: update instance settings icon to be consistent
* fix: icon container
* feat: upload state persistence across tabs
* fix: server labels text wrapping
* fix: use surface-5 border
* fix: loading spinner showing with onboarding below
* feat: new server button shows purchase modal in website
* fix: billing page not showing quarterly interval
* fix: server downgrade not showing updated subscription notification
* fix: server settings invalidate saved state and remove server context provider since its already provided in the page
* pnpm prepr
* add stripe publishable key to app build
* feat: console highlighting
* fix: rename servers title to modrinth hosting
* feat: search fix
* fix: qa/styles
* fix: ip click active and remove power dont ask again
* fix: qa
* feat: highlighting fix console
* fix: disable conflicts action
* fix: error dismiss bug
* feat: modal clarification
* fix: files perms issue
* fix: lint
* feat: modal fix
* enable show uptime
* fix: add loading state to edit server icon
* fix: notification panel take in has sidebar from settings
* fix: consistency pass on app settings
* fix: consistency pass on instance settings
* pnpm prepr
* fix: nagivate to billing button in app to go to website
* fix: stripe return url in app causing app to open modrinth.com in tauri
* refactor: better show polling UI code
* fix: new server polling comparison to use server ids instead of length
* fix: buttonstyled story
* fix: button styling
* fix: content.vue regression
* feat: project url redirects
* fix: breadcrumbs
* fix: purchase with newly added card
* fix: console ordering problems
* fix: app-frontend missing env config and staging environment
* fix: log syncing for instances and server panel accidentally
* fix: QA issues
* fix: server page loading state
* fix: stats card logic
* fix: lint
* fix: qa
* fix: console height padding
* fix: terminal padding + loading indicator
* feat: update medal server listing styling
* fix: no upgrade button for medal server listing in app
* fix: go to overview instead of content tab after onboarding
* fix: qa
* fix: teleport modals to body
* fix: logs tab + qa
* fix: local storage for user preferences
* fix: qa loading indic
* feat: considitonal debug and trace
* fix: jump to top on install bug
* feat: swap out server hard drive icon to server stack icon
* feat: servers in app feature flag default true
* fix: highlight row ufll
* fix: webkit thing onto a tag
* fix: input field
* fix: clear fix
* fix: lint
* fix: fmt
* feat: improve share modal and bring it back for sharing log
* pnpm prepr
* fix: menu overflowing
* feat: remove servers in app feature flag
* fix: server stat charts no longer showing color
* fix: library nav no primary state
* fix: better modal height and width
* fix: highlighting bugs
* fix: empty states
* fix: delay import to fix overview page slow load on MacOS
* fix: medal server listing too bright on light mode
* fix: admon analysis + fix logs
* fix: bug
* fix: clear purchase intent from sign-in after closing modal
* performance: improve server manage stats loading by splitting reactivity
* fix: deploy + admon + disable highlighting
* fix: clippy
---------
Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
* feat: temp wrangler
* fix: lint
* fix: logs upload
* fix: console empty state and admon regressions
* fix: fields
* feat: log deleting + prefetch for Logs.vue
* feat: move delete before share
* feat: clear endpoint
* feat: we ball!
---------
Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
756 lines
23 KiB
Vue
756 lines
23 KiB
Vue
<template>
|
|
<AddServerModal
|
|
ref="addServerModal"
|
|
:instance="instance"
|
|
@submit="
|
|
(server, start) => {
|
|
addServer(server)
|
|
if (start) {
|
|
joinWorld(server)
|
|
}
|
|
}
|
|
"
|
|
/>
|
|
<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"
|
|
/>
|
|
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<StyledInput
|
|
v-model="searchFilter"
|
|
:icon="SearchIcon"
|
|
type="text"
|
|
autocomplete="off"
|
|
:spellcheck="false"
|
|
input-class="!h-10"
|
|
wrapper-class="flex-1 min-w-0"
|
|
clearable
|
|
:placeholder="
|
|
formatMessage(messages.searchWorldsPlaceholder, { count: dedupedWorlds.length })
|
|
"
|
|
/>
|
|
<div class="flex gap-2">
|
|
<ButtonStyled type="outlined">
|
|
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
|
<PlusIcon class="size-5" />
|
|
{{ formatMessage(messages.addServer) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled color="brand">
|
|
<button
|
|
class="!h-10 flex items-center gap-2"
|
|
@click="
|
|
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
|
"
|
|
>
|
|
<CompassIcon class="size-5" />
|
|
<span>{{ formatMessage(messages.browseServers) }}</span>
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<div class="flex flex-wrap items-center gap-1.5">
|
|
<FilterIcon class="size-5 text-secondary" />
|
|
<button
|
|
:class="filterPillClass(selectedFilters.length === 0)"
|
|
@click="selectedFilters = []"
|
|
>
|
|
{{ formatMessage(commonMessages.allProjectType) }}
|
|
</button>
|
|
<button
|
|
v-for="option in filterOptions"
|
|
:key="option.id"
|
|
:class="filterPillClass(selectedFilters.includes(option.id))"
|
|
@click="toggleFilter(option.id)"
|
|
>
|
|
{{ option.label }}
|
|
</button>
|
|
</div>
|
|
<ButtonStyled type="transparent" hover-color-fill="none">
|
|
<button :disabled="refreshingAll" @click="refreshAllWorlds">
|
|
<RefreshCwIcon :class="refreshingAll ? 'animate-spin' : ''" />
|
|
{{ formatMessage(commonMessages.refreshButton) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
<div class="flex flex-col w-full gap-2">
|
|
<WorldItem
|
|
v-for="world in filteredWorlds"
|
|
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
|
:world="world"
|
|
:managed="world.type === 'server' ? isManagedServerWorld(world) : false"
|
|
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
|
:supports-server-quick-play="supportsServerQuickPlay"
|
|
:supports-world-quick-play="supportsWorldQuickPlay"
|
|
:current-protocol="protocolVersion"
|
|
:playing-instance="playing"
|
|
:playing-world="worldsMatch(world, worldPlaying)"
|
|
:starting-instance="startingInstance"
|
|
:refreshing="world.type === 'server' ? serverData[world.address]?.refreshing : undefined"
|
|
:server-status="world.type === 'server' ? serverData[world.address]?.status : undefined"
|
|
:rendered-motd="
|
|
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
|
|
"
|
|
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
|
|
@play="() => joinWorld(world)"
|
|
@stop="() => emit('stop')"
|
|
@refresh="() => refreshServer((world as ServerWorld).address)"
|
|
@edit="
|
|
() =>
|
|
world.type === 'singleplayer'
|
|
? editWorldModal?.show(world)
|
|
: isManagedServerWorld(world)
|
|
? undefined
|
|
: editServerModal?.show(world)
|
|
"
|
|
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
|
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<EmptyState
|
|
v-else
|
|
type="empty-inbox"
|
|
:heading="formatMessage(messages.noWorldsHeading)"
|
|
:description="formatMessage(messages.noWorldsDescription)"
|
|
>
|
|
<template #actions>
|
|
<ButtonStyled type="outlined">
|
|
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
|
<PlusIcon class="size-5" />
|
|
{{ formatMessage(messages.addServer) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled color="brand">
|
|
<button
|
|
class="!h-10 flex items-center gap-2"
|
|
@click="
|
|
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
|
"
|
|
>
|
|
<CompassIcon class="size-5" />
|
|
<span>{{ formatMessage(messages.browseServers) }}</span>
|
|
</button>
|
|
</ButtonStyled>
|
|
</template>
|
|
</EmptyState>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { CompassIcon, FilterIcon, PlusIcon, RefreshCwIcon, SearchIcon } from '@modrinth/assets'
|
|
import {
|
|
ButtonStyled,
|
|
commonMessages,
|
|
defineMessages,
|
|
EmptyState,
|
|
GAME_MODES,
|
|
type GameVersion,
|
|
injectNotificationManager,
|
|
StyledInput,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
import { platform } from '@tauri-apps/plugin-os'
|
|
import { computed, onUnmounted, 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 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 { get_game_versions } from '@/helpers/tags'
|
|
import type { GameInstance } from '@/helpers/types'
|
|
import {
|
|
delete_world,
|
|
get_profile_protocol_version,
|
|
getServerDomainKey,
|
|
getWorldIdentifier,
|
|
handleDefaultProfileUpdateEvent,
|
|
hasServerQuickPlaySupport,
|
|
hasWorldQuickPlaySupport,
|
|
normalizeServerAddress,
|
|
type ProfileEvent,
|
|
type ProtocolVersion,
|
|
refreshServerData,
|
|
refreshServers,
|
|
refreshWorld,
|
|
refreshWorlds,
|
|
remove_server_from_profile,
|
|
resolveManagedServerWorld,
|
|
type ServerData,
|
|
type ServerWorld,
|
|
showWorldInFolder,
|
|
type SingleplayerWorld,
|
|
sortWorlds,
|
|
start_join_server,
|
|
start_join_singleplayer_world,
|
|
type World,
|
|
} 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...',
|
|
},
|
|
addServer: {
|
|
id: 'app.instance.worlds.add-server',
|
|
defaultMessage: 'Add server',
|
|
},
|
|
browseServers: {
|
|
id: 'app.instance.worlds.browse-servers',
|
|
defaultMessage: 'Browse servers',
|
|
},
|
|
noWorldsHeading: {
|
|
id: 'app.instance.worlds.no-worlds-heading',
|
|
defaultMessage: 'No servers or worlds added',
|
|
},
|
|
noWorldsDescription: {
|
|
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',
|
|
},
|
|
moddedFilter: {
|
|
id: 'app.instance.worlds.filter-modded',
|
|
defaultMessage: 'Modded',
|
|
},
|
|
onlineFilter: {
|
|
id: 'app.instance.worlds.filter-online',
|
|
defaultMessage: 'Online',
|
|
},
|
|
offlineFilter: {
|
|
id: 'app.instance.worlds.filter-offline',
|
|
defaultMessage: 'Offline',
|
|
},
|
|
})
|
|
|
|
const { formatMessage } = useVIntl()
|
|
const { handleError } = injectNotificationManager()
|
|
const { playServerProject } = injectServerInstall()
|
|
const route = useRoute()
|
|
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 serverToRemove = ref<ServerWorld>()
|
|
const worldToDelete = ref<SingleplayerWorld>()
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'play', world: World): void
|
|
(event: 'stop'): void
|
|
}>()
|
|
|
|
const props = defineProps<{
|
|
instance: GameInstance
|
|
options: InstanceType<typeof ContextMenu> | null
|
|
offline: boolean
|
|
playing: boolean
|
|
installed: boolean
|
|
}>()
|
|
|
|
const instance = computed(() => props.instance)
|
|
const playing = computed(() => props.playing)
|
|
|
|
function play(world: World) {
|
|
emit('play', world)
|
|
}
|
|
|
|
const selectedFilters = ref<string[]>([])
|
|
const searchFilter = ref('')
|
|
|
|
function filterPillClass(isActive: boolean) {
|
|
return [
|
|
'cursor-pointer rounded-full border border-solid px-3 py-1.5 text-base font-semibold leading-5 transition-all duration-100 active:scale-[0.97]',
|
|
isActive
|
|
? 'border-green bg-brand-highlight text-brand'
|
|
: 'border-surface-5 bg-surface-4 text-primary hover:bg-surface-5',
|
|
]
|
|
}
|
|
|
|
function toggleFilter(id: string) {
|
|
const idx = selectedFilters.value.indexOf(id)
|
|
if (idx >= 0) {
|
|
selectedFilters.value.splice(idx, 1)
|
|
} else {
|
|
selectedFilters.value.push(id)
|
|
if (id === 'singleplayer') {
|
|
selectedFilters.value = selectedFilters.value.filter((f) => f !== 'online' && f !== 'offline')
|
|
} else if (id === 'online' || id === 'offline') {
|
|
selectedFilters.value = selectedFilters.value.filter((f) => f !== 'singleplayer')
|
|
}
|
|
}
|
|
}
|
|
|
|
const refreshingAll = ref(false)
|
|
const hadNoWorlds = ref(true)
|
|
const startingInstance = ref(false)
|
|
const worldPlaying = ref<World>()
|
|
|
|
const worlds = ref<World[]>([])
|
|
const serverData = ref<Record<string, ServerData>>({})
|
|
|
|
// Track servers_updated calls on Linux to prevent server ping spam
|
|
const MAX_LINUX_REFRESHES = 3
|
|
const isLinux = platform() === 'linux'
|
|
const linuxRefreshCount = ref(0)
|
|
|
|
const protocolVersion = ref<ProtocolVersion | null>(null)
|
|
const managedServerName = ref<string | null>(null)
|
|
const managedServerAddress = ref<string | null>(null)
|
|
|
|
const managedServerWorld = computed(() =>
|
|
resolveManagedServerWorld(worlds.value, managedServerName.value, managedServerAddress.value),
|
|
)
|
|
|
|
function isManagedServerWorld(world: World): world is ServerWorld {
|
|
return world.type === 'server' && managedServerWorld.value?.index === world.index
|
|
}
|
|
|
|
async function refreshManagedServerMetadata() {
|
|
await ensureManagedServerWorldExists(
|
|
instance.value.path,
|
|
managedServerName.value,
|
|
managedServerAddress.value,
|
|
)
|
|
|
|
const projectId = instance.value.linked_data?.project_id
|
|
if (!projectId) {
|
|
managedServerName.value = null
|
|
managedServerAddress.value = null
|
|
return
|
|
}
|
|
|
|
try {
|
|
const [project, projectV3] = await Promise.all([
|
|
get_project(projectId, 'bypass'),
|
|
get_project_v3(projectId, 'bypass'),
|
|
])
|
|
|
|
if (projectV3?.minecraft_server == null) {
|
|
managedServerName.value = null
|
|
managedServerAddress.value = null
|
|
return
|
|
}
|
|
|
|
const serverAddress = getServerAddress(projectV3.minecraft_java_server)
|
|
if (!serverAddress) {
|
|
managedServerName.value = null
|
|
managedServerAddress.value = null
|
|
return
|
|
}
|
|
|
|
managedServerName.value = project.title
|
|
managedServerAddress.value = serverAddress
|
|
} catch (err) {
|
|
console.error(
|
|
`Failed to resolve managed server metadata for profile: ${instance.value.path}`,
|
|
err,
|
|
)
|
|
managedServerName.value = null
|
|
managedServerAddress.value = null
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => instance.value.linked_data?.project_id,
|
|
async () => {
|
|
await refreshManagedServerMetadata()
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
const [unlistenProfile, , resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
|
profile_listener(async (e: ProfileEvent) => {
|
|
if (e.profile_path_id !== instance.value.path) return
|
|
|
|
console.info(`Handling profile event '${e.event}' for profile: ${e.profile_path_id}`)
|
|
|
|
if (e.event === 'servers_updated') {
|
|
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
|
if (isLinux) linuxRefreshCount.value++
|
|
|
|
await refreshAllWorlds()
|
|
}
|
|
|
|
await handleDefaultProfileUpdateEvent(worlds.value, instance.value.path, e)
|
|
}),
|
|
refreshAllWorlds(),
|
|
get_profile_protocol_version(instance.value.path).catch(() => null),
|
|
get_game_versions().catch(() => [] as GameVersion[]),
|
|
])
|
|
|
|
protocolVersion.value = resolvedProtocolVersion
|
|
|
|
async function refreshServer(address: string) {
|
|
if (!serverData.value[address]) {
|
|
serverData.value[address] = {
|
|
refreshing: true,
|
|
}
|
|
}
|
|
await refreshServerData(serverData.value[address], protocolVersion.value, address)
|
|
}
|
|
|
|
async function refreshAllWorlds() {
|
|
if (refreshingAll.value) {
|
|
console.log(`Already refreshing, cancelling refresh.`)
|
|
return
|
|
}
|
|
await refreshManagedServerMetadata()
|
|
|
|
refreshingAll.value = true
|
|
|
|
worlds.value = await refreshWorlds(instance.value.path).finally(
|
|
() => (refreshingAll.value = false),
|
|
)
|
|
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
|
|
|
const hasNoWorlds = worlds.value.length === 0
|
|
|
|
if (hadNoWorlds.value && hasNoWorlds) {
|
|
setTimeout(() => {
|
|
refreshingAll.value = false
|
|
}, 1000)
|
|
} else {
|
|
refreshingAll.value = false
|
|
}
|
|
|
|
hadNoWorlds.value = hasNoWorlds
|
|
}
|
|
|
|
async function addServer(server: ServerWorld) {
|
|
worlds.value.push(server)
|
|
sortWorlds(worlds.value)
|
|
await refreshServer(server.address)
|
|
}
|
|
|
|
async function editServer(server: ServerWorld) {
|
|
const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index)
|
|
if (index !== -1) {
|
|
const oldServer = worlds.value[index] as ServerWorld
|
|
worlds.value[index] = server
|
|
sortWorlds(worlds.value)
|
|
if (oldServer.address !== server.address) {
|
|
await refreshServer(server.address)
|
|
}
|
|
} else {
|
|
handleError(new Error(`Error refreshing server, refreshing all worlds`))
|
|
await refreshAllWorlds()
|
|
}
|
|
}
|
|
|
|
async function removeServer(server: ServerWorld) {
|
|
await remove_server_from_profile(instance.value.path, 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) {
|
|
if (w.type === 'server') {
|
|
w.index = serverIdx++
|
|
}
|
|
}
|
|
}
|
|
|
|
async function editWorld(path: string, name: string, removeIcon: boolean) {
|
|
const world = worlds.value.find((world) => world.type === 'singleplayer' && world.path === path)
|
|
if (world) {
|
|
world.name = name
|
|
if (removeIcon) {
|
|
world.icon = undefined
|
|
}
|
|
sortWorlds(worlds.value)
|
|
} else {
|
|
handleError(new Error(`Error finding world in list, refreshing all worlds`))
|
|
await refreshAllWorlds()
|
|
}
|
|
}
|
|
|
|
async function deleteWorld(world: SingleplayerWorld) {
|
|
await delete_world(instance.value.path, 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 })
|
|
startingInstance.value = false
|
|
worldPlaying.value = undefined
|
|
}
|
|
|
|
async function joinWorld(world: World) {
|
|
console.log(`Joining world ${getWorldIdentifier(world)}`)
|
|
startingInstance.value = true
|
|
worldPlaying.value = world
|
|
if (world.type === 'server') {
|
|
const managedProjectId = instance.value.linked_data?.project_id
|
|
if (managedProjectId && isManagedServerWorld(world)) {
|
|
await playServerProject(managedProjectId).catch(handleJoinError)
|
|
trackEvent('InstanceStart', {
|
|
loader: instance.value.loader,
|
|
game_version: instance.value.game_version,
|
|
source: 'WorldsPage',
|
|
})
|
|
startingInstance.value = false
|
|
return
|
|
}
|
|
await start_join_server(instance.value.path, 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)
|
|
}
|
|
play(world)
|
|
startingInstance.value = false
|
|
}
|
|
|
|
watch(
|
|
() => playing.value,
|
|
(playing) => {
|
|
if (!playing) {
|
|
worldPlaying.value = undefined
|
|
|
|
setTimeout(async () => {
|
|
for (const world of worlds.value) {
|
|
if (world.type === 'singleplayer' && world.locked) {
|
|
await refreshWorld(worlds.value, instance.value.path, world.path)
|
|
}
|
|
}
|
|
}, 1000)
|
|
}
|
|
},
|
|
)
|
|
|
|
function worldsMatch(world: World, other: World | undefined) {
|
|
if (world.type === 'server' && other?.type === 'server') {
|
|
return world.address === other.address
|
|
} else if (world.type === 'singleplayer' && other?.type === 'singleplayer') {
|
|
return world.path === other.path
|
|
}
|
|
return false
|
|
}
|
|
|
|
const gameVersions = ref<GameVersion[]>(resolvedGameVersions)
|
|
const supportsServerQuickPlay = computed(() =>
|
|
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
|
)
|
|
const supportsWorldQuickPlay = computed(() =>
|
|
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
|
)
|
|
|
|
const dedupedWorlds = computed(() => {
|
|
const visibleWorlds: World[] = []
|
|
const serverIndexByDomain = new Map<string, number>()
|
|
|
|
for (const world of worlds.value) {
|
|
if (world.type !== 'server') {
|
|
visibleWorlds.push(world)
|
|
continue
|
|
}
|
|
|
|
const domainKey =
|
|
getServerDomainKey(world.address) ||
|
|
normalizeServerAddress(world.address) ||
|
|
`server-${world.index}`
|
|
const existingIndex = serverIndexByDomain.get(domainKey)
|
|
|
|
if (existingIndex == null) {
|
|
serverIndexByDomain.set(domainKey, visibleWorlds.length)
|
|
visibleWorlds.push(world)
|
|
continue
|
|
}
|
|
|
|
// replace world with managed world if applicable
|
|
const existingWorld = visibleWorlds[existingIndex]
|
|
if (
|
|
existingWorld?.type === 'server' &&
|
|
!isManagedServerWorld(existingWorld) &&
|
|
isManagedServerWorld(world)
|
|
) {
|
|
visibleWorlds[existingIndex] = world
|
|
}
|
|
}
|
|
|
|
return visibleWorlds
|
|
})
|
|
|
|
const filterOptions = computed(() => {
|
|
const options: { id: string; label: string }[] = []
|
|
const hasSingleplayer = dedupedWorlds.value.some((x) => x.type === 'singleplayer')
|
|
const hasServer = dedupedWorlds.value.some((x) => x.type === 'server')
|
|
|
|
const hasStatusFilter =
|
|
selectedFilters.value.includes('online') || selectedFilters.value.includes('offline')
|
|
|
|
if (hasSingleplayer && hasServer && !hasStatusFilter) {
|
|
options.push({ id: 'singleplayer', label: formatMessage(commonMessages.singleplayerLabel) })
|
|
}
|
|
|
|
if (hasServer) {
|
|
const servers = dedupedWorlds.value.filter((x) => x.type === 'server')
|
|
const hasVanilla = servers.some((x) => x.content_kind !== 'modpack')
|
|
const hasModded = servers.some((x) => x.content_kind === 'modpack')
|
|
if (hasVanilla && hasModded) {
|
|
options.push({ id: 'vanilla', label: formatMessage(messages.vanillaFilter) })
|
|
options.push({ id: 'modded', label: formatMessage(messages.moddedFilter) })
|
|
}
|
|
if (!selectedFilters.value.includes('singleplayer')) {
|
|
const hasOnline = servers.some((x) => !!serverData.value[x.address]?.status)
|
|
const hasOffline = servers.some((x) => !serverData.value[x.address]?.status)
|
|
if (hasOnline && hasOffline) {
|
|
options.push({ id: 'online', label: formatMessage(messages.onlineFilter) })
|
|
options.push({ id: 'offline', label: formatMessage(messages.offlineFilter) })
|
|
}
|
|
}
|
|
}
|
|
|
|
return options
|
|
})
|
|
|
|
watch(filterOptions, (options) => {
|
|
const validIds = new Set(options.map((opt) => opt.id))
|
|
const cleaned = selectedFilters.value.filter((f) => validIds.has(f))
|
|
if (cleaned.length !== selectedFilters.value.length) {
|
|
selectedFilters.value = cleaned
|
|
}
|
|
})
|
|
|
|
const filteredWorlds = computed(() =>
|
|
dedupedWorlds.value.filter((x) => {
|
|
if (searchFilter.value && !x.name.toLowerCase().includes(searchFilter.value.toLowerCase())) {
|
|
return false
|
|
}
|
|
|
|
if (selectedFilters.value.length === 0) return true
|
|
|
|
const hasSingleplayerFilter = selectedFilters.value.includes('singleplayer')
|
|
const typeFilters = selectedFilters.value.filter((f) => f === 'vanilla' || f === 'modded')
|
|
const statusFilters = selectedFilters.value.filter((f) => f === 'online' || f === 'offline')
|
|
|
|
if (x.type === 'singleplayer') {
|
|
return hasSingleplayerFilter || (typeFilters.length === 0 && statusFilters.length === 0)
|
|
}
|
|
|
|
if (hasSingleplayerFilter && typeFilters.length === 0 && statusFilters.length === 0) {
|
|
return false
|
|
}
|
|
|
|
let passesType = true
|
|
if (typeFilters.length > 0) {
|
|
const isModded = x.content_kind === 'modpack'
|
|
passesType =
|
|
(typeFilters.includes('modded') && isModded) ||
|
|
(typeFilters.includes('vanilla') && !isModded)
|
|
}
|
|
|
|
let passesStatus = true
|
|
if (statusFilters.length > 0) {
|
|
const isOnline = !!serverData.value[x.address]?.status
|
|
passesStatus =
|
|
(statusFilters.includes('online') && isOnline) ||
|
|
(statusFilters.includes('offline') && !isOnline)
|
|
}
|
|
|
|
return passesType && passesStatus
|
|
}),
|
|
)
|
|
|
|
const highlightedWorld = ref(route.query.highlight)
|
|
|
|
function promptToRemoveWorld(world: World): boolean {
|
|
if (world.type === 'server') {
|
|
serverToRemove.value = world
|
|
removeServerModal.value?.show()
|
|
return !!removeServerModal.value
|
|
} else {
|
|
worldToDelete.value = world
|
|
deleteWorldModal.value?.show()
|
|
return !!deleteWorldModal.value
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
unlistenProfile()
|
|
})
|
|
</script>
|