Context menus (#133)
* Context Menus in home and library * Menu impl * FInalize context menus * Update App.vue * Update App.vue * fix scrolling
This commit is contained in:
parent
e0e9c3f166
commit
e836738887
@ -54,7 +54,8 @@ watch(notificationsWrapper, () => {
|
||||
notifications.setNotifs(notificationsWrapper.value)
|
||||
})
|
||||
|
||||
// Link handler
|
||||
document.addEventListener('contextmenu', (event) => event.preventDefault())
|
||||
|
||||
document.querySelector('body').addEventListener('click', function (e) {
|
||||
let target = e.target
|
||||
while (target != null) {
|
||||
|
||||
@ -1,7 +1,20 @@
|
||||
<script setup>
|
||||
import Instance from '@/components/ui/Instance.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { SearchIcon, DropdownSelect, Card, formatCategoryHeader } from 'omorphia'
|
||||
import {
|
||||
ClipboardCopyIcon,
|
||||
FolderOpenIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
StopCircleIcon,
|
||||
EyeIcon,
|
||||
Card,
|
||||
DropdownSelect,
|
||||
SearchIcon,
|
||||
formatCategoryHeader,
|
||||
} from 'omorphia'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps({
|
||||
@ -16,6 +29,70 @@ const props = defineProps({
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const instanceOptions = ref(null)
|
||||
const instanceComponents = ref(null)
|
||||
|
||||
const handleRightClick = (event, item) => {
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'edit' },
|
||||
{ name: 'open' },
|
||||
{ name: 'copy' },
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'delete',
|
||||
color: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
instanceOptions.value.showMenu(
|
||||
event,
|
||||
item,
|
||||
item.playing
|
||||
? [
|
||||
{
|
||||
name: 'stop',
|
||||
color: 'danger',
|
||||
},
|
||||
...baseOptions,
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'play',
|
||||
color: 'primary',
|
||||
},
|
||||
...baseOptions,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const handleOptionsClick = async (args) => {
|
||||
console.log(args)
|
||||
switch (args.option) {
|
||||
case 'play':
|
||||
args.item.play()
|
||||
break
|
||||
case 'stop':
|
||||
args.item.stop()
|
||||
break
|
||||
case 'add_content':
|
||||
await args.item.addContent()
|
||||
break
|
||||
case 'edit':
|
||||
await args.item.seeInstance()
|
||||
break
|
||||
case 'delete':
|
||||
await args.item.deleteInstance()
|
||||
break
|
||||
case 'open':
|
||||
await args.item.openFolder()
|
||||
break
|
||||
case 'copy':
|
||||
await navigator.clipboard.writeText(args.item.instance.path)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
const group = ref('Category')
|
||||
@ -142,7 +219,10 @@ const filteredResults = computed(() => {
|
||||
</div>
|
||||
</Card>
|
||||
<div
|
||||
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({ key, value }))"
|
||||
v-for="(instanceSection, index) in Array.from(filteredResults, ([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}))"
|
||||
:key="instanceSection.key"
|
||||
class="row"
|
||||
>
|
||||
@ -153,12 +233,22 @@ const filteredResults = computed(() => {
|
||||
<section class="instances">
|
||||
<Instance
|
||||
v-for="instance in instanceSection.value"
|
||||
ref="instanceComponents"
|
||||
:key="instance.id"
|
||||
display="card"
|
||||
:instance="instance"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instanceComponents[index])"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
<template #stop> <StopCircleIcon /> Stop </template>
|
||||
<template #add_content> <PlusIcon /> Add content </template>
|
||||
<template #edit> <EyeIcon /> View instance </template>
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
<template #open> <FolderOpenIcon /> Open folder </template>
|
||||
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
|
||||
@ -1,7 +1,21 @@
|
||||
<script setup>
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'omorphia'
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ClipboardCopyIcon,
|
||||
FolderOpenIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
DownloadIcon,
|
||||
GlobeIcon,
|
||||
StopCircleIcon,
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
} from 'omorphia'
|
||||
import Instance from '@/components/ui/Instance.vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
|
||||
const props = defineProps({
|
||||
instances: {
|
||||
@ -17,11 +31,14 @@ const props = defineProps({
|
||||
canPaginate: Boolean,
|
||||
})
|
||||
|
||||
const allowPagination = ref(false)
|
||||
const allowPagination = ref(Array.apply(false, Array(props.instances.length)))
|
||||
const modsRow = ref(null)
|
||||
const instanceOptions = ref(null)
|
||||
const instanceComponents = ref(null)
|
||||
|
||||
const handlePaginationDisplay = () => {
|
||||
let parentsRow = modsRow.value
|
||||
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
|
||||
@ -32,43 +49,171 @@ const handlePaginationDisplay = () => {
|
||||
const childBox = lastChild?.getBoundingClientRect()
|
||||
|
||||
if (childBox?.x + childBox?.width > window.innerWidth && props.canPaginate)
|
||||
allowPagination.value = true
|
||||
else allowPagination.value = false
|
||||
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 baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'edit' },
|
||||
{ name: 'open_folder' },
|
||||
{ name: 'copy_path' },
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'delete',
|
||||
color: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const options = !passedInstance.instance.path
|
||||
? [
|
||||
{
|
||||
name: 'install',
|
||||
color: 'primary',
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
]
|
||||
: passedInstance.playing
|
||||
? [
|
||||
{
|
||||
name: 'stop',
|
||||
color: 'danger',
|
||||
},
|
||||
...baseOptions,
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'play',
|
||||
color: 'primary',
|
||||
},
|
||||
...baseOptions,
|
||||
]
|
||||
|
||||
instanceOptions.value.showMenu(event, passedInstance, options)
|
||||
}
|
||||
|
||||
const handleOptionsClick = async (args) => {
|
||||
switch (args.option) {
|
||||
case 'play':
|
||||
await args.item.play()
|
||||
break
|
||||
case 'stop':
|
||||
await args.item.stop()
|
||||
break
|
||||
case 'add_content':
|
||||
await args.item.addContent()
|
||||
break
|
||||
case 'edit':
|
||||
await args.item.seeInstance()
|
||||
break
|
||||
case 'delete':
|
||||
await args.item.deleteInstance()
|
||||
break
|
||||
case 'open_folder':
|
||||
await args.item.openFolder()
|
||||
break
|
||||
case 'copy_path':
|
||||
await navigator.clipboard.writeText(args.item.instance.path)
|
||||
break
|
||||
case 'install':
|
||||
args.item.install()
|
||||
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}`,
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'copy_link':
|
||||
await navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.instance.project_type}/${args.item.instance.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
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.instances.length > 0" class="row">
|
||||
<div class="content">
|
||||
<div v-for="(row, rowIndex) in instances" :key="row.label" class="row">
|
||||
<div class="header">
|
||||
<p>{{ props.label }}</p>
|
||||
<p>{{ row.label }}</p>
|
||||
<hr aria-hidden="true" />
|
||||
<div v-if="allowPagination" class="pagination">
|
||||
<ChevronLeftIcon role="button" @click="modsRow.value.scrollLeft -= 170" />
|
||||
<ChevronRightIcon role="button" @click="modsRow.value.scrollLeft += 170" />
|
||||
<div v-if="allowPagination[rowIndex]" class="pagination">
|
||||
<ChevronLeftIcon role="button" @click="modsRow[rowIndex].scrollLeft -= 170" />
|
||||
<ChevronRightIcon role="button" @click="modsRow[rowIndex].scrollLeft += 170" />
|
||||
</div>
|
||||
</div>
|
||||
<section ref="modsRow" class="instances">
|
||||
<Instance
|
||||
v-for="instance in props.instances"
|
||||
v-for="(instance, instanceIndex) in row.instances"
|
||||
ref="instanceComponents"
|
||||
:key="instance?.project_id || instance?.id"
|
||||
display="card"
|
||||
:instance="instance"
|
||||
class="row-instance"
|
||||
@contextmenu.prevent.stop="
|
||||
(event) =>
|
||||
handleInstanceRightClick(
|
||||
event,
|
||||
instanceComponents[getInstanceIndex(rowIndex, instanceIndex)]
|
||||
)
|
||||
"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
<template #stop> <StopCircleIcon /> Stop </template>
|
||||
<template #add_content> <PlusIcon /> Add content </template>
|
||||
<template #edit> <EyeIcon /> View instance </template>
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -147,6 +292,11 @@ onUnmounted(() => {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.instance) {
|
||||
min-width: 10.5rem;
|
||||
max-width: 10.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,9 +305,4 @@ onUnmounted(() => {
|
||||
background-color: rgb(30, 31, 34);
|
||||
}
|
||||
}
|
||||
|
||||
.row-instance {
|
||||
min-width: 10.5rem;
|
||||
max-width: 10.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
161
theseus_gui/src/components/ui/ContextMenu.vue
Normal file
161
theseus_gui/src/components/ui/ContextMenu.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="shown"
|
||||
ref="contextMenu"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
left: left,
|
||||
top: top,
|
||||
}"
|
||||
>
|
||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||
<hr v-if="option.type === 'divider'" class="divider" />
|
||||
<div v-else class="item clickable" :class="[option.color ?? 'base']">
|
||||
<slot :name="option.name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
||||
|
||||
const item = ref(null)
|
||||
const contextMenu = ref(null)
|
||||
const options = ref([])
|
||||
const left = ref('0px')
|
||||
const top = ref('0px')
|
||||
const shown = ref(false)
|
||||
|
||||
defineExpose({
|
||||
showMenu: (event, passedItem, passedOptions) => {
|
||||
item.value = passedItem
|
||||
options.value = passedOptions
|
||||
|
||||
const menuWidth = contextMenu.value.clientWidth
|
||||
const menuHeight = contextMenu.value.clientHeight
|
||||
|
||||
if (menuWidth + event.pageX >= window.innerWidth) {
|
||||
left.value = event.pageX - menuWidth + 2 + 'px'
|
||||
} else {
|
||||
left.value = event.pageX - 2 + 'px'
|
||||
}
|
||||
|
||||
if (menuHeight + event.pageY >= window.innerHeight) {
|
||||
top.value = event.pageY - menuHeight + 2 + 'px'
|
||||
} else {
|
||||
top.value = event.pageY - 2 + 'px'
|
||||
}
|
||||
|
||||
shown.value = true
|
||||
},
|
||||
})
|
||||
|
||||
const hideContextMenu = () => {
|
||||
shown.value = false
|
||||
emit('menu-closed')
|
||||
}
|
||||
|
||||
const optionClicked = (option) => {
|
||||
console.log('item check', item.value)
|
||||
emit('option-clicked', {
|
||||
item: item.value,
|
||||
option: option,
|
||||
})
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
const onEscKeyRelease = (event) => {
|
||||
if (event.keyCode === 27) {
|
||||
hideContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||
if (
|
||||
contextMenu.value &&
|
||||
contextMenu.value.$el !== event.target &&
|
||||
!elements.includes(contextMenu.value.$el)
|
||||
) {
|
||||
hideContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', handleClickOutside)
|
||||
document.body.addEventListener('keyup', onEscKeyRelease)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', handleClickOutside)
|
||||
document.removeEventListener('keyup', onEscKeyRelease)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.context-menu {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-floating);
|
||||
border: 1px solid var(--color-button-bg);
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
z-index: 1000000;
|
||||
overflow: hidden;
|
||||
padding: var(--gap-sm);
|
||||
|
||||
.item {
|
||||
align-items: center;
|
||||
color: var(--color-base);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
padding: var(--gap-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
&.base {
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--color-brand);
|
||||
color: var(--color-accent-contrast);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: var(--color-red);
|
||||
color: var(--color-accent-contrast);
|
||||
}
|
||||
|
||||
&.contrast {
|
||||
background-color: var(--color-orange);
|
||||
color: var(--color-accent-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: 1px solid var(--color-button-bg);
|
||||
margin: var(--gap-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,21 +1,24 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref, useSlots, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Card, DownloadIcon, XIcon, Avatar, AnimatedLogo, PlayIcon } from 'omorphia'
|
||||
import { Card, DownloadIcon, StopCircleIcon, Avatar, AnimatedLogo, PlayIcon } from 'omorphia'
|
||||
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 { get, list, remove, run } from '@/helpers/profile'
|
||||
import {
|
||||
kill_by_uuid,
|
||||
get_all_running_profile_paths,
|
||||
get_uuids_by_profile_path,
|
||||
kill_by_uuid,
|
||||
} from '@/helpers/process'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
|
||||
import { handleError, useSearch } from '@/store/state.js'
|
||||
import { showInFolder } from '@/helpers/utils.js'
|
||||
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
|
||||
|
||||
const searchStore = useSearch()
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: Object,
|
||||
@ -69,7 +72,7 @@ const checkProcess = async () => {
|
||||
}
|
||||
|
||||
const install = async (e) => {
|
||||
e.stopPropagation()
|
||||
e?.stopPropagation()
|
||||
modLoading.value = true
|
||||
const versions = await useFetch(
|
||||
`https://api.modrinth.com/v2/project/${props.instance.project_id}/version`,
|
||||
@ -108,7 +111,7 @@ const install = async (e) => {
|
||||
}
|
||||
|
||||
const play = async (e) => {
|
||||
e.stopPropagation()
|
||||
e?.stopPropagation()
|
||||
modLoading.value = true
|
||||
uuid.value = await run(props.instance.path).catch(handleError)
|
||||
modLoading.value = false
|
||||
@ -116,7 +119,7 @@ const play = async (e) => {
|
||||
}
|
||||
|
||||
const stop = async (e) => {
|
||||
e.stopPropagation()
|
||||
e?.stopPropagation()
|
||||
playing.value = false
|
||||
|
||||
// If we lost the uuid for some reason, such as a user navigating
|
||||
@ -131,6 +134,31 @@ const stop = async (e) => {
|
||||
uuid.value = null
|
||||
}
|
||||
|
||||
const deleteInstance = async () => {
|
||||
await remove(props.instance.path).catch(handleError)
|
||||
}
|
||||
|
||||
const openFolder = async () => {
|
||||
await showInFolder(props.instance.path)
|
||||
}
|
||||
|
||||
const addContent = async () => {
|
||||
searchStore.instanceContext = await get(props.instance.path).catch(handleError)
|
||||
await router.push({ path: '/browse/mod' })
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
install,
|
||||
playing,
|
||||
play,
|
||||
stop,
|
||||
seeInstance,
|
||||
openFolder,
|
||||
deleteInstance,
|
||||
addContent,
|
||||
instance: props.instance,
|
||||
})
|
||||
|
||||
const unlisten = await process_listener((e) => {
|
||||
if (e.event === 'finished' && e.uuid === uuid.value) playing.value = false
|
||||
})
|
||||
@ -176,7 +204,7 @@ onUnmounted(() => unlisten())
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<Avatar
|
||||
size="none"
|
||||
size="sm"
|
||||
:src="
|
||||
props.instance.metadata
|
||||
? !props.instance.metadata.icon ||
|
||||
@ -213,7 +241,7 @@ onUnmounted(() => unlisten())
|
||||
@click="stop"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<XIcon />
|
||||
<StopCircleIcon />
|
||||
</div>
|
||||
<div v-else class="install cta button-base" @click="install"><DownloadIcon /></div>
|
||||
</template>
|
||||
|
||||
@ -66,9 +66,26 @@ onUnmounted(() => unlisten())
|
||||
|
||||
<template>
|
||||
<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" />
|
||||
<RowDisplay
|
||||
:instances="[
|
||||
{
|
||||
label: 'Jump back in',
|
||||
instances: recentInstances,
|
||||
downloaded: true,
|
||||
},
|
||||
{
|
||||
label: 'Popular packs',
|
||||
instances: featuredModpacks,
|
||||
downloaded: false,
|
||||
},
|
||||
{
|
||||
label: 'Popular mods',
|
||||
instances: featuredMods,
|
||||
downloaded: false,
|
||||
},
|
||||
]"
|
||||
:can-paginate="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="instance-container">
|
||||
<div class="side-cards">
|
||||
<Card class="instance-card">
|
||||
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar
|
||||
size="lg"
|
||||
:src="
|
||||
@ -28,7 +28,7 @@
|
||||
@click="stopInstance"
|
||||
@mouseover="checkProcess"
|
||||
>
|
||||
<XIcon />
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
</Button>
|
||||
<Button
|
||||
@ -78,40 +78,60 @@
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
<component :is="Component" :instance="instance"></component>
|
||||
<component :is="Component" :instance="instance" :options="options"></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 #repair> <HammerIcon /> Repair </template>
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
BoxIcon,
|
||||
SettingsIcon,
|
||||
FileIcon,
|
||||
XIcon,
|
||||
Button,
|
||||
Avatar,
|
||||
Card,
|
||||
Promotion,
|
||||
PlayIcon,
|
||||
StopCircleIcon,
|
||||
EditIcon,
|
||||
HammerIcon,
|
||||
TrashIcon,
|
||||
FolderOpenIcon,
|
||||
ClipboardCopyIcon,
|
||||
PlusIcon,
|
||||
ExternalIcon,
|
||||
} from 'omorphia'
|
||||
import { get, run } from '@/helpers/profile'
|
||||
import { get, install, remove, 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 { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { handleError, useBreadcrumbs, useLoading, useSearch } from '@/store/state'
|
||||
import { showInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const searchStore = useSearch()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
@ -129,6 +149,7 @@ const loadingBar = useLoading()
|
||||
const uuid = ref(null)
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
const options = ref(null)
|
||||
|
||||
const startInstance = async () => {
|
||||
loading.value = true
|
||||
@ -170,6 +191,91 @@ const unlistenProcesses = await process_listener((e) => {
|
||||
if (e.event === 'finished' && uuid.value === e.uuid) playing.value = false
|
||||
})
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'edit' },
|
||||
{ name: 'open_folder' },
|
||||
{ name: 'copy_path' },
|
||||
{ type: 'divider' },
|
||||
{
|
||||
name: 'repair',
|
||||
color: 'contrast',
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
color: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
options.value.showMenu(
|
||||
event,
|
||||
instance.value,
|
||||
playing.value
|
||||
? [
|
||||
{
|
||||
name: 'stop',
|
||||
color: 'danger',
|
||||
},
|
||||
...baseOptions,
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'play',
|
||||
color: 'primary',
|
||||
},
|
||||
...baseOptions,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const handleOptionsClick = async (args) => {
|
||||
console.log(args)
|
||||
switch (args.option) {
|
||||
case 'play':
|
||||
await startInstance()
|
||||
break
|
||||
case 'stop':
|
||||
await stopInstance()
|
||||
break
|
||||
case 'add_content':
|
||||
await router.push({
|
||||
path: `/browse/${instance.value.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||
})
|
||||
break
|
||||
case 'edit':
|
||||
await router.push({
|
||||
path: `/instance/${encodeURIComponent(route.params.id)}/options`,
|
||||
})
|
||||
break
|
||||
case 'repair':
|
||||
await install(instance.value.path).catch(handleError)
|
||||
break
|
||||
case 'delete':
|
||||
await remove(instance.value.path).catch(handleError)
|
||||
break
|
||||
case 'open_folder':
|
||||
await showInFolder(instance.value.path)
|
||||
break
|
||||
case 'copy_path':
|
||||
await navigator.clipboard.writeText(instance.value.path)
|
||||
break
|
||||
case 'open_link':
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: args.item.link,
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'copy_link':
|
||||
await navigator.clipboard.writeText(args.item.link)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenProfiles()
|
||||
|
||||
@ -60,7 +60,12 @@
|
||||
<div class="table-cell table-text">Author</div>
|
||||
<div class="table-cell table-text">Actions</div>
|
||||
</div>
|
||||
<div v-for="mod in search" :key="mod.file_name" class="table-row">
|
||||
<div
|
||||
v-for="mod in search"
|
||||
:key="mod.file_name"
|
||||
class="table-row"
|
||||
@contextmenu.prevent.stop="(c) => handleRightClick(c, mod)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator"></AnimatedLogo>
|
||||
<Button
|
||||
@ -138,6 +143,12 @@ const props = defineProps({
|
||||
return {}
|
||||
},
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const projects = ref([])
|
||||
@ -302,6 +313,18 @@ async function removeMod(mod) {
|
||||
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
||||
}
|
||||
|
||||
const handleRightClick = (event, mod) => {
|
||||
if (mod.slug && mod.project_type) {
|
||||
props.options.showMenu(
|
||||
event,
|
||||
{
|
||||
link: `https://modrinth.com/${mod.project_type}/${mod.slug}`,
|
||||
},
|
||||
[{ name: 'open_link' }, { name: 'copy_link' }]
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="root-container">
|
||||
<div v-if="data" class="project-sidebar">
|
||||
<Instance v-if="instance" :instance="instance" small />
|
||||
<Card class="sidebar-card">
|
||||
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="lg" :src="data.icon_url" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ data.title }}</h2>
|
||||
@ -197,6 +197,11 @@
|
||||
<InstallConfirmModal ref="confirmModal" />
|
||||
<InstanceInstallModal ref="modInstallModal" />
|
||||
<IncompatibilityWarningModal ref="incompatibilityWarning" />
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -220,6 +225,8 @@ import {
|
||||
formatNumber,
|
||||
ExternalIcon,
|
||||
CheckIcon,
|
||||
GlobeIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from 'omorphia'
|
||||
import {
|
||||
BuyMeACoffeeIcon,
|
||||
@ -245,6 +252,7 @@ import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
|
||||
const searchStore = useSearch()
|
||||
|
||||
@ -255,6 +263,7 @@ const breadcrumbs = useBreadcrumbs()
|
||||
const confirmModal = ref(null)
|
||||
const modInstallModal = ref(null)
|
||||
const incompatibilityWarning = ref(null)
|
||||
const options = ref(null)
|
||||
const instance = ref(searchStore.instanceContext)
|
||||
const installing = ref(false)
|
||||
|
||||
@ -394,6 +403,37 @@ async function install(version) {
|
||||
|
||||
installing.value = false
|
||||
}
|
||||
|
||||
const handleRightClick = (e) => {
|
||||
options.value.showMenu(e, data.value, [
|
||||
{ name: 'install' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'open_link' },
|
||||
{ name: 'copy_link' },
|
||||
])
|
||||
}
|
||||
|
||||
const handleOptionsClick = (args) => {
|
||||
switch (args.option) {
|
||||
case 'install':
|
||||
install(null)
|
||||
break
|
||||
case 'open_link':
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: `https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user