Merge branch 'master' into loading

This commit is contained in:
CodexAdrian
2023-05-10 00:16:07 -04:00
11 changed files with 471 additions and 156 deletions

View File

@@ -1,3 +1,3 @@
# Windows has stack overflows when calling from Tauri, so we increase compiler size
[target.'cfg(windows)']
rustflags = ["-C", "link-args=/STACK:8388608"]
rustflags = ["-C", "link-args=/STACK:16777220"]

View File

@@ -117,9 +117,13 @@ defineExpose({
</section>
</div>
<div class="router-view">
<Suspense>
<RouterView />
</Suspense>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense>
<component :is="Component"></component>
</Suspense>
</template>
</RouterView>
</div>
</div>
</div>
@@ -133,10 +137,10 @@ defineExpose({
overflow: hidden;
.view {
width: calc(100% - 5rem);
width: var(--view-width);
&.expanded {
width: calc(100% - 12rem);
width: var(--expanded-view-width);
}
.appbar {

View File

@@ -3,6 +3,8 @@
:root {
font-family: var(--font-standard);
color-scheme: dark;
--view-width: calc(100% - 5rem);
--expanded-view-width: calc(100% - 13rem);
}
* {

View File

@@ -23,23 +23,34 @@ const props = defineProps({
},
canPaginate: Boolean,
})
const allowPagination = ref(false)
const modsRow = ref(null)
const newsRow = ref(null)
// Remove after state is populated with real data
const shouldRenderNormalInstances = props.instances && props.instances?.length !== 0
const shouldRenderNews = props.news && props.news?.length !== 0
const handlePaginationDisplay = () => {
let parentsRow
if (shouldRenderNormalInstances) parentsRow = modsRow.value
if (shouldRenderNews) parentsRow = newsRow.value
if (!parentsRow) return
const children = parentsRow.children
const lastChild = children[children.length - 1]
const childBox = lastChild.getBoundingClientRect()
if (childBox.x + childBox.width > window.innerWidth) allowPagination.value = true
else allowPagination.value = false
// This is wrapped in a setTimeout because the HtmlCollection seems to struggle
// with getting populated sometimes. It's a flaky error, but providing a bit of
// wait-time for the below expressions has not failed thus-far.
setTimeout(() => {
const children = parentsRow.children
const lastChild = children[children.length - 1]
const childBox = lastChild?.getBoundingClientRect()
if (childBox?.x + childBox?.width > window.innerWidth && props.canPaginate)
allowPagination.value = true
else allowPagination.value = false
}, 300)
}
onMounted(() => {
if (props.canPaginate) window.addEventListener('resize', handlePaginationDisplay)
// Check if pagination should be rendered on mount
@@ -48,6 +59,7 @@ onMounted(() => {
onUnmounted(() => {
if (props.canPaginate) window.removeEventListener('resize', handlePaginationDisplay)
})
const handleLeftPage = () => {
if (shouldRenderNormalInstances) modsRow.value.scrollLeft -= 170
else if (shouldRenderNews) newsRow.value.scrollLeft -= 170
@@ -58,7 +70,7 @@ const handleRightPage = () => {
}
</script>
<template>
<div class="row">
<div v-if="props.instances.length > 0" class="row">
<div class="header">
<p>{{ props.label }}</p>
<hr aria-hidden="true" />
@@ -70,7 +82,7 @@ const handleRightPage = () => {
<section v-if="shouldRenderNormalInstances" ref="modsRow" class="instances">
<Instance
v-for="instance in props.instances"
:key="instance.id"
:key="instance?.project_id || instance?.id"
display="card"
:instance="instance"
class="row-instance"

View File

@@ -8,7 +8,7 @@
<Avatar :size="expanded ? 'xs' : 'sm'" :src="selectedAccount?.profile_picture ?? ''" />
<div v-show="expanded" class="avatar-text">
<div class="text no-select">
{{ selectedAccount.username }}
{{ selectedAccount ? selectedAccount.username : 'Offline' }}
</div>
<p class="no-select">
<UsersIcon />

View File

@@ -1,8 +1,19 @@
<script setup>
import { RouterLink } from 'vue-router'
import { AnimatedLogo, Avatar, Card } from 'omorphia'
import { shallowRef, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ofetch } from 'ofetch'
import { Card, SaveIcon, XIcon, Avatar, AnimatedLogo } from 'omorphia'
import { PlayIcon } from '@/assets/icons'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import { install as pack_install } from '@/helpers/pack'
import { run, list } from '@/helpers/profile'
import {
kill_by_uuid,
get_all_running_profile_paths,
get_uuids_by_profile_path,
} from '@/helpers/process'
import { process_listener } from '@/helpers/events'
const props = defineProps({
instance: {
@@ -16,44 +27,173 @@ const props = defineProps({
default: false,
},
})
const confirmModal = ref(null)
const playing = ref(false)
const uuid = ref(null)
const modLoading = ref(false)
const router = useRouter()
const seeInstance = async () => {
const instancePath = props.instance.metadata
? `/instance/${encodeURIComponent(props.instance.path)}`
: `/project/${encodeURIComponent(props.instance.project_id)}`
await router.push(instancePath)
}
const checkProcess = async () => {
const runningPaths = await get_all_running_profile_paths()
if (runningPaths.includes(props.instance.path)) {
playing.value = true
return
}
playing.value = false
uuid.value = null
}
const install = async (e) => {
e.stopPropagation()
modLoading.value = true
const [data, versions] = await Promise.all([
ofetch(
`https://api.modrinth.com/v2/project/${
props.instance.metadata
? props.instance.metadata?.linked_data?.project_id
: props.instance.project_id
}`
).then(shallowRef),
ofetch(
`https://api.modrinth.com/v2/project/${
props.instance.metadata
? props.instance.metadata?.linked_dadta?.project_id
: props.instance.project_id
}/version`
).then(shallowRef),
])
if (data.value.project_type === 'modpack') {
const packs = Object.values(await list())
if (
packs.length === 0 ||
!packs
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === data.value.id)
) {
await pack_install(versions.value[0].id)
} else confirmModal.value.show(versions.value[0].id)
}
modLoading.value = false
// TODO: Add condition for installing a mod
}
const play = async (e) => {
e.stopPropagation()
modLoading.value = true
uuid.value = await run(props.instance.path)
modLoading.value = false
playing.value = true
}
const stop = async (e) => {
e.stopPropagation()
playing.value = false
try {
// If we lost the uuid for some reason, such as a user navigating
// from-then-back to this page, we will get all uuids by the instance path.
// For-each uuid, kill the process.
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(props.instance.path)
uuid.value = uuids[0]
uuids.forEach(async (u) => await kill_by_uuid(u))
} else await kill_by_uuid(uuid.value) // If we still have the uuid, just kill it
} catch (err) {
// Theseus currently throws:
// "Error launching Minecraft: Minecraft exited with non-zero code 1" error
// For now, we will catch and just warn
console.warn(err)
}
uuid.value = null
}
await process_listener((e) => {
if (e.event === 'Finished' && e.uuid == uuid.value) playing.value = false
})
</script>
<template>
<div>
<RouterLink :to="`/instance/${encodeURIComponent(props.instance.path)}`">
<Card v-if="props.small" class="instance-small-card button-base">
<Avatar
:src="convertFileSrc(props.instance.metadata.icon)"
:alt="props.instance.metadata.name"
size="sm"
/>
<div class="instance-small-card__info">
<span class="title">{{ props.instance.metadata.name }}</span>
{{
props.instance.metadata.loader.charAt(0).toUpperCase() +
props.instance.metadata.loader.slice(1)
}}
{{ props.instance.metadata.game_version }}
</div>
</Card>
<Card v-else class="instance-card-item">
<img :src="convertFileSrc(props.instance.metadata.icon)" alt="Trending mod card" />
<div class="project-info">
<p class="title">{{ props.instance.metadata.name }}</p>
<p class="description">
{{ props.instance.metadata.loader }} {{ props.instance.metadata.game_version }}
</p>
</div>
<div class="cta" :class="{ loading: !instance.installed }">
<PlayIcon v-if="instance.installed" />
<AnimatedLogo v-else class="loading-icon" />
</div>
</Card>
</RouterLink>
<div class="instance">
<Card v-if="props.small" class="instance-small-card button-base">
<Avatar
:src="convertFileSrc(props.instance.metadata.icon)"
:alt="props.instance.metadata.name"
size="sm"
/>
<div class="instance-small-card__info">
<span class="title">{{ props.instance.metadata.name }}</span>
{{
props.instance.metadata.loader.charAt(0).toUpperCase() +
props.instance.metadata.loader.slice(1)
}}
{{ props.instance.metadata.game_version }}
</div>
</Card>
<Card
v-else
class="instance-card-item button-base"
@click="seeInstance"
@mouseenter="checkProcess"
>
<Avatar
size="lg"
:src="
props.instance.metadata
? convertFileSrc(props.instance.metadata?.icon)
: props.instance.icon_url
"
alt="Mod card"
class="mod-image"
/>
<div class="project-info">
<p class="title">{{ props.instance.metadata?.name || props.instance.title }}</p>
<p class="description">
{{ props.instance.metadata?.loader }}
{{ props.instance.metadata?.game_version || props.instance.latest_version }}
</p>
</div>
</Card>
<div
v-if="props.instance.metadata && playing === false && modLoading === false"
class="install cta button-base"
@click="play"
>
<PlayIcon />
</div>
<div v-else-if="modLoading === true && playing === false" class="cta loading">
<AnimatedLogo class="loading" />
</div>
<div
v-else-if="playing === true"
class="stop cta button-base"
@click="stop"
@mousehover="checkProcess"
>
<XIcon />
</div>
<div v-else class="install cta buttonbase" @click="install"><SaveIcon /></div>
<InstallConfirmModal ref="confirmModal" />
</div>
</template>
<style lang="scss" scoped>
<style lang="scss">
.instance-small-card {
background-color: var(--color-bg) !important;
padding: 1rem !important;
@@ -75,21 +215,106 @@ const props = defineProps({
}
}
.instance {
position: relative;
&:hover {
.cta {
opacity: 1;
bottom: 4.5rem;
}
.instance-card-item {
background: hsl(220, 11%, 11%) !important;
}
}
}
.light-mode {
.instance:hover {
.instance-card-item {
background: hsl(0, 0%, 91%) !important;
}
}
}
.install {
background: var(--color-brand);
display: flex;
}
.stop {
background: var(--color-red);
display: flex;
}
.cta.loading {
background: hsl(220, 11%, 10%) !important;
display: flex;
justify-content: center;
align-items: center;
.loading {
width: 2.5rem !important;
height: 2.5rem !important;
}
svg {
width: 2.5rem !important;
height: 2.5rem !important;
}
}
.light-mode {
.instance-card-item {
background: hsl(0, 0%, 100%) !important;
&:hover {
background: hsl(0, 0%, 91%) !important;
}
}
}
.cta {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
z-index: 41;
width: 3rem;
height: 3rem;
right: 1rem;
bottom: 3.5rem;
opacity: 0;
transition: 0.3s ease-in-out bottom, 0.1s ease-in-out opacity !important;
cursor: pointer;
svg {
color: var(--color-accent-contrast);
width: 1.5rem !important;
height: 1.5rem !important;
}
&:hover {
filter: none !important; /* overrides button-base class */
box-shadow: var(--shadow-floating);
}
}
.instance-card-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0.75rem;
transition: 0.1s ease-in-out all;
padding: 0.75rem !important; /* overrides card class */
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
background: hsl(220, 11%, 17%) !important;
&:hover {
filter: brightness(0.85);
.cta {
opacity: 1;
bottom: 4.5rem;
}
filter: brightness(1) !important;
background: hsl(220, 11%, 11%) !important;
}
.cta {
@@ -137,22 +362,24 @@ const props = defineProps({
border-radius: var(--radius-sm);
filter: none !important;
aspect-ratio: 1;
}
.project-info {
margin-top: 1rem;
width: 100%;
.title {
color: var(--color-contrast);
//max-width: 10rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
margin: 0;
font-weight: 600;
font-size: 1rem;
line-height: 110%;
display: inline-block;
}
.description {
color: var(--color-base);
display: -webkit-box;

View File

@@ -36,7 +36,7 @@ import { listen } from '@tauri-apps/api/event'
}
loader_uuid: unique identification of the loading bar
fraction: number, (as a fraction of 1, how much we'vel oaded so far). If null, by convention, loading is finished
fraction: number, (as a fraction of 1, how much we've loaded so far). If null, by convention, loading is finished
message: message to display to the user
}
*/

View File

@@ -1,34 +1,67 @@
<script setup>
import RowDisplay from '@/components/RowDisplay.vue'
import { onMounted, ref } from 'vue'
import { list } from '@/helpers/profile.js'
import {ref, onUnmounted, shallowRef} from 'vue'
import { ofetch } from 'ofetch'
import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import { profile_listener } from '@/helpers/events'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import SplashScreen from '@/components/ui/SplashScreen.vue'
const featuredModpacks = ref({})
const featuredMods = ref({})
const filter = ref('')
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
const loading = ref(true)
const recentInstances = ref(null)
onMounted(async () => {
recentInstances.value = Object.values(await list())
loading.value = false
})
const recentInstances = shallowRef(Object.values(await list()))
breadcrumbs.setRootContext({ name: 'Home', link: route.path })
const getInstances = async () => {
filter.value = ''
const profiles = await list()
recentInstances.value = Object.values(profiles)
const excludeIds = recentInstances.value.map((i) => i.metadata?.linked_data?.project_id)
excludeIds.forEach((id, index) => {
filter.value += `NOT"project_id"="${id}"`
if (index < excludeIds.length - 1) filter.value += ' AND '
})
}
const getFeaturedModpacks = async () => {
const response = await ofetch(
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`
)
featuredModpacks.value = response.hits
}
const getFeaturedMods = async () => {
const response = await ofetch(
`https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows&filters=${filter.value}`
)
featuredMods.value = response.hits
}
await getInstances()
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
const unlisten = await profile_listener(async (e) => {
if (e.event === 'edited') {
await getInstances()
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
}
})
onUnmounted(() => unlisten())
</script>
<template>
<transition name="fade">
<SplashScreen v-if="loading" />
<div v-else class="page-container">
<RowDisplay label="Jump back in" :instances="recentInstances" :can-paginate="false" />
<RowDisplay label="Popular packs" :instances="recentInstances" :can-paginate="true" />
<RowDisplay label="Test" :instances="recentInstances" :can-paginate="true" />
</div>
</transition>
<div class="page-container">
<RowDisplay label="Jump back in" :instances="recentInstances" :can-paginate="false" />
<RowDisplay label="Popular packs" :instances="featuredModpacks" :can-paginate="true" />
<RowDisplay label="Popular mods" :instances="featuredMods" :can-paginate="true" />
</div>
</template>
<style lang="scss" scoped>

View File

@@ -1,53 +1,43 @@
<script setup>
import { shallowRef } from 'vue'
import GridDisplay from '@/components/GridDisplay.vue'
import { onMounted, ref } from 'vue'
import { list } from '@/helpers/profile'
import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { loading_listener } from '@/helpers/events.js'
import { progress_bars_list } from '@/helpers/state.js'
import SplashScreen from '@/components/ui/SplashScreen.vue'
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
const loading = ref(true)
const instances = ref(null)
const loadingInstances = ref(null)
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
onMounted(async () => {
instances.value = Object.values(await list())
loadingInstances.value = Object.values(await progress_bars_list())
loading.value = false
})
const profiles = await list()
const instances = shallowRef(
Object.values(profiles).filter((prof) => !prof.metadata.linked_project_id)
)
const modpacks = shallowRef(
Object.values(profiles).filter((prof) => prof.metadata.linked_project_id)
)
loading_listener(async (profile) => {
console.log(profile)
instances.value = Object.values(await list())
loadingInstances.value = Object.values(await progress_bars_list())
if (profile.event === 'loaded') {
const profiles = await list()
instances.value = Object.values(profiles).filter(
(prof) => !prof.metadata.linked_project_id
)
modpacks.value = Object.values(profiles).filter(
(prof) => prof.metadata.linked_project_id
)
}
})
</script>
<template>
<transition name="fade">
<SplashScreen v-if="loading" />
<div v-else>
<GridDisplay label="Instances" :instances="instances" />
<GridDisplay label="Modpacks" :instances="instances" />
</div>
</transition>
<div>
<GridDisplay label="Instances" :instances="instances" />
<GridDisplay label="Modpacks" :instances="modpacks" />
</div>
</template>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<style lang="scss" scoped></style>

View File

@@ -360,7 +360,7 @@ const setJavaInstall = (javaInstall) => {
</transition>
</template>
<style lang="scss" scoped>
<style lang="scss">
.concurrent-downloads {
width: 80% !important;
}

View File

@@ -1,7 +1,5 @@
<template>
<transition name="fade">
<SplashScreen v-if="loading" />
<div v-else class="instance-container">
<div class="instance-container">
<div class="side-cards">
<Card class="instance-card">
<Avatar size="lg" :src="convertFileSrc(instance.metadata.icon)" />
@@ -12,20 +10,33 @@
</span>
</div>
<span class="button-group">
<Button
:color="instance.installed ? 'primary' : ''"
class="instance-button"
:disabled="!instance.installed"
@click="run($route.params.id)"
>
<PlayIcon v-if="instance.installed" />
<AnimatedLogo v-else class="loading-icon" />
{{ instance.installed ? 'Play' : 'Installing' }}
</Button>
<Button class="instance-button" icon-only>
<OpenFolderIcon />
</Button>
</span>
<Button
v-if="playing === true"
color="danger"
class="instance-button"
@click="stopInstance"
@mouseover="checkProcess"
>
<XIcon />
Stop
</Button>
<Button
v-else-if="playing === false && loading === false"
color="primary"
class="instance-button"
@click="startInstance"
@mouseover="checkProcess"
>
<PlayIcon />
Play
</Button>
<Button v-else-if="loading === true && playing === false" disabled class="instance-button"
>Loading...</Button
>
<Button class="instance-button" icon-only @click="open({ defaultPath: instance.path })">
<OpenFolderIcon />
</Button>
</span>
</Card>
<div class="pages-list">
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/`" class="btn">
@@ -45,47 +56,36 @@
<div class="content">
<Promotion />
<router-view :instance="instance" />
</div>
</div>
</transition>
</div>
</template>
<script setup>
import {
BoxIcon,
SettingsIcon,
FileIcon,
Button,
Avatar,
Card,
Promotion,
AnimatedLogo,
} from 'omorphia'
import { BoxIcon, SettingsIcon, FileIcon, XIcon, Button, Avatar, Card, Promotion } from 'omorphia'
import { PlayIcon, OpenFolderIcon } from '@/assets/icons'
import { get, run } from '@/helpers/profile'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
kill_by_uuid,
} from '@/helpers/process'
import {process_listener, profile_listener} from '@/helpers/events'
import { useRoute } from 'vue-router'
import { onMounted, ref } from 'vue'
import { ref, onUnmounted } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useSearch } from '@/store/search'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { profile_listener } from '@/helpers/events.js'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import { open } from '@tauri-apps/api/dialog'
import { useBreadcrumbs, useSearch } from '@/store/state'
const route = useRoute()
const searchStore = useSearch()
const breadcrumbs = useBreadcrumbs()
const instance = ref(null)
const loading = ref(true)
const instance = ref(await get(route.params.id))
onMounted(async () => {
instance.value = await get(route.params.id)
searchStore.instanceContext = instance.value
breadcrumbs.setName('Instance', instance.value.metadata.name)
breadcrumbs.setContext({
name: instance.value.metadata.name,
link: route.path,
})
loading.value = false
searchStore.instanceContext = instance.value
breadcrumbs.setName('Instance', instance.value.metadata.name)
breadcrumbs.setContext({
name: instance.value.metadata.name,
link: route.path,
})
profile_listener(async (event) => {
@@ -93,6 +93,53 @@ profile_listener(async (event) => {
instance.value = await get(route.params.id)
}
})
const uuid = ref(null)
const playing = ref(false)
const loading = ref(false)
const startInstance = async () => {
loading.value = true
uuid.value = await run(route.params.id)
loading.value = false
playing.value = true
}
const checkProcess = async () => {
const runningPaths = await get_all_running_profile_paths()
if (runningPaths.includes(instance.value.path)) {
playing.value = true
return
}
playing.value = false
uuid.value = null
}
await checkProcess()
const stopInstance = async () => {
playing.value = false
try {
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(instance.value.path)
uuid.value = uuids[0] // populate Uuid to listen for in the process_listener
uuids.forEach(async (u) => await kill_by_uuid(u))
} else await kill_by_uuid(uuid.value)
} catch (err) {
// Theseus currently throws:
// "Error launching Minecraft: Minecraft exited with non-zero code 1" error
// For now, we will catch and just warn
console.warn(err)
}
}
const unlisten = await process_listener((e) => {
if (e.event === 'Finished' && uuid.value === e.uuid) playing.value = false
})
onUnmounted(() => unlisten())
</script>
<style scoped lang="scss">