You've already forked AstralRinth
381ea51cce
* fix: files.vue bugs before styling changes * feat: move files tab to shared layout structure * fix: qa * fix: qa * fix: bugs * fix: lint * fix: admonition cleanup with progress + actions * fix: cleanup * fix: modals * fix: admon title * fix: i18n standard * fix: lint + i18n pass * fix: remove transition * fix: type errors * feat: files tab in app * fix: qa * fix: backup item minmax * fix: use ContentPageHeader for server panel * fix: lint * fix: lint * fix: lint * feat: page leave safety * fix: lint * fix: cargo fmt fix * fix: blank in prod * fix: content card table stuff * Revert "fix: blank in prod" This reverts commit 74758fe185cf85a4a20355857f889cb091b97ace. * fix: import * feat: browse worlds/servers flow * fix: worlds tab parity with content tab * fix: perf bug + shader filter pill copy * feat: singleplayer filter * fix: ordering * fix: breadcrumbs * fix: lint * fix: qa * feat: store server proj id when adding to a non-linked instance * fix: lint * fix: i18n + qa * fix: conflict * qa: already installed modal + placeholders not server-specific * fix: qa * fix: add + edit server modals * fix: qa * fix: security * fix: devin flags * fix: lint * chore: change file to break build cache * fix: admon * fix: import path stuff * feat: qa * fix: fmt fmt idiot --------- Signed-off-by: Calum H. <calum@modrinth.com>
183 lines
5.2 KiB
Vue
183 lines
5.2 KiB
Vue
<template>
|
|
<Teleport to="#teleports">
|
|
<Transition
|
|
enter-active-class="transition duration-125 ease-out"
|
|
enter-from-class="transform scale-75 opacity-0"
|
|
enter-to-class="transform scale-100 opacity-100"
|
|
leave-active-class="transition duration-125 ease-in"
|
|
leave-from-class="transform scale-100 opacity-100"
|
|
leave-to-class="transform scale-75 opacity-0"
|
|
>
|
|
<div
|
|
v-if="visible"
|
|
ref="menuRef"
|
|
class="experimental-styles-within fixed isolate z-[9999] flex w-fit min-w-[180px] flex-col gap-2 overflow-hidden rounded-2xl border border-solid border-surface-5 bg-bg-raised p-2 shadow-lg"
|
|
:style="{ left: `${position.x}px`, top: `${position.y}px` }"
|
|
role="menu"
|
|
tabindex="-1"
|
|
@mousedown.stop
|
|
>
|
|
<ButtonStyled type="transparent">
|
|
<button
|
|
class="w-full !justify-start !whitespace-nowrap"
|
|
role="menuitem"
|
|
@click="handleCopyFilename"
|
|
>
|
|
<ClipboardCopyIcon class="size-5" />
|
|
{{ formatMessage(commonMessages.copyFilenameButton) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled type="transparent">
|
|
<button
|
|
class="w-full !justify-start !whitespace-nowrap"
|
|
role="menuitem"
|
|
@click="handleCopyPath"
|
|
>
|
|
<ClipboardCopyIcon class="size-5" />
|
|
{{ formatMessage(commonMessages.copyFullPathButton) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled v-if="ctx.openInFolder" type="transparent">
|
|
<button
|
|
class="w-full !justify-start !whitespace-nowrap"
|
|
role="menuitem"
|
|
@click="handleOpenInFolder"
|
|
>
|
|
<FolderOpenIcon class="size-5" />
|
|
{{ formatMessage(commonMessages.openInFolderButton) }}
|
|
</button>
|
|
</ButtonStyled>
|
|
<div class="h-px w-full bg-surface-5" />
|
|
<template v-for="(option, index) in menuOptions" :key="index">
|
|
<div
|
|
v-if="'divider' in option && option.divider && option.shown !== false"
|
|
class="h-px w-full bg-surface-5"
|
|
/>
|
|
<ButtonStyled
|
|
v-else-if="'id' in option && option.shown !== false"
|
|
type="transparent"
|
|
:color="option.color"
|
|
>
|
|
<button
|
|
v-tooltip="option.tooltip"
|
|
:disabled="option.disabled"
|
|
class="w-full !justify-start !whitespace-nowrap"
|
|
role="menuitem"
|
|
@click="handleOptionClick(option)"
|
|
>
|
|
<slot :name="option.id" />
|
|
</button>
|
|
</ButtonStyled>
|
|
</template>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ClipboardCopyIcon, FolderOpenIcon } from '@modrinth/assets'
|
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
|
import { useVIntl } from '#ui/composables/i18n'
|
|
import { injectNotificationManager } from '#ui/providers/web-notifications'
|
|
import { commonMessages } from '#ui/utils/common-messages'
|
|
|
|
import { injectFileManager } from '../providers/file-manager'
|
|
import type { FileContextMenuOption, FileItem } from '../types'
|
|
|
|
const { formatMessage } = useVIntl()
|
|
const { addNotification } = injectNotificationManager()
|
|
const ctx = injectFileManager()
|
|
|
|
const visible = ref(false)
|
|
const menuRef = ref<HTMLElement>()
|
|
const position = ref({ x: 0, y: 0 })
|
|
const currentItem = ref<FileItem | null>(null)
|
|
const menuOptions = ref<FileContextMenuOption[]>([])
|
|
|
|
function show(item: FileItem, x: number, y: number, options: typeof menuOptions.value) {
|
|
currentItem.value = item
|
|
menuOptions.value = options
|
|
position.value = { x, y }
|
|
visible.value = true
|
|
|
|
nextTick(() => {
|
|
if (!menuRef.value) return
|
|
const rect = menuRef.value.getBoundingClientRect()
|
|
const padding = 10
|
|
if (rect.right > window.innerWidth - padding) {
|
|
position.value.x = Math.max(padding, x - rect.width)
|
|
}
|
|
if (rect.bottom > window.innerHeight - padding) {
|
|
position.value.y = Math.max(padding, y - rect.height)
|
|
}
|
|
})
|
|
}
|
|
|
|
function hide() {
|
|
visible.value = false
|
|
currentItem.value = null
|
|
}
|
|
|
|
function handleCopyFilename() {
|
|
if (!currentItem.value) return
|
|
navigator.clipboard.writeText(currentItem.value.name)
|
|
addNotification({ title: formatMessage(commonMessages.copiedFilenameLabel), type: 'success' })
|
|
hide()
|
|
}
|
|
|
|
function getFullPath() {
|
|
if (!currentItem.value) return ''
|
|
const basePath = ctx.basePath?.value
|
|
const itemPath = currentItem.value.path
|
|
return basePath ? `${basePath}/${itemPath}`.replace(/\/+/g, '/') : itemPath
|
|
}
|
|
|
|
function handleCopyPath() {
|
|
if (!currentItem.value) return
|
|
navigator.clipboard.writeText(getFullPath())
|
|
addNotification({ title: formatMessage(commonMessages.copiedPathLabel), type: 'success' })
|
|
hide()
|
|
}
|
|
|
|
function handleOpenInFolder() {
|
|
if (!currentItem.value) return
|
|
ctx.openInFolder?.(getFullPath())
|
|
hide()
|
|
}
|
|
|
|
function handleOptionClick(option: { action?: () => void }) {
|
|
option.action?.()
|
|
hide()
|
|
}
|
|
|
|
function onClickOutside(event: MouseEvent) {
|
|
if (menuRef.value && !menuRef.value.contains(event.target as Node)) {
|
|
hide()
|
|
}
|
|
}
|
|
|
|
function onEscape(event: KeyboardEvent) {
|
|
if (event.key === 'Escape') {
|
|
hide()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('mousedown', onClickOutside)
|
|
document.addEventListener('keydown', onEscape)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('mousedown', onClickOutside)
|
|
document.removeEventListener('keydown', onEscape)
|
|
})
|
|
|
|
watch(visible, (v) => {
|
|
if (!v) currentItem.value = null
|
|
})
|
|
|
|
defineExpose({ show, hide })
|
|
</script>
|