Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
8a3ab94
chore: dedupe lockfile
ferothefox Dec 13, 2024
f69c15d
fix: incorrect spacing between editing and browsing state
ferothefox Dec 13, 2024
e3819ea
chore: improve files image viewer toolbar
ferothefox Dec 13, 2024
77e9db4
chore: image viewer cursor affordance
ferothefox Dec 13, 2024
3da4a45
chore: clean imports
ferothefox Dec 13, 2024
183cbb5
chore: add tooltips
ferothefox Dec 13, 2024
80b0a95
chore: use black background
ferothefox Dec 13, 2024
3d66082
feat: show scale factor, handle large images, consolidate state
ferothefox Dec 13, 2024
13f3a43
chore: add types to fs operations
ferothefox Dec 13, 2024
3b3e9b5
feat: add date create sorting option
ferothefox Dec 13, 2024
172c131
fix: match name of folder creation modal
ferothefox Dec 13, 2024
ffd01f2
fix: add it here too
ferothefox Dec 13, 2024
7b5f811
feat: add creation date to file item, file manager header
ferothefox Dec 13, 2024
e7057eb
chore: a11y
ferothefox Dec 13, 2024
bcbd069
fix: ensure move item modal always has leading slash
ferothefox Dec 14, 2024
5381562
fix: correct move input placeholder
ferothefox Dec 14, 2024
c6361b1
chore: correct design disparity
ferothefox Dec 14, 2024
e24bd5f
chore: add better affordance on active file item state
ferothefox Dec 14, 2024
59a3185
fix: correct instances where we dont sentence case
ferothefox Dec 14, 2024
8549991
chore: clean
ferothefox Dec 14, 2024
f2199d6
chore: notify that server restarted on saveandrestart
ferothefox Dec 14, 2024
9cea408
fix: consolidate error state in file manager
ferothefox Dec 14, 2024
445fa56
chore: adjust sizing
ferothefox Dec 14, 2024
afbd7af
feat: drag and drop file items to move them
ferothefox Dec 14, 2024
ceb4336
feat: enable ability to drag folders too
ferothefox Dec 14, 2024
d04a25c
feat: better file movement toasts
ferothefox Dec 14, 2024
ad18e1e
just say u hate me
ferothefox Dec 14, 2024
3322582
feat: uploading indicator for file uploads
ferothefox Dec 14, 2024
1cfa85a
chore: cleaner file item ghost when dragging
ferothefox Dec 14, 2024
69b9fc1
fix: enforce max length and truncate on ghost
ferothefox Dec 14, 2024
ec6beaa
chore: improve item rename toast
ferothefox Dec 14, 2024
4b56a49
chore: improve item create toast
ferothefox Dec 14, 2024
532d596
feat: undo and redo stack
ferothefox Dec 14, 2024
9231990
fix: confusing behavior where folders were not sorted alphabetically
ferothefox Dec 14, 2024
dd8d4eb
feat: find and replace in file editor
ferothefox Dec 14, 2024
829281d
feat: correctly set language mode of file editor
ferothefox Dec 14, 2024
ff23786
chore: slop
ferothefox Dec 14, 2024
8677784
chore: actually handle case with multiple dots in file name before se…
ferothefox Dec 14, 2024
9d45f15
Merge branch 'modrinth:main' into evan/files-ux-improvements-1
ferothefox Dec 14, 2024
af29d0d
fix: match move icons in file context/threedot
ferothefox Dec 15, 2024
fbbc411
feat: upload indicator
ferothefox Dec 15, 2024
fca3a6c
Merge remote-tracking branch 'origin/main' into evan/files-ux-improve…
ferothefox Dec 15, 2024
135eb85
chore: dedupe lockfile again
ferothefox Dec 15, 2024
c0202a2
Merge remote-tracking branch 'origin/main' into evan/files-ux-improve…
ferothefox Dec 15, 2024
4c6b2c2
lockfile
ferothefox Dec 15, 2024
e427f79
fix: file undefinedness
ferothefox Dec 15, 2024
6b6b4b6
Merge branch 'modrinth:main' into evan/files-ux-improvements-1
ferothefox Dec 15, 2024
41ea17b
Merge branch 'modrinth:main' into evan/files-ux-improvements-1
ferothefox Dec 15, 2024
b72729e
Merge branch 'modrinth:main' into evan/files-ux-improvements-1
ferothefox Dec 15, 2024
f015ba2
Merge branch 'modrinth:main' into evan/files-ux-improvements-1
ferothefox Dec 15, 2024
427d3ca
checkpoint
ferothefox Dec 15, 2024
ef7702c
checkpoint
ferothefox Dec 15, 2024
8ae5bfa
checkpoint
ferothefox Dec 15, 2024
24b77c4
remove shitty animation logic
ferothefox Dec 15, 2024
6646de2
feat: file upload queuer
ferothefox Dec 16, 2024
33c46c6
chore: only allow editable files to have active affordance
ferothefox Dec 16, 2024
c830826
fix: properly throw pyrofetcherror when rename fails
ferothefox Dec 16, 2024
5d19948
feat: cancel file uploads
ferothefox Dec 16, 2024
dce1e77
chore: clean
ferothefox Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 186 additions & 18 deletions apps/frontend/src/components/ui/servers/FileItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,61 @@
<li
role="button"
data-pyro-file
:class="containerClasses"
:class="[
containerClasses,
isDragOver && type === 'directory' ? 'bg-brand-highlight' : '',
isDragging ? 'opacity-50' : '',
]"
tabindex="0"
draggable="true"
@click="selectItem"
@contextmenu="openContextMenu"
@keydown="(e) => e.key === 'Enter' && selectItem()"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
@dragenter.prevent="handleDragEnter"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
>
<div data-pyro-file-metadata class="flex w-full items-center gap-4 truncate">
<div
data-pyro-file-metadata
class="pointer-events-none flex w-full items-center gap-4 truncate"
>
<div
class="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="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>
<div class="flex w-full flex-col truncate">
<div class="pointer-events-none flex w-full flex-col truncate">
<span
class="w-[98%] truncate font-bold group-hover:text-contrast group-focus:text-contrast"
>{{ name }}</span
class="pointer-events-none w-[98%] truncate font-bold group-hover:text-contrast group-focus:text-contrast"
>
<span class="text-xs text-secondary group-hover:text-primary">
{{ 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="flex w-fit flex-shrink-0 items-center gap-4 md:gap-12">
<span class="w-[160px] text-nowrap text-right font-mono text-sm text-secondary">{{
formattedDate
}}</span>
<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">
{{ formattedCreationDate }}
</span>
<span class="w-[160px] text-nowrap font-mono text-sm text-secondary">
{{ formattedModifiedDate }}
</span>
<ButtonStyled circular type="transparent">
<UiServersTeleportOverflowMenu :options="menuOptions" direction="left" position="bottom">
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
<template #rename> <EditIcon /> Rename </template>
<template #move> <RightArrowIcon /> Move </template>
<template #download> <DownloadIcon /> Download </template>
<template #delete> <TrashIcon /> Delete </template>
<template #rename><EditIcon /> Rename</template>
<template #move><RightArrowIcon /> Move</template>
<template #download><DownloadIcon /> Download</template>
<template #delete><TrashIcon /> Delete</template>
</UiServersTeleportOverflowMenu>
</ButtonStyled>
</div>
Expand All @@ -54,6 +75,7 @@ import {
RightArrowIcon,
} from "@modrinth/assets";
import { computed, shallowRef, ref } from "vue";
import { renderToString } from "@vue/server-renderer";
import { useRouter, useRoute } from "vue-router";
import {
UiServersIconsCogFolderIcon,
Expand All @@ -70,12 +92,27 @@ interface FileItemProps {
size?: number;
count?: number;
modified: number;
created: number;
path: string;
}

const props = defineProps<FileItemProps>();

const emit = defineEmits(["rename", "download", "delete", "move", "edit", "contextmenu"]);
const emit = defineEmits<{
(e: "rename", item: { name: string; type: string; path: string }): void;
(e: "move", item: { name: string; type: string; path: string }): void;
(
e: "moveDirectTo",
item: { name: string; type: string; path: string; destination: string },
): void;
(e: "download", item: { name: string; type: string; path: string }): void;
(e: "delete", item: { name: string; type: string; path: string }): void;
(e: "edit", item: { name: string; type: string; path: string }): void;
(e: "contextmenu", x: number, y: number): void;
}>();

const isDragOver = ref(false);
const isDragging = ref(false);

const codeExtensions = Object.freeze([
"json",
Expand Down Expand Up @@ -114,6 +151,7 @@ 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",
isEditableFile.value ? "cursor-pointer" : props.type === "directory" ? "cursor-pointer" : "",
isDragOver.value ? "bg-brand-highlight" : "",
]);

const fileExtension = computed(() => props.name.split(".").pop()?.toLowerCase() || "");
Expand Down Expand Up @@ -161,7 +199,7 @@ const subText = computed(() => {
return formattedSize.value;
});

const formattedDate = computed(() => {
const formattedModifiedDate = computed(() => {
const date = new Date(props.modified * 1000);
return `${date.toLocaleDateString("en-US", {
month: "2-digit",
Expand All @@ -174,6 +212,19 @@ const formattedDate = computed(() => {
})}`;
});

const formattedCreationDate = computed(() => {
const date = new Date(props.created * 1000);
return `${date.toLocaleDateString("en-US", {
month: "2-digit",
day: "2-digit",
year: "2-digit",
})}, ${date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true,
})}`;
});

const isEditableFile = computed(() => {
if (props.type === "file") {
const ext = fileExtension.value;
Expand Down Expand Up @@ -226,4 +277,121 @@ const selectItem = () => {
isNavigating.value = false;
}, 500);
};

const getDragIcon = async () => {
let iconToUse;

if (props.type === "directory") {
if (props.name === "config") {
iconToUse = UiServersIconsCogFolderIcon;
} else if (props.name === "world") {
iconToUse = UiServersIconsEarthIcon;
} else if (props.name === "resourcepacks") {
iconToUse = PaletteIcon;
} else {
iconToUse = FolderOpenIcon;
}
} else {
const ext = fileExtension.value;
if (codeExtensions.includes(ext)) {
iconToUse = UiServersIconsCodeFileIcon;
} else if (textExtensions.includes(ext)) {
iconToUse = UiServersIconsTextFileIcon;
} else if (imageExtensions.includes(ext)) {
iconToUse = UiServersIconsImageFileIcon;
} else {
iconToUse = FileIcon;
}
}

return await renderToString(h(iconToUse));
};

const handleDragStart = async (event: DragEvent) => {
if (!event.dataTransfer) return;
isDragging.value = true;

const dragGhost = document.createElement("div");
dragGhost.className =
"fixed left-0 top-0 flex items-center max-w-[500px] flex-row gap-3 rounded-lg bg-bg-raised p-3 shadow-lg pointer-events-none";

const iconContainer = document.createElement("div");
iconContainer.className = "flex size-6 items-center justify-center";

const icon = document.createElement("div");
icon.className = "size-4";
icon.innerHTML = await getDragIcon();
iconContainer.appendChild(icon);

const nameSpan = document.createElement("span");
nameSpan.className = "font-bold truncate text-contrast";
nameSpan.textContent = props.name;

dragGhost.appendChild(iconContainer);
dragGhost.appendChild(nameSpan);
document.body.appendChild(dragGhost);

event.dataTransfer.setDragImage(dragGhost, 0, 0);

requestAnimationFrame(() => {
document.body.removeChild(dragGhost);
});

event.dataTransfer.setData(
"application/pyro-file-move",
JSON.stringify({
name: props.name,
type: props.type,
path: props.path,
}),
);
event.dataTransfer.effectAllowed = "move";
};

const isChildPath = (parentPath: string, childPath: string) => {
return childPath.startsWith(parentPath + "/");
};

const handleDragEnd = () => {
isDragging.value = false;
};

const handleDragEnter = () => {
if (props.type !== "directory") return;
isDragOver.value = true;
};

const handleDragOver = (event: DragEvent) => {
if (props.type !== "directory" || !event.dataTransfer) return;
event.dataTransfer.dropEffect = "move";
};

const handleDragLeave = () => {
isDragOver.value = false;
};

const 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"));

if (dragData.path === props.path) return;

if (dragData.type === "directory" && isChildPath(dragData.path, props.path)) {
console.error("Cannot move a folder into its own subfolder");
return;
}

emit("moveDirectTo", {
name: dragData.name,
type: dragData.type,
path: dragData.path,
destination: props.path,
});
} catch (error) {
console.error("Error handling file drop:", error);
}
};
</script>
2 changes: 2 additions & 0 deletions apps/frontend/src/components/ui/servers/FileVirtualList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
@rename="$emit('rename', item)"
@download="$emit('download', item)"
@move="$emit('move', item)"
@move-direct-to="$emit('moveDirectTo', $event)"
@edit="$emit('edit', item)"
@contextmenu="(x, y) => $emit('contextmenu', item, x, y)"
/>
Expand All @@ -55,6 +56,7 @@ const emit = defineEmits<{
(e: "edit", item: any): void;
(e: "contextmenu", item: any, x: number, y: number): void;
(e: "loadMore"): void;
(e: "moveDirectTo", item: any): void;
}>();

const ITEM_HEIGHT = 61;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
:options="[
{ id: 'normal', action: () => $emit('sort', 'default') },
{ id: 'modified', action: () => $emit('sort', 'modified') },
{ id: 'created', action: () => $emit('sort', 'created') },
{ id: 'filesOnly', action: () => $emit('sort', 'filesOnly') },
{ id: 'foldersOnly', action: () => $emit('sort', 'foldersOnly') },
]"
Expand All @@ -91,11 +92,12 @@
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" />
<template #normal> Alphabetical </template>
<template #modified> Date modified </template>
<template #created> Date created </template>
<template #filesOnly> Files only </template>
<template #foldersOnly> Folders only </template>
</UiServersTeleportOverflowMenu>
</ButtonStyled>
<div class="mx-1 w-full text-sm sm:w-40">
<div class="mx-1 w-full text-sm sm:w-48">
<label for="search-folder" class="sr-only">Search folder</label>
<div class="relative">
<SearchIcon
Expand Down Expand Up @@ -183,6 +185,8 @@ const sortMethodLabel = computed(() => {
switch (props.sortMethod) {
case "modified":
return "Date modified";
case "created":
return "Date created";
case "filesOnly":
return "Files only";
case "foldersOnly":
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/src/components/ui/servers/FilesContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
id="item-context-menu"
ref="ctxRef"
:style="{
border: '1px solid var(--color-button-bg)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-divider)',
borderRadius: 'var(--radius-lg)',
backgroundColor: 'var(--color-raised-bg)',
padding: 'var(--gap-sm)',
boxShadow: 'var(--shadow-floating)',
Expand All @@ -31,7 +31,7 @@
Rename
</button>
<button class="btn btn-transparent flex !w-full items-center" @click="$emit('move', item)">
<ArrowBigUpDashIcon class="h-5 w-5" />
<RightArrowIcon />
Move
</button>
<button
Expand All @@ -55,7 +55,7 @@
</template>

<script setup lang="ts">
import { EditIcon, ArrowBigUpDashIcon, DownloadIcon, TrashIcon } from "@modrinth/assets";
import { EditIcon, DownloadIcon, TrashIcon, RightArrowIcon } from "@modrinth/assets";

interface FileItem {
type: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<NewModal ref="modal" :header="`Creating a ${type}`">
<NewModal ref="modal" :header="`Creating a ${displayType}`">
<form class="flex flex-col gap-4 md:w-[600px]" @submit.prevent="handleSubmit">
<div class="flex flex-col gap-2">
<div class="font-semibold text-contrast">Name</div>
Expand All @@ -18,7 +18,7 @@
<ButtonStyled color="brand">
<button :disabled="!!error" type="submit">
<PlusIcon class="h-5 w-5" />
Create
Create {{ displayType }}
</button>
</ButtonStyled>
<ButtonStyled>
Expand Down Expand Up @@ -46,6 +46,7 @@ const emit = defineEmits<{
}>();

const modal = ref<typeof NewModal>();
const displayType = computed(() => (props.type === "directory" ? "folder" : props.type));
const createInput = ref<HTMLInputElement | null>(null);
const itemName = ref("");
const submitted = ref(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
>
<nav
aria-label="Breadcrumb navigation"
class="m-0 flex list-none items-center p-0 text-contrast"
class="m-0 flex min-w-0 flex-shrink items-center p-0 text-contrast"
>
<ol class="m-0 flex list-none items-center p-0">
<li class="-ml-1">
<ol class="m-0 flex min-w-0 flex-shrink list-none items-center p-0">
<li class="-ml-1 flex-shrink-0">
<ButtonStyled type="transparent">
<button
v-tooltip="'Back to home'"
type="button"
class="grid h-12 w-10 place-content-center focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
class="mr-2 grid h-12 w-10 place-content-center focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
@click="goHome"
>
<span
Expand Down
Loading
Loading