You've already forked AstralRinth
forked from didirus/AstralRinth
feat: modrinth hosting - files tab refactor (#4912)
* feat: api-client module for content v0 * feat: delete unused components + modules + setting * feat: xhr uploading * feat: fs module -> api-client * feat: migrate files.vue to use tanstack * fix: mem leak + other issues * fix: build * feat: switch to monaco * fix: go back to using ace, but improve preloading + theme * fix: styling + dead attrs * feat: match figma * fix: padding * feat: files-new for ui page structure * feat: finalize files.vue * fix: lint * fix: qa * fix: dep * fix: lint * fix: lockfile merge * feat: icons on navtab * fix: surface alternating on table * fix: hover surface color --------- Signed-off-by: Calum H. <contact@cal.engineer> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<li
|
||||
role="button"
|
||||
data-pyro-file
|
||||
:class="[
|
||||
containerClasses,
|
||||
isDragOver && type === 'directory' ? 'bg-brand-highlight' : '',
|
||||
@@ -12,6 +11,7 @@
|
||||
@click="selectItem"
|
||||
@contextmenu="openContextMenu"
|
||||
@keydown="(e) => e.key === 'Enter' && selectItem()"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@dragstart="handleDragStart"
|
||||
@dragend="handleDragEnd"
|
||||
@dragenter.prevent="handleDragEnter"
|
||||
@@ -19,35 +19,32 @@
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<div
|
||||
data-pyro-file-metadata
|
||||
class="pointer-events-none flex w-full items-center gap-4 truncate"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none flex size-8 items-center justify-center rounded-full bg-bg-raised p-[6px] group-hover:bg-brand-highlight group-hover:text-brand group-focus:bg-brand-highlight group-focus:text-brand"
|
||||
:class="isEditableFile ? 'group-active:scale-[0.8]' : ''"
|
||||
>
|
||||
<component :is="iconComponent" class="size-6" />
|
||||
<div class="pointer-events-none flex flex-1 items-center gap-3 truncate">
|
||||
<Checkbox
|
||||
class="pointer-events-auto"
|
||||
:model-value="selected"
|
||||
@click.stop
|
||||
@update:model-value="emit('toggle-select')"
|
||||
/>
|
||||
<div class="pointer-events-none flex size-5 items-center justify-center">
|
||||
<component :is="iconComponent" class="size-5" />
|
||||
</div>
|
||||
<div class="pointer-events-none flex w-full flex-col truncate">
|
||||
<div class="pointer-events-none flex flex-col truncate">
|
||||
<span
|
||||
class="pointer-events-none w-[98%] truncate font-bold group-hover:text-contrast group-focus:text-contrast"
|
||||
class="pointer-events-none truncate group-hover:text-contrast group-focus:text-contrast"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
<span class="pointer-events-none text-xs text-secondary group-hover:text-primary">
|
||||
{{ subText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-pyro-file-actions
|
||||
class="pointer-events-auto flex w-fit flex-shrink-0 items-center gap-4 md:gap-12"
|
||||
>
|
||||
<span class="hidden w-[160px] text-nowrap font-mono text-sm text-secondary md:flex">
|
||||
<div class="pointer-events-auto flex w-fit flex-shrink-0 items-center gap-4 md:gap-12">
|
||||
<span class="hidden w-[100px] text-nowrap text-sm text-secondary md:block">
|
||||
{{ formattedSize }}
|
||||
</span>
|
||||
<span class="hidden w-[160px] text-nowrap text-sm text-secondary md:block">
|
||||
{{ formattedCreationDate }}
|
||||
</span>
|
||||
<span class="w-[160px] text-nowrap font-mono text-sm text-secondary">
|
||||
<span class="hidden w-[160px] text-nowrap text-sm text-secondary md:block">
|
||||
{{ formattedModifiedDate }}
|
||||
</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
@@ -71,22 +68,23 @@ import {
|
||||
FolderOpenIcon,
|
||||
MoreHorizontalIcon,
|
||||
PackageOpenIcon,
|
||||
PaletteIcon,
|
||||
RightArrowIcon,
|
||||
TrashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
CODE_EXTENSIONS,
|
||||
Checkbox,
|
||||
getFileExtension,
|
||||
getFileExtensionIcon,
|
||||
IMAGE_EXTENSIONS,
|
||||
TEXT_EXTENSIONS,
|
||||
isEditableFile as isEditableFileExt,
|
||||
isImageFile,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, h, ref, shallowRef } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { UiServersIconsCogFolderIcon, UiServersIconsEarthIcon } from '#components'
|
||||
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||
|
||||
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
||||
|
||||
@@ -98,17 +96,21 @@ interface FileItemProps {
|
||||
modified: number
|
||||
created: number
|
||||
path: string
|
||||
index: number
|
||||
isLast: boolean
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<FileItemProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'rename' | 'move' | 'download' | 'delete' | 'edit' | 'extract',
|
||||
e: 'rename' | 'move' | 'download' | 'delete' | 'edit' | 'extract' | 'hover',
|
||||
item: { name: string; type: string; path: string },
|
||||
): void
|
||||
(e: 'moveDirectTo', item: { name: string; type: string; path: string; destination: string }): void
|
||||
(e: 'contextmenu', x: number, y: number): void
|
||||
(e: 'toggle-select'): void
|
||||
}>()
|
||||
|
||||
const isDragOver = ref(false)
|
||||
@@ -120,12 +122,15 @@ const route = shallowRef(useRoute())
|
||||
const router = useRouter()
|
||||
|
||||
const containerClasses = computed(() => [
|
||||
'group m-0 p-0 focus:!outline-none flex w-full select-none items-center justify-between overflow-hidden border-0 border-b border-solid border-bg-raised p-3 last:border-none hover:bg-bg-raised focus:bg-bg-raised',
|
||||
'group m-0 flex w-full select-none items-center justify-between overflow-hidden border-0 border-t border-solid border-surface-3 px-4 py-3 focus:!outline-none',
|
||||
props.index % 2 === 0 ? 'bg-surface-2' : 'bg-surface-3',
|
||||
props.isLast ? 'rounded-b-[20px] border-b' : '',
|
||||
isEditableFile.value ? 'cursor-pointer' : props.type === 'directory' ? 'cursor-pointer' : '',
|
||||
isDragOver.value ? 'bg-brand-highlight' : '',
|
||||
isDragOver.value ? '!bg-brand-highlight' : '',
|
||||
'hover:brightness-110 focus:brightness-110',
|
||||
])
|
||||
|
||||
const fileExtension = computed(() => props.name.split('.').pop()?.toLowerCase() || '')
|
||||
const fileExtension = computed(() => getFileExtension(props.name))
|
||||
|
||||
const isZip = computed(() => fileExtension.value === 'zip')
|
||||
|
||||
@@ -170,13 +175,6 @@ const iconComponent = computed(() => {
|
||||
return getFileExtensionIcon(fileExtension.value)
|
||||
})
|
||||
|
||||
const subText = computed(() => {
|
||||
if (props.type === 'directory') {
|
||||
return `${props.count} ${props.count === 1 ? 'item' : 'items'}`
|
||||
}
|
||||
return formattedSize.value
|
||||
})
|
||||
|
||||
const formattedModifiedDate = computed(() => {
|
||||
const date = new Date(props.modified * 1000)
|
||||
return `${date.toLocaleDateString('en-US', {
|
||||
@@ -206,17 +204,16 @@ const formattedCreationDate = computed(() => {
|
||||
const isEditableFile = computed(() => {
|
||||
if (props.type === 'file') {
|
||||
const ext = fileExtension.value
|
||||
return (
|
||||
!props.name.includes('.') ||
|
||||
TEXT_EXTENSIONS.includes(ext) ||
|
||||
CODE_EXTENSIONS.includes(ext) ||
|
||||
IMAGE_EXTENSIONS.includes(ext)
|
||||
)
|
||||
return !props.name.includes('.') || isEditableFileExt(ext) || isImageFile(ext)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const formattedSize = computed(() => {
|
||||
if (props.type === 'directory') {
|
||||
return `${props.count} ${props.count === 1 ? 'item' : 'items'}`
|
||||
}
|
||||
|
||||
if (props.size === undefined) return ''
|
||||
const bytes = props.size
|
||||
if (bytes === 0) return '0 B'
|
||||
@@ -226,12 +223,16 @@ const formattedSize = computed(() => {
|
||||
return `${size} ${units[exponent]}`
|
||||
})
|
||||
|
||||
const openContextMenu = (event: MouseEvent) => {
|
||||
function openContextMenu(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
emit('contextmenu', event.clientX, event.clientY)
|
||||
}
|
||||
|
||||
const navigateToFolder = () => {
|
||||
function handleMouseEnter() {
|
||||
emit('hover', { name: props.name, type: props.type, path: props.path })
|
||||
}
|
||||
|
||||
function navigateToFolder() {
|
||||
const currentPath = route.value.query.path?.toString() || ''
|
||||
const newPath = currentPath.endsWith('/')
|
||||
? `${currentPath}${props.name}`
|
||||
@@ -241,7 +242,7 @@ const navigateToFolder = () => {
|
||||
|
||||
const isNavigating = ref(false)
|
||||
|
||||
const selectItem = () => {
|
||||
function selectItem() {
|
||||
if (isNavigating.value) return
|
||||
isNavigating.value = true
|
||||
|
||||
@@ -256,11 +257,12 @@ const selectItem = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const getDragIcon = async () => {
|
||||
async function getDragIcon() {
|
||||
// Reuse iconComponent computed for consistency
|
||||
return await renderToString(h(iconComponent.value))
|
||||
}
|
||||
|
||||
const handleDragStart = async (event: DragEvent) => {
|
||||
async function handleDragStart(event: DragEvent) {
|
||||
if (!event.dataTransfer) return
|
||||
isDragging.value = true
|
||||
|
||||
@@ -291,7 +293,7 @@ const handleDragStart = async (event: DragEvent) => {
|
||||
})
|
||||
|
||||
event.dataTransfer.setData(
|
||||
'application/pyro-file-move',
|
||||
'application/modrinth-file-move',
|
||||
JSON.stringify({
|
||||
name: props.name,
|
||||
type: props.type,
|
||||
@@ -301,34 +303,34 @@ const handleDragStart = async (event: DragEvent) => {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
|
||||
const isChildPath = (parentPath: string, childPath: string) => {
|
||||
function isChildPath(parentPath: string, childPath: string) {
|
||||
return childPath.startsWith(parentPath + '/')
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
function handleDragEnd() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
const handleDragEnter = () => {
|
||||
function handleDragEnter() {
|
||||
if (props.type !== 'directory') return
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
function handleDragOver(event: DragEvent) {
|
||||
if (props.type !== 'directory' || !event.dataTransfer) return
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
function handleDragLeave() {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
function handleDrop(event: DragEvent) {
|
||||
isDragOver.value = false
|
||||
if (props.type !== 'directory' || !event.dataTransfer) return
|
||||
|
||||
try {
|
||||
const dragData = JSON.parse(event.dataTransfer.getData('application/pyro-file-move'))
|
||||
const dragData = JSON.parse(event.dataTransfer.getData('application/modrinth-file-move'))
|
||||
|
||||
if (dragData.path === props.path) return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user