You've already forked AstralRinth
forked from didirus/AstralRinth
UI/UX improvements (#146)
* Some initial changes * New project cards * Version * Finalize improvements * Fixed styling issues on versions page * Removed light mode * Run linter * Added mixpanel stuff in context menus * Fix styling issues * Fix windows * homepage fixes * Finishing touches on mac styling * Fixed windows related styling * Update global.scss --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
<script setup>
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ClipboardCopyIcon,
|
||||
FolderOpenIcon,
|
||||
PlayIcon,
|
||||
@@ -12,14 +10,30 @@ import {
|
||||
StopCircleIcon,
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
ChevronRightIcon,
|
||||
ModalConfirm,
|
||||
} from 'omorphia'
|
||||
import Instance from '@/components/ui/Instance.vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { remove } from '@/helpers/profile.js'
|
||||
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
||||
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
|
||||
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
|
||||
import {
|
||||
get_all_running_profile_paths,
|
||||
get_uuids_by_profile_path,
|
||||
kill_by_uuid,
|
||||
} from '@/helpers/process.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { remove, run } from '@/helpers/profile.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showInFolder } from '@/helpers/utils.js'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { install as pack_install } from '@/helpers/pack.js'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
import mixpanel from 'mixpanel-browser'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
instances: {
|
||||
@@ -35,53 +49,28 @@ const props = defineProps({
|
||||
canPaginate: Boolean,
|
||||
})
|
||||
|
||||
const allowPagination = ref(Array.apply(false, Array(props.instances.length)))
|
||||
const actualInstances = computed(() =>
|
||||
props.instances.filter((x) => x && x.instances && x.instances[0])
|
||||
)
|
||||
|
||||
const modsRow = ref(null)
|
||||
const instanceOptions = ref(null)
|
||||
const instanceComponents = ref(null)
|
||||
const rows = ref(null)
|
||||
const confirmModal = ref(null)
|
||||
const deleteConfirmModal = ref(null)
|
||||
const modInstallModal = ref(null)
|
||||
|
||||
const themeStore = useTheming()
|
||||
const currentDeleteInstance = ref(null)
|
||||
const confirmModal = ref(null)
|
||||
|
||||
async function deleteProfile() {
|
||||
if (currentDeleteInstance.value) {
|
||||
instanceComponents.value = instanceComponents.value.filter(
|
||||
(x) => x.instance.path !== currentDeleteInstance.value
|
||||
)
|
||||
await remove(currentDeleteInstance.value).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaginationDisplay = () => {
|
||||
for (let i = 0; i < props.instances.length; i++) {
|
||||
let parentsRow = modsRow.value[i]
|
||||
|
||||
// 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[i] = true
|
||||
else allowPagination.value[i] = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.canPaginate) window.addEventListener('resize', handlePaginationDisplay)
|
||||
handlePaginationDisplay()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (props.canPaginate) window.removeEventListener('resize', handlePaginationDisplay)
|
||||
})
|
||||
|
||||
const handleInstanceRightClick = (event, passedInstance) => {
|
||||
const handleInstanceRightClick = async (event, passedInstance) => {
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
{ type: 'divider' },
|
||||
@@ -95,21 +84,9 @@ const handleInstanceRightClick = (event, passedInstance) => {
|
||||
},
|
||||
]
|
||||
|
||||
const options = !passedInstance.instance.path
|
||||
? [
|
||||
{
|
||||
name: 'install',
|
||||
color: 'primary',
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
]
|
||||
: passedInstance.playing
|
||||
const running = await get_all_running_profile_paths().catch(handleError)
|
||||
|
||||
const options = running.includes(passedInstance.path)
|
||||
? [
|
||||
{
|
||||
name: 'stop',
|
||||
@@ -128,63 +105,122 @@ const handleInstanceRightClick = (event, passedInstance) => {
|
||||
instanceOptions.value.showMenu(event, passedInstance, options)
|
||||
}
|
||||
|
||||
const handleProjectClick = (event, passedInstance) => {
|
||||
instanceOptions.value.showMenu(event, passedInstance, [
|
||||
{
|
||||
name: 'install',
|
||||
color: 'primary',
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const handleOptionsClick = async (args) => {
|
||||
switch (args.option) {
|
||||
case 'play':
|
||||
await args.item.play(null, 'InstanceRowContextMenu')
|
||||
await run(args.item.path).catch(handleError)
|
||||
mixpanel.track('InstanceStart', {
|
||||
loader: args.item.metadata.loader,
|
||||
game_version: args.item.metadata.game_version,
|
||||
})
|
||||
break
|
||||
case 'stop':
|
||||
await args.item.stop(null, 'InstanceRowContextMenu')
|
||||
for (const u of await get_uuids_by_profile_path(args.item.path).catch(handleError)) {
|
||||
await kill_by_uuid(u).catch(handleError)
|
||||
}
|
||||
mixpanel.track('InstanceStop', {
|
||||
loader: args.item.metadata.loader,
|
||||
game_version: args.item.metadata.game_version,
|
||||
})
|
||||
break
|
||||
case 'add_content':
|
||||
await args.item.addContent()
|
||||
await router.push({
|
||||
path: `/browse/${args.item.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||
query: { i: args.item.path },
|
||||
})
|
||||
break
|
||||
case 'edit':
|
||||
await args.item.seeInstance()
|
||||
await router.push({
|
||||
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
||||
})
|
||||
break
|
||||
case 'delete':
|
||||
currentDeleteInstance.value = args.item.instance.path
|
||||
confirmModal.value.show()
|
||||
currentDeleteInstance.value = args.item.path
|
||||
deleteConfirmModal.value.show()
|
||||
break
|
||||
case 'open_folder':
|
||||
await args.item.openFolder()
|
||||
await showInFolder(args.item.path)
|
||||
break
|
||||
case 'copy_path':
|
||||
await navigator.clipboard.writeText(args.item.instance.path)
|
||||
await navigator.clipboard.writeText(args.item.path)
|
||||
break
|
||||
case 'install':
|
||||
args.item.install()
|
||||
case 'install': {
|
||||
const versions = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${args.item.project_id}/version`,
|
||||
'project versions'
|
||||
)
|
||||
|
||||
if (args.item.project_type === 'modpack') {
|
||||
await pack_install(
|
||||
args.item.project_id,
|
||||
versions[0].id,
|
||||
args.item.title,
|
||||
args.item.icon_url
|
||||
)
|
||||
} else {
|
||||
modInstallModal.value.show(args.item.project_id, versions)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'open_link':
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: `https://modrinth.com/${args.item.instance.project_type}/${args.item.instance.slug}`,
|
||||
path: `https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'copy_link':
|
||||
await navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.instance.project_type}/${args.item.instance.slug}`
|
||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const getInstanceIndex = (rowIndex, index) => {
|
||||
let instanceIndex = 0
|
||||
for (let i = 0; i < rowIndex; i++) {
|
||||
instanceIndex += props.instances[i].instances.length
|
||||
}
|
||||
instanceIndex += index
|
||||
return instanceIndex
|
||||
const maxInstancesPerRow = ref(0)
|
||||
const maxProjectsPerRow = ref(0)
|
||||
|
||||
const calculateCardsPerRow = () => {
|
||||
// Calculate how many cards fit in one row
|
||||
const containerWidth = rows.value[0].clientWidth
|
||||
// Convert container width from pixels to rem
|
||||
const containerWidthInRem =
|
||||
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 1) / 11)
|
||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 1) / 17)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
calculateCardsPerRow()
|
||||
window.addEventListener('resize', calculateCardsPerRow)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', calculateCardsPerRow)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalConfirm
|
||||
ref="confirmModal"
|
||||
ref="deleteConfirmModal"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||
:has-to-type="false"
|
||||
@@ -193,28 +229,29 @@ const getInstanceIndex = (rowIndex, index) => {
|
||||
@proceed="deleteProfile"
|
||||
/>
|
||||
<div class="content">
|
||||
<div v-for="(row, rowIndex) in instances" :key="row.label" class="row">
|
||||
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
||||
<div class="header">
|
||||
<p>{{ row.label }}</p>
|
||||
<hr aria-hidden="true" />
|
||||
<div v-if="allowPagination[rowIndex]" class="pagination">
|
||||
<ChevronLeftIcon role="button" @click="modsRow[rowIndex].scrollLeft -= 170" />
|
||||
<ChevronRightIcon role="button" @click="modsRow[rowIndex].scrollLeft += 170" />
|
||||
</div>
|
||||
<router-link :to="row.route">{{ row.label }}</router-link>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
<section ref="modsRow" class="instances">
|
||||
<section v-if="row.instances[0].metadata" ref="modsRow" class="instances">
|
||||
<Instance
|
||||
v-for="(instance, instanceIndex) in row.instances"
|
||||
ref="instanceComponents"
|
||||
v-for="instance in row.instances.slice(0, maxInstancesPerRow)"
|
||||
:key="instance?.project_id || instance?.id"
|
||||
:instance="instance"
|
||||
@contextmenu.prevent.stop="
|
||||
(event) =>
|
||||
handleInstanceRightClick(
|
||||
event,
|
||||
instanceComponents[getInstanceIndex(rowIndex, instanceIndex)]
|
||||
)
|
||||
"
|
||||
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
||||
/>
|
||||
</section>
|
||||
<section v-else ref="modsRow" class="projects">
|
||||
<ProjectCard
|
||||
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
||||
:key="project?.project_id"
|
||||
ref="instanceComponents"
|
||||
class="item"
|
||||
:project="project"
|
||||
:confirm-modal="confirmModal"
|
||||
:mod-install-modal="modInstallModal"
|
||||
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -231,6 +268,8 @@ const getInstanceIndex = (rowIndex, index) => {
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
<InstallConfirmModal ref="confirmModal" />
|
||||
<InstanceInstallModal ref="modInstallModal" />
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
@@ -239,97 +278,70 @@ const getInstanceIndex = (rowIndex, index) => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
min-width: 100%;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: inherit;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1rem;
|
||||
gap: var(--gap-xs);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
a {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: bolder;
|
||||
white-space: nowrap;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: var(--color-gray);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
svg {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: inherit;
|
||||
align-items: inherit;
|
||||
|
||||
svg {
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 1.3rem;
|
||||
height: 1.2rem;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
transition: all ease-in-out 0.1s;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
align-items: inherit;
|
||||
transition: all ease-in-out 0.4s;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.instances {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
margin-right: auto;
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
|
||||
:deep(.instance-card-item) {
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.instance) {
|
||||
min-width: 10.5rem;
|
||||
max-width: 10.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
.row {
|
||||
background-color: rgb(30, 31, 34);
|
||||
.projects {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user