Files
Rocketmc/apps/app-frontend/src/pages/instance/Index.vue
Geometrically d1bc65c266 Monorepo missing features (#1273)
* fix tauri config

* fix package patch

* regen pnpm lock

* use new workflow

* New GH actions

* Update lockfile

* update scripts

* Fix build script

* Fix missing deps

* Fix assets eslint

* Update libraries lint

* Fix all lint configs

* update lockfile

* add fmt + clippy fails

* Separate App Tauri portion

* fix app features

* Fix lints

* install tauri cli

* update lockfile

* corepack, fix lints

* add store path

* fix unused import

* Fix tests

* Issue templates + port over tauri release

* fix actions

* fix before build command

* Add X86 target

* Update build matrix

* finalize actions

* make debug build smaller

* Use debug build to make cache smaller

* dummy commit

* change proj name

* update file name

* Use release builds for less space use

* Remove rust cache

* Readd for app build

* add merge queue trigger
2024-07-09 15:17:38 -07:00

519 lines
12 KiB
Vue

<template>
<div class="instance-container">
<div class="side-cards">
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar
size="lg"
:src="
!instance.metadata.icon ||
(instance.metadata.icon && instance.metadata.icon.startsWith('http'))
? instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
/>
<div class="instance-info">
<h2 class="name">{{ instance.metadata.name }}</h2>
<span class="metadata">
{{ instance.metadata.loader }} {{ instance.metadata.game_version }}
</span>
</div>
<span class="button-group">
<Button v-if="instance.install_stage !== 'installed'" disabled class="instance-button">
Installing...
</Button>
<Button
v-else-if="playing === true"
color="danger"
class="instance-button"
@click="stopInstance('InstancePage')"
@mouseover="checkProcess"
>
<StopCircleIcon />
Stop
</Button>
<Button
v-else-if="playing === false && loading === false"
color="primary"
class="instance-button"
@click="startInstance('InstancePage')"
@mouseover="checkProcess"
>
<PlayIcon />
Play
</Button>
<Button
v-else-if="loading === true && playing === false"
disabled
class="instance-button"
>
Loading...
</Button>
<Button
v-tooltip="'Open instance folder'"
class="instance-button"
@click="showProfileInFolder(instance.path)"
>
<FolderOpenIcon />
Folder
</Button>
</span>
<hr class="card-divider" />
<div class="pages-list">
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/`" class="btn">
<BoxIcon />
Content
</RouterLink>
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/logs`" class="btn">
<FileIcon />
Logs
</RouterLink>
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/options`" class="btn">
<SettingsIcon />
Options
</RouterLink>
</div>
</Card>
</div>
<div class="content">
<Promotion :external="false" query-param="?r=launcher" />
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
<component
:is="Component"
:instance="instance"
:options="options"
:offline="offline"
:playing="playing"
:versions="modrinthVersions"
:installed="instance.install_stage !== 'installed'"
></component>
</Suspense>
</template>
</RouterView>
</div>
</div>
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #play> <PlayIcon /> Play </template>
<template #stop> <StopCircleIcon /> Stop </template>
<template #add_content> <PlusIcon /> Add Content </template>
<template #edit> <EditIcon /> Edit </template>
<template #copy_path> <ClipboardCopyIcon /> Copy Path </template>
<template #open_folder> <ClipboardCopyIcon /> Open Folder </template>
<template #copy_link> <ClipboardCopyIcon /> Copy Link </template>
<template #open_link> <ClipboardCopyIcon /> Open In Modrinth <ExternalIcon /> </template>
<template #copy_names><EditIcon />Copy names</template>
<template #copy_slugs><HashIcon />Copy slugs</template>
<template #copy_links><GlobeIcon />Copy Links</template>
<template #toggle><EditIcon />Toggle selected</template>
<template #disable><XIcon />Disable selected</template>
<template #enable><CheckCircleIcon />Enable selected</template>
<template #hide_show><EyeIcon />Show/Hide unselected</template>
<template #update_all
><UpdatedIcon />Update {{ selected.length > 0 ? 'selected' : 'all' }}</template
>
<template #filter_update><UpdatedIcon />Select Updatable</template>
</ContextMenu>
</template>
<script setup>
import { Button, Avatar, Card, Promotion } from '@modrinth/ui'
import {
BoxIcon,
SettingsIcon,
FileIcon,
PlayIcon,
StopCircleIcon,
EditIcon,
FolderOpenIcon,
ClipboardCopyIcon,
PlusIcon,
ExternalIcon,
HashIcon,
GlobeIcon,
EyeIcon,
XIcon,
CheckCircleIcon,
UpdatedIcon,
} from '@modrinth/assets'
import { get, run } from '@/helpers/profile'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
kill_by_uuid,
} from '@/helpers/process'
import { offline_listener, process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router'
import { ref, onUnmounted } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { isOffline, showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useFetch } from '@/helpers/fetch'
import { handleSevereError } from '@/store/error.js'
const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const instance = ref(await get(route.params.id).catch(handleError))
breadcrumbs.setName(
'Instance',
instance.value.metadata.name.length > 40
? instance.value.metadata.name.substring(0, 40) + '...'
: instance.value.metadata.name,
)
breadcrumbs.setContext({
name: instance.value.metadata.name,
link: route.path,
query: route.query,
})
const offline = ref(await isOffline())
const loadingBar = useLoading()
const uuid = ref(null)
const playing = ref(false)
const loading = ref(false)
const options = ref(null)
const startInstance = async (context) => {
loading.value = true
uuid.value = await run(route.params.id).catch(handleSevereError)
loading.value = false
playing.value = true
mixpanel_track('InstanceStart', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
source: context,
})
}
const checkProcess = async () => {
const runningPaths = await get_all_running_profile_paths().catch(handleError)
if (runningPaths.includes(instance.value.path)) {
playing.value = true
return
}
playing.value = false
uuid.value = null
}
// Get information on associated modrinth versions, if any
const modrinthVersions = ref([])
if (!(await isOffline()) && instance.value.metadata.linked_data?.project_id) {
modrinthVersions.value = await useFetch(
`https://api.modrinth.com/v2/project/${instance.value.metadata.linked_data.project_id}/version`,
'project',
)
}
await checkProcess()
const stopInstance = async (context) => {
playing.value = false
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(instance.value.path).catch(handleError)
uuid.value = uuids[0] // populate Uuid to listen for in the process_listener
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError)
mixpanel_track('InstanceStop', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
source: context,
})
}
const handleRightClick = (event) => {
const baseOptions = [
{ name: 'add_content' },
{ type: 'divider' },
{ name: 'edit' },
{ name: 'open_folder' },
{ name: 'copy_path' },
]
options.value.showMenu(
event,
instance.value,
playing.value
? [
{
name: 'stop',
color: 'danger',
},
...baseOptions,
]
: [
{
name: 'play',
color: 'primary',
},
...baseOptions,
],
)
}
const handleOptionsClick = async (args) => {
switch (args.option) {
case 'play':
await startInstance('InstancePageContextMenu')
break
case 'stop':
await stopInstance('InstancePageContextMenu')
break
case 'add_content':
await router.push({
path: `/browse/${instance.value.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: route.params.id },
})
break
case 'edit':
await router.push({
path: `/instance/${encodeURIComponent(route.params.id)}/options`,
})
break
case 'open_folder':
await showProfileInFolder(instance.value.path)
break
case 'copy_path':
await navigator.clipboard.writeText(instance.value.path)
break
}
}
const unlistenProfiles = await profile_listener(async (event) => {
if (event.profile_path_id === route.params.id) {
if (event.event === 'removed') {
await router.push({
path: '/',
})
return
}
instance.value = await get(route.params.id).catch(handleError)
}
})
const unlistenProcesses = await process_listener((e) => {
if (e.event === 'finished' && uuid.value === e.uuid) playing.value = false
})
const unlistenOffline = await offline_listener((b) => {
offline.value = b
})
onUnmounted(() => {
unlistenProcesses()
unlistenProfiles()
unlistenOffline()
})
</script>
<style scoped lang="scss">
.instance-card {
display: flex;
flex-direction: column;
gap: 1rem;
width: 17rem;
}
Button {
width: 100%;
}
.button-group {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.side-cards {
position: absolute;
display: flex;
flex-direction: column;
padding: 1rem;
min-height: calc(100% - 3.25rem);
max-height: calc(100% - 3.25rem);
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
.card {
min-height: unset;
margin-bottom: 0;
}
}
.instance-nav {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 1rem;
gap: 0.5rem;
background: var(--color-raised-bg);
height: 100%;
}
.name {
font-size: 1.25rem;
color: var(--color-contrast);
overflow: hidden;
text-overflow: ellipsis;
}
.metadata {
text-transform: capitalize;
}
.instance-container {
display: flex;
flex-direction: row;
overflow: auto;
gap: 1rem;
min-height: 100%;
}
.content {
margin-left: 19rem;
}
.instance-info {
display: flex;
flex-direction: column;
width: 100%;
}
.badge {
display: flex;
align-items: center;
font-weight: bold;
width: fit-content;
color: var(--color-orange);
}
.pages-list {
display: flex;
flex-direction: column;
gap: var(--gap-xs);
.btn {
font-size: 100%;
font-weight: 400;
background: inherit;
transition: all ease-in-out 0.1s;
width: 100%;
color: var(--color-primary);
box-shadow: none;
&.router-link-exact-active {
box-shadow: var(--shadow-inset-lg);
background: var(--color-button-bg);
color: var(--color-contrast);
}
&:hover {
background-color: var(--color-button-bg);
color: var(--color-contrast);
box-shadow: var(--shadow-inset-lg);
text-decoration: none;
}
svg {
width: 1.3rem;
height: 1.3rem;
}
}
}
.instance-nav {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: left;
padding: 1rem;
gap: 0.5rem;
height: min-content;
width: 100%;
}
.instance-button {
width: fit-content;
}
.actions {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 0.5rem;
}
.content {
width: 100%;
display: flex;
flex-direction: column;
padding: 1rem 1rem 0 0;
overflow: auto;
}
.stats {
grid-area: stats;
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: var(--gap-md);
.stat {
display: flex;
flex-direction: row;
align-items: center;
width: fit-content;
gap: var(--gap-xs);
--stat-strong-size: 1.25rem;
strong {
font-size: var(--stat-strong-size);
}
p {
margin: 0;
}
svg {
height: var(--stat-strong-size);
width: var(--stat-strong-size);
}
}
.date {
margin-top: auto;
}
@media screen and (max-width: 750px) {
flex-direction: row;
column-gap: var(--gap-md);
margin-top: var(--gap-xs);
}
@media screen and (max-width: 600px) {
margin-top: 0;
.stat-label {
display: none;
}
}
}
</style>