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:
Adrian O.V 2023-06-10 15:31:52 -04:00 committed by GitHub
parent e0e9c3f166
commit e836738887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 672 additions and 61 deletions

View File

@ -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) {

View File

@ -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 {

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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()

View File

@ -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">

View File

@ -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">