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)
|
notifications.setNotifs(notificationsWrapper.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Link handler
|
document.addEventListener('contextmenu', (event) => event.preventDefault())
|
||||||
|
|
||||||
document.querySelector('body').addEventListener('click', function (e) {
|
document.querySelector('body').addEventListener('click', function (e) {
|
||||||
let target = e.target
|
let target = e.target
|
||||||
while (target != null) {
|
while (target != null) {
|
||||||
|
|||||||
@ -1,7 +1,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
import { computed, ref } from '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'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -16,6 +29,70 @@ const props = defineProps({
|
|||||||
default: '',
|
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 search = ref('')
|
||||||
const group = ref('Category')
|
const group = ref('Category')
|
||||||
@ -142,7 +219,10 @@ const filteredResults = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div
|
<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"
|
:key="instanceSection.key"
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
@ -153,12 +233,22 @@ const filteredResults = computed(() => {
|
|||||||
<section class="instances">
|
<section class="instances">
|
||||||
<Instance
|
<Instance
|
||||||
v-for="instance in instanceSection.value"
|
v-for="instance in instanceSection.value"
|
||||||
|
ref="instanceComponents"
|
||||||
:key="instance.id"
|
:key="instance.id"
|
||||||
display="card"
|
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
|
@contextmenu.prevent.stop="(event) => handleRightClick(event, instanceComponents[index])"
|
||||||
/>
|
/>
|
||||||
</section>
|
</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> <FolderOpenIcon /> Open folder </template>
|
||||||
|
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
||||||
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.row {
|
.row {
|
||||||
|
|||||||
@ -1,7 +1,21 @@
|
|||||||
<script setup>
|
<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 Instance from '@/components/ui/Instance.vue'
|
||||||
import { onMounted, onUnmounted, ref } from 'vue'
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
@ -17,58 +31,189 @@ const props = defineProps({
|
|||||||
canPaginate: Boolean,
|
canPaginate: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const allowPagination = ref(false)
|
const allowPagination = ref(Array.apply(false, Array(props.instances.length)))
|
||||||
const modsRow = ref(null)
|
const modsRow = ref(null)
|
||||||
|
const instanceOptions = ref(null)
|
||||||
|
const instanceComponents = ref(null)
|
||||||
|
|
||||||
const handlePaginationDisplay = () => {
|
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
|
// 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
|
// 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.
|
// wait-time for the below expressions has not failed thus-far.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const children = parentsRow.children
|
const children = parentsRow.children
|
||||||
const lastChild = children[children.length - 1]
|
const lastChild = children[children.length - 1]
|
||||||
const childBox = lastChild?.getBoundingClientRect()
|
const childBox = lastChild?.getBoundingClientRect()
|
||||||
|
|
||||||
if (childBox?.x + childBox?.width > window.innerWidth && props.canPaginate)
|
if (childBox?.x + childBox?.width > window.innerWidth && props.canPaginate)
|
||||||
allowPagination.value = true
|
allowPagination.value[i] = true
|
||||||
else allowPagination.value = false
|
else allowPagination.value[i] = false
|
||||||
}, 300)
|
}, 300)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.canPaginate) window.addEventListener('resize', handlePaginationDisplay)
|
if (props.canPaginate) window.addEventListener('resize', handlePaginationDisplay)
|
||||||
|
|
||||||
handlePaginationDisplay()
|
handlePaginationDisplay()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (props.canPaginate) window.removeEventListener('resize', handlePaginationDisplay)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="props.instances.length > 0" class="row">
|
<div class="content">
|
||||||
<div class="header">
|
<div v-for="(row, rowIndex) in instances" :key="row.label" class="row">
|
||||||
<p>{{ props.label }}</p>
|
<div class="header">
|
||||||
<hr aria-hidden="true" />
|
<p>{{ row.label }}</p>
|
||||||
<div v-if="allowPagination" class="pagination">
|
<hr aria-hidden="true" />
|
||||||
<ChevronLeftIcon role="button" @click="modsRow.value.scrollLeft -= 170" />
|
<div v-if="allowPagination[rowIndex]" class="pagination">
|
||||||
<ChevronRightIcon role="button" @click="modsRow.value.scrollLeft += 170" />
|
<ChevronLeftIcon role="button" @click="modsRow[rowIndex].scrollLeft -= 170" />
|
||||||
|
<ChevronRightIcon role="button" @click="modsRow[rowIndex].scrollLeft += 170" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<section ref="modsRow" class="instances">
|
||||||
|
<Instance
|
||||||
|
v-for="(instance, instanceIndex) in row.instances"
|
||||||
|
ref="instanceComponents"
|
||||||
|
:key="instance?.project_id || instance?.id"
|
||||||
|
:instance="instance"
|
||||||
|
@contextmenu.prevent.stop="
|
||||||
|
(event) =>
|
||||||
|
handleInstanceRightClick(
|
||||||
|
event,
|
||||||
|
instanceComponents[getInstanceIndex(rowIndex, instanceIndex)]
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<section ref="modsRow" class="instances">
|
|
||||||
<Instance
|
|
||||||
v-for="instance in props.instances"
|
|
||||||
:key="instance?.project_id || instance?.id"
|
|
||||||
display="card"
|
|
||||||
:instance="instance"
|
|
||||||
class="row-instance"
|
|
||||||
/>
|
|
||||||
</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>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -147,6 +292,11 @@ onUnmounted(() => {
|
|||||||
width: 0px;
|
width: 0px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.instance) {
|
||||||
|
min-width: 10.5rem;
|
||||||
|
max-width: 10.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,9 +305,4 @@ onUnmounted(() => {
|
|||||||
background-color: rgb(30, 31, 34);
|
background-color: rgb(30, 31, 34);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-instance {
|
|
||||||
min-width: 10.5rem;
|
|
||||||
max-width: 10.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
</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>
|
<script setup>
|
||||||
import { onUnmounted, ref, useSlots, watch } from 'vue'
|
import { onUnmounted, ref, useSlots, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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 { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||||
|
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
|
||||||
import { install as pack_install } from '@/helpers/pack'
|
import { install as pack_install } from '@/helpers/pack'
|
||||||
import { run, list } from '@/helpers/profile'
|
import { get, list, remove, run } from '@/helpers/profile'
|
||||||
import {
|
import {
|
||||||
kill_by_uuid,
|
|
||||||
get_all_running_profile_paths,
|
get_all_running_profile_paths,
|
||||||
get_uuids_by_profile_path,
|
get_uuids_by_profile_path,
|
||||||
|
kill_by_uuid,
|
||||||
} from '@/helpers/process'
|
} from '@/helpers/process'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { useFetch } from '@/helpers/fetch.js'
|
import { useFetch } from '@/helpers/fetch.js'
|
||||||
import { handleError } from '@/store/state.js'
|
import { handleError, useSearch } from '@/store/state.js'
|
||||||
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
|
import { showInFolder } from '@/helpers/utils.js'
|
||||||
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
|
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
|
||||||
|
|
||||||
|
const searchStore = useSearch()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -69,7 +72,7 @@ const checkProcess = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const install = async (e) => {
|
const install = async (e) => {
|
||||||
e.stopPropagation()
|
e?.stopPropagation()
|
||||||
modLoading.value = true
|
modLoading.value = true
|
||||||
const versions = await useFetch(
|
const versions = await useFetch(
|
||||||
`https://api.modrinth.com/v2/project/${props.instance.project_id}/version`,
|
`https://api.modrinth.com/v2/project/${props.instance.project_id}/version`,
|
||||||
@ -108,7 +111,7 @@ const install = async (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const play = async (e) => {
|
const play = async (e) => {
|
||||||
e.stopPropagation()
|
e?.stopPropagation()
|
||||||
modLoading.value = true
|
modLoading.value = true
|
||||||
uuid.value = await run(props.instance.path).catch(handleError)
|
uuid.value = await run(props.instance.path).catch(handleError)
|
||||||
modLoading.value = false
|
modLoading.value = false
|
||||||
@ -116,7 +119,7 @@ const play = async (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stop = async (e) => {
|
const stop = async (e) => {
|
||||||
e.stopPropagation()
|
e?.stopPropagation()
|
||||||
playing.value = false
|
playing.value = false
|
||||||
|
|
||||||
// If we lost the uuid for some reason, such as a user navigating
|
// If we lost the uuid for some reason, such as a user navigating
|
||||||
@ -131,6 +134,31 @@ const stop = async (e) => {
|
|||||||
uuid.value = null
|
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) => {
|
const unlisten = await process_listener((e) => {
|
||||||
if (e.event === 'finished' && e.uuid === uuid.value) playing.value = false
|
if (e.event === 'finished' && e.uuid === uuid.value) playing.value = false
|
||||||
})
|
})
|
||||||
@ -176,7 +204,7 @@ onUnmounted(() => unlisten())
|
|||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="none"
|
size="sm"
|
||||||
:src="
|
:src="
|
||||||
props.instance.metadata
|
props.instance.metadata
|
||||||
? !props.instance.metadata.icon ||
|
? !props.instance.metadata.icon ||
|
||||||
@ -213,7 +241,7 @@ onUnmounted(() => unlisten())
|
|||||||
@click="stop"
|
@click="stop"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<StopCircleIcon />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="install cta button-base" @click="install"><DownloadIcon /></div>
|
<div v-else class="install cta button-base" @click="install"><DownloadIcon /></div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -66,9 +66,26 @@ onUnmounted(() => unlisten())
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<RowDisplay label="Jump back in" :instances="recentInstances" :can-paginate="false" />
|
<RowDisplay
|
||||||
<RowDisplay label="Popular packs" :instances="featuredModpacks" :can-paginate="true" />
|
:instances="[
|
||||||
<RowDisplay label="Popular mods" :instances="featuredMods" :can-paginate="true" />
|
{
|
||||||
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="instance-container">
|
<div class="instance-container">
|
||||||
<div class="side-cards">
|
<div class="side-cards">
|
||||||
<Card class="instance-card">
|
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="lg"
|
size="lg"
|
||||||
:src="
|
:src="
|
||||||
@ -28,7 +28,7 @@
|
|||||||
@click="stopInstance"
|
@click="stopInstance"
|
||||||
@mouseover="checkProcess"
|
@mouseover="checkProcess"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<StopCircleIcon />
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -78,40 +78,60 @@
|
|||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<template v-if="Component">
|
<template v-if="Component">
|
||||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||||
<component :is="Component" :instance="instance"></component>
|
<component :is="Component" :instance="instance" :options="options"></component>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</template>
|
</template>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
XIcon,
|
|
||||||
Button,
|
Button,
|
||||||
Avatar,
|
Avatar,
|
||||||
Card,
|
Card,
|
||||||
Promotion,
|
Promotion,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
EditIcon,
|
||||||
|
HammerIcon,
|
||||||
|
TrashIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ExternalIcon,
|
||||||
} from 'omorphia'
|
} from 'omorphia'
|
||||||
import { get, run } from '@/helpers/profile'
|
import { get, install, remove, run } from '@/helpers/profile'
|
||||||
import {
|
import {
|
||||||
get_all_running_profile_paths,
|
get_all_running_profile_paths,
|
||||||
get_uuids_by_profile_path,
|
get_uuids_by_profile_path,
|
||||||
kill_by_uuid,
|
kill_by_uuid,
|
||||||
} from '@/helpers/process'
|
} from '@/helpers/process'
|
||||||
import { process_listener, profile_listener } from '@/helpers/events'
|
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 { ref, onUnmounted } from 'vue'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||||
import { handleError, useBreadcrumbs, useLoading, useSearch } from '@/store/state'
|
import { handleError, useBreadcrumbs, useLoading, useSearch } from '@/store/state'
|
||||||
import { showInFolder } from '@/helpers/utils.js'
|
import { showInFolder } from '@/helpers/utils.js'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const searchStore = useSearch()
|
const searchStore = useSearch()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
|
|
||||||
@ -129,6 +149,7 @@ const loadingBar = useLoading()
|
|||||||
const uuid = ref(null)
|
const uuid = ref(null)
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const options = ref(null)
|
||||||
|
|
||||||
const startInstance = async () => {
|
const startInstance = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -170,6 +191,91 @@ const unlistenProcesses = await process_listener((e) => {
|
|||||||
if (e.event === 'finished' && uuid.value === e.uuid) playing.value = false
|
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(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
unlistenProfiles()
|
unlistenProfiles()
|
||||||
|
|||||||
@ -60,7 +60,12 @@
|
|||||||
<div class="table-cell table-text">Author</div>
|
<div class="table-cell table-text">Author</div>
|
||||||
<div class="table-cell table-text">Actions</div>
|
<div class="table-cell table-text">Actions</div>
|
||||||
</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">
|
<div class="table-cell table-text">
|
||||||
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator"></AnimatedLogo>
|
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator"></AnimatedLogo>
|
||||||
<Button
|
<Button
|
||||||
@ -138,6 +143,12 @@ const props = defineProps({
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const projects = ref([])
|
const projects = ref([])
|
||||||
@ -302,6 +313,18 @@ async function removeMod(mod) {
|
|||||||
await remove_project(props.instance.path, mod.path).catch(handleError)
|
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="root-container">
|
<div class="root-container">
|
||||||
<div v-if="data" class="project-sidebar">
|
<div v-if="data" class="project-sidebar">
|
||||||
<Instance v-if="instance" :instance="instance" small />
|
<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" />
|
<Avatar size="lg" :src="data.icon_url" />
|
||||||
<div class="instance-info">
|
<div class="instance-info">
|
||||||
<h2 class="name">{{ data.title }}</h2>
|
<h2 class="name">{{ data.title }}</h2>
|
||||||
@ -197,6 +197,11 @@
|
|||||||
<InstallConfirmModal ref="confirmModal" />
|
<InstallConfirmModal ref="confirmModal" />
|
||||||
<InstanceInstallModal ref="modInstallModal" />
|
<InstanceInstallModal ref="modInstallModal" />
|
||||||
<IncompatibilityWarningModal ref="incompatibilityWarning" />
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -220,6 +225,8 @@ import {
|
|||||||
formatNumber,
|
formatNumber,
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
} from 'omorphia'
|
} from 'omorphia'
|
||||||
import {
|
import {
|
||||||
BuyMeACoffeeIcon,
|
BuyMeACoffeeIcon,
|
||||||
@ -245,6 +252,7 @@ import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|||||||
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
|
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
|
||||||
import { useFetch } from '@/helpers/fetch.js'
|
import { useFetch } from '@/helpers/fetch.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
|
||||||
const searchStore = useSearch()
|
const searchStore = useSearch()
|
||||||
|
|
||||||
@ -255,6 +263,7 @@ const breadcrumbs = useBreadcrumbs()
|
|||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
const modInstallModal = ref(null)
|
const modInstallModal = ref(null)
|
||||||
const incompatibilityWarning = ref(null)
|
const incompatibilityWarning = ref(null)
|
||||||
|
const options = ref(null)
|
||||||
const instance = ref(searchStore.instanceContext)
|
const instance = ref(searchStore.instanceContext)
|
||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
|
|
||||||
@ -394,6 +403,37 @@ async function install(version) {
|
|||||||
|
|
||||||
installing.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user