feat:移除了弹窗,服务器添加sls
This commit is contained in:
775
apps/app-frontend/src/pages/instance/Worlds.vue
Normal file
775
apps/app-frontend/src/pages/instance/Worlds.vue
Normal file
@ -0,0 +1,775 @@
|
||||
<template>
|
||||
<AddServerModal
|
||||
ref="addServerModal"
|
||||
:instance="instance"
|
||||
@submit="
|
||||
(server, start) => {
|
||||
addServer(server)
|
||||
if (start) {
|
||||
joinWorld(server)
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<EditServerModal ref="editServerModal" :instance="instance" @submit="editServer" />
|
||||
<ConfirmRemoveWorldModal
|
||||
ref="removeWorldModal"
|
||||
:world="worldToRemove"
|
||||
:symlink-target="instance.symlink_target"
|
||||
@confirm="proceedRemoveWorld"
|
||||
/>
|
||||
<ReadyTransition :pending="worldsReadyPending">
|
||||
<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" @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.id, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({
|
||||
path: '/browse/world',
|
||||
query: { i: instance.id, from: 'world-maps' },
|
||||
})
|
||||
"
|
||||
>
|
||||
<WorldIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseMaps) }}</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"
|
||||
:shortcut-instance-id="instance.id"
|
||||
@update="refreshAllWorlds"
|
||||
@play="() => joinWorld(world)"
|
||||
@stop="() => emit('stop')"
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
@edit="
|
||||
() =>
|
||||
world.type === 'singleplayer'
|
||||
? router.push(
|
||||
`/instance/${encodeURIComponent(instance.id)}/worlds/${encodeURIComponent(world.path)}/edit`,
|
||||
)
|
||||
: isManagedServerWorld(world)
|
||||
? undefined
|
||||
: editServerModal?.show(world)
|
||||
"
|
||||
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.id, 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" @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.id, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({ path: '/browse/world', query: { i: instance.id, from: 'world-maps' } })
|
||||
"
|
||||
>
|
||||
<WorldIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseMaps) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</ReadyTransition>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CompassIcon,
|
||||
FilterIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
WorldIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
EmptyState,
|
||||
GAME_MODES,
|
||||
type GameVersion,
|
||||
injectNotificationManager,
|
||||
ReadyTransition,
|
||||
StyledInput,
|
||||
useReadyState,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||
import ConfirmRemoveWorldModal from '@/components/ui/world/modal/ConfirmRemoveWorldModal.vue'
|
||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import { useMinecraftLaunchError } from '@/composables/useMinecraftLaunchError'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project, get_project_v3 } from '@/helpers/cache.js'
|
||||
import { instance_listener } from '@/helpers/events'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
|
||||
import {
|
||||
delete_world,
|
||||
get_instance_protocol_version,
|
||||
getServerDomainKey,
|
||||
getWorldIdentifier,
|
||||
handleDefaultInstanceUpdateEvent,
|
||||
hasServerQuickPlaySupport,
|
||||
hasWorldQuickPlaySupport,
|
||||
type InstanceEvent,
|
||||
normalizeServerAddress,
|
||||
type ProtocolVersion,
|
||||
refreshServerData,
|
||||
refreshServers,
|
||||
refreshWorld,
|
||||
refreshWorlds,
|
||||
remove_server_from_instance,
|
||||
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'
|
||||
|
||||
const messages = defineMessages({
|
||||
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',
|
||||
},
|
||||
browseMaps: {
|
||||
id: 'app.instance.worlds.browse-maps',
|
||||
defaultMessage: 'Browse maps',
|
||||
},
|
||||
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',
|
||||
},
|
||||
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 handleMinecraftLaunchError = useMinecraftLaunchError()
|
||||
const { playServerProject } = injectServerInstall()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const addServerModal = ref<InstanceType<typeof AddServerModal>>()
|
||||
const editServerModal = ref<InstanceType<typeof EditServerModal>>()
|
||||
const removeWorldModal = ref<InstanceType<typeof ConfirmRemoveWorldModal>>()
|
||||
|
||||
const worldToRemove = ref<World | null>(null)
|
||||
|
||||
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-brand 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 queryClient = useQueryClient()
|
||||
|
||||
const refreshingAll = ref(false)
|
||||
const hadNoWorlds = ref(true)
|
||||
const startingInstance = ref(false)
|
||||
const worldPlaying = ref<World>()
|
||||
|
||||
const worldsQuery = useQuery({
|
||||
queryKey: computed(() => ['worlds', instance.value.id]),
|
||||
queryFn: () => refreshWorlds(instance.value.id),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const worldsReadyPending = useReadyState(worldsQuery)
|
||||
|
||||
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 protocolVersionReady = ref(false)
|
||||
|
||||
const gameVersions = ref<GameVersion[]>([])
|
||||
const supportsServerQuickPlay = computed(() =>
|
||||
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
const supportsWorldQuickPlay = computed(() =>
|
||||
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => worldsQuery.data.value,
|
||||
(data) => {
|
||||
if (data) {
|
||||
worlds.value = [...data]
|
||||
hadNoWorlds.value = worlds.value.length === 0
|
||||
if (!refreshingAll.value) {
|
||||
void refreshServers(
|
||||
worlds.value,
|
||||
serverData.value,
|
||||
protocolVersion.value,
|
||||
protocolVersionReady.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
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.id,
|
||||
managedServerName.value,
|
||||
managedServerAddress.value,
|
||||
)
|
||||
|
||||
const projectId = instance.value.link?.project_id
|
||||
if (!projectId) {
|
||||
managedServerName.value = null
|
||||
managedServerAddress.value = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const [project, projectV3] = await Promise.all([
|
||||
get_project(projectId),
|
||||
get_project_v3(projectId),
|
||||
])
|
||||
|
||||
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 instance: ${instance.value.id}`,
|
||||
err,
|
||||
)
|
||||
managedServerName.value = null
|
||||
managedServerAddress.value = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => instance.value.link?.project_id,
|
||||
async () => {
|
||||
await refreshManagedServerMetadata()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
let unlistenInstance: (() => void) | null = null
|
||||
let worldsTabAlive = true
|
||||
|
||||
async function initWorldsTab() {
|
||||
const [_unlistenInstance, resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
instance_listener(async (e: InstanceEvent) => {
|
||||
if (e.instance_id !== instance.value.id) return
|
||||
|
||||
console.info(`Handling instance event '${e.event}' for instance: ${e.instance_id}`)
|
||||
|
||||
if (e.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
if (isLinux) linuxRefreshCount.value++
|
||||
|
||||
await refreshAllWorlds()
|
||||
}
|
||||
|
||||
await handleDefaultInstanceUpdateEvent(worlds.value, instance.value.id, e)
|
||||
}),
|
||||
get_instance_protocol_version(instance.value.id).catch(() => null),
|
||||
get_game_versions().catch(() => [] as GameVersion[]),
|
||||
])
|
||||
|
||||
if (!worldsTabAlive) {
|
||||
_unlistenInstance()
|
||||
return
|
||||
}
|
||||
|
||||
unlistenInstance = _unlistenInstance
|
||||
protocolVersion.value = resolvedProtocolVersion
|
||||
gameVersions.value = resolvedGameVersions
|
||||
protocolVersionReady.value = true
|
||||
|
||||
if (worlds.value.length > 0) {
|
||||
void refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
}
|
||||
}
|
||||
|
||||
await initWorldsTab()
|
||||
|
||||
async function refreshServer(address: string) {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
}
|
||||
}
|
||||
if (!protocolVersionReady.value) return
|
||||
await refreshServerData(serverData.value[address], protocolVersion.value, address)
|
||||
}
|
||||
|
||||
async function refreshAllWorlds() {
|
||||
if (refreshingAll.value) {
|
||||
console.log(`Already refreshing, cancelling refresh.`)
|
||||
return
|
||||
}
|
||||
|
||||
refreshingAll.value = true
|
||||
try {
|
||||
for (const world of worlds.value) {
|
||||
if (world.type === 'server') {
|
||||
if (!serverData.value[world.address]) {
|
||||
serverData.value[world.address] = { refreshing: true }
|
||||
} else {
|
||||
serverData.value[world.address].refreshing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.value.id] })
|
||||
await refreshServers(
|
||||
worlds.value,
|
||||
serverData.value,
|
||||
protocolVersion.value,
|
||||
protocolVersionReady.value,
|
||||
)
|
||||
} finally {
|
||||
refreshingAll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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_instance(instance.value.id, server.index).catch(handleError)
|
||||
worlds.value = worlds.value.filter((w) => w.type !== 'server' || w.index !== server.index)
|
||||
let serverIdx = 0
|
||||
for (const w of worlds.value) {
|
||||
if (w.type === 'server') {
|
||||
w.index = serverIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWorld(world: SingleplayerWorld) {
|
||||
await delete_world(instance.value.id, world.path).catch(handleError)
|
||||
worlds.value = worlds.value.filter((w) => w.type !== 'singleplayer' || w.path !== world.path)
|
||||
}
|
||||
|
||||
async function handleJoinError(err: Error) {
|
||||
const handled = await handleMinecraftLaunchError(err, {
|
||||
instance_id: instance.value.id,
|
||||
instance_name: instance.value.name,
|
||||
})
|
||||
if (!handled) handleSevereError(err, { instanceId: instance.value.id })
|
||||
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.link?.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.id, world.address).catch(handleJoinError)
|
||||
trackEvent('InstanceStart', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
source: 'WorldsPage',
|
||||
})
|
||||
} else if (world.type === 'singleplayer') {
|
||||
await start_join_singleplayer_world(instance.value.id, 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.id, 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 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')
|
||||
|
||||
if (hasSingleplayer && hasServer) {
|
||||
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) })
|
||||
}
|
||||
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 {
|
||||
worldToRemove.value = world
|
||||
removeWorldModal.value?.show()
|
||||
return !!removeWorldModal.value
|
||||
}
|
||||
|
||||
async function proceedRemoveWorld(world: World) {
|
||||
if (world.type === 'server') {
|
||||
await removeServer(world)
|
||||
} else {
|
||||
await deleteWorld(world)
|
||||
}
|
||||
worldToRemove.value = null
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
worldsTabAlive = false
|
||||
unlistenInstance?.()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user