Wire up homepage & library (#83)

* Top 10 packs & mods by follows plugged into home page. Modpacks installable.

* Only displays row if packs are present. Confirmation modal added. Displays play or X ctas.

* Fixes attr ordering.

* Rewires library page. Adds loader.

* Updates kill_by_pid to kill_by_uuid.

* Starts loading animation when installing on homepage.

* Changes RowDisplay key. Polish.

* Removes loader. Fixes InstallConfirmModal.

* Removes loader. Polishing.

* Z-index changes.

* Z-index changes.

* Fixes content going off screen.

* Styling changes.

* Filters out projects already installed on the home page.

* Wires up instance.vue, homepage, and appbar to process API.

* Cleans up process handling. App bar partially hooked up.

* Removes scoped in Settings to fix AnimatedLogo. Adds loader to Instance.

* Moves ctas outside of card.

* Adds mouse over to Stop btn.

* Removes unnecessary code. Fixes uuid reset.

* Wires up Instance.vue to process API.

* Removes appbar mod count. Updates code to use new linked_data and updated events.

* Switches load_listener to profile_listener. Unlistens on unmount.

* Cleans up instance card styling.

* Fixes margin with uncollapsed navbar. Ensures RowDisplay has data.

* Updates profile_listener. Increases stack size.

* Provides more margin when navbar is expanded.

* fix proper

* Re-adds calculated width and height. Fixes navbar.

* Increases stack size further. Navbar is not absolute. View width made into var.

* Ensures the specific isntance for a killed process is set to off.

* fix menu when not logged in

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
Zach Baird 2023-05-09 19:48:47 -04:00 committed by GitHub
parent da4fc1c835
commit ba20c482bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 371 additions and 111 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

@ -1,11 +1,10 @@
<script setup>
import { ref, onMounted } from 'vue'
import { onMounted } from 'vue'
import { RouterView, RouterLink } from 'vue-router'
import { HomeIcon, SearchIcon, LibraryIcon, PlusIcon, SettingsIcon, Button } from 'omorphia'
import { useTheming } from '@/store/state'
import AccountsCard from '@/components/ui/AccountsCard.vue'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { list } from '@/helpers/profile'
import { get } from '@/helpers/settings'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
@ -17,16 +16,6 @@ onMounted(async () => {
themeStore.setThemeState(settings)
themeStore.collapsedNavigation = collapsed_navigation
})
const installedMods = ref(0)
list().then(
(profiles) =>
(installedMods.value = Object.values(profiles).reduce(
(acc, val) => acc + Object.keys(val.projects).length,
0
))
)
// TODO: add event when profiles update to update installed mods count
</script>
<template>
@ -117,9 +106,13 @@ list().then(
</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 +126,10 @@ list().then(
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 { 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,41 +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"><PlayIcon /></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;
@ -72,68 +215,129 @@ 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 {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-brand);
border-radius: var(--radius-lg);
width: 3rem;
height: 3rem;
right: 1rem;
bottom: 3.5rem;
opacity: 0;
transition: 0.3s ease-in-out bottom, 0.1s ease-in-out opacity;
cursor: pointer;
svg {
color: var(--color-accent-contrast);
width: 1.5rem;
height: 1.5rem;
}
&:hover {
filter: brightness(0.75);
box-shadow: var(--shadow-floating);
}
}
img {
width: 100%;
border-radius: var(--radius-sm);
filter: none !important;
aspect-ratio: 1;
.mod-image {
border-radius: 1.5rem !important;
}
.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,24 +1,66 @@
<script setup>
import RowDisplay from '@/components/RowDisplay.vue'
import { shallowRef } from 'vue'
import { list } from '@/helpers/profile.js'
import { ref, shallowRef, onUnmounted } from 'vue'
import { ofetch } from 'ofetch'
import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js'
import { profile_listener } from '@/helpers/events'
import { useBreadcrumbs } from '@/store/breadcrumbs'
const featuredModpacks = ref({})
const featuredMods = ref({})
const filter = ref('')
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
const profiles = await list()
const recentInstances = shallowRef(Object.values(profiles))
breadcrumbs.setRootContext({ name: 'Home', link: route.path })
const recentInstances = shallowRef()
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>
<div 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" />
<RowDisplay label="Popular packs" :instances="featuredModpacks" :can-paginate="true" />
<RowDisplay label="Popular mods" :instances="featuredMods" :can-paginate="true" />
</div>
</template>

View File

@ -1,22 +1,29 @@
<script setup>
import GridDisplay from '@/components/GridDisplay.vue'
import { shallowRef } from 'vue'
import { list } from '@/helpers/profile'
import GridDisplay from '@/components/GridDisplay.vue'
import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
const profiles = await list()
const instances = shallowRef(Object.values(profiles))
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
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)
)
</script>
<template>
<div>
<GridDisplay label="Instances" :instances="instances" />
<GridDisplay label="Modpacks" :instances="instances" />
<GridDisplay label="Modpacks" :instances="modpacks" />
</div>
</template>
<style lang="scss" scoped></style>

View File

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