Pyro Integration (#2503)

* fix

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor(fileitem): optimize

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore(fileitem): fixed width timestamp

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(fileitem): allow editing json5/jsonc

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: motd pt 1, auto backups scaffolding, editing navbar changes

* feat: fancy sidebar animations

* fix: files

* fix: files pt2

* fix: faulty name validation disallowing spaces in file names

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: fileitem props

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: upload files not refreshing files list

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(imgviewer): handle invalid/empty images

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: return of the sticky files header

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: prevent servericon from shrinking

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: wtf were we thinking with this anyway

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: further mobile optimization

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: propagate margin

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: truncation fixes

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: track navbar with sentinel

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(files): a11y

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: improve inspector styles

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: console preformance improvements, decrease blur

* feat(mobile): new server header

* fix: linting

* fix: useless z indeces

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust file filter names

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(files): true breadcrumbs

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(marketing): make custom responsive

* fix(marketing): mobile file manager card

* feat: trackable navtabs

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: oh no

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: smartly truncate

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(terminal): z-indexes

* fix: autofocus more inputs

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: color

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust copy

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: backup modal usability improvements

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: padding

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: title

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(content): update banner mobile support

* fix: server listing icons

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: ignore clicks in server listing for labels

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(mobile): backup card

* fix(backups): make plural conditional

* fix: debounce file item selectitem

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: lint

Signed-off-by: Evan Song <theevansong@gmail.com>

* stuff

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: temp sidebar fix until i can be smart

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: explictly set button type in file modals

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: properly sort backups

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: add getautobackup method to pyroservers

Signed-off-by: Evan Song <theevansong@gmail.com>

* choer: update autobackup params

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: update autobackup methods (REALLY GUYS)

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: implement autobackups

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: implement backup-while-running preference

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: make server labels a component

* feat: implement 'All details' modal

* fix(mobile): server manage page

* feat(files): mobile compatible

* fix(info labels): wrap

* chore(inspector): clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(backup settings): swap + and -

* fix(manage): new -> plans instead of modal

* feat: more small mobile fixes

* fix(auto backup modal): manual input validation

* fix(file browse navbar): home margin

* feat(purchase modal): mobile support

* fix(marketing): faded line alignments

* feat: add servers to mobile nav

* feat(network): dns record fixes

* feat: make all settings work on mobile

* fix(loader settings): modpack mobile

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(marketing): add 'Manage your servers' button

* fix(marketing): only check servers if logged in

* fix(network): allocation edit & delete button

* fix(backups): use UiServersTeleportOverflowMenu

* chore: linting

* chore: but here comes the sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(marketing): make buttons consistent

* lint

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(loader): prevent multiline version names in dropdown

Signed-off-by: Evan Song <theevansong@gmail.com>

* lint

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: copy

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: linting

* chore: rename dumbass preference key

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: rewrite power action buttons

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: robust download logic

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(loader mobile): modpack dropdown width

* fix: sentence case

* fix(save & 'working on it'): look good on mobile

* fix(TeleportDropdown): width

* fix(inspecting error): mobile

* fix: show action button dropdown when installing

* fix(navtabs): temp fix for mobile scrolling issue

* fix(install error): mobile compatible

* chore: just remove tracking

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: cleanup

* fix: broken svg clr in checkbox when using experimental styles

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust vanilla icon

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust loader props

Signed-off-by: Evan Song <theevansong@gmail.com>

* revert changes to serversidebar

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: server properties flicker

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(backups): plural

* fix: cases where the telepoverflow would clash with viewport edge

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(backups): auto-backups label

* fix(network): titlecase

* feat(fileitem): new rename icon

* fix(properties): wiki proper noun

* fix: disable motd for the time being

* chore: adjust wording for power conifmration

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: "external" to billing

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: icon

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: add EULA checkbox

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* me and bro deciding which case rules to enforce

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(sftp): copy address & username, launch tooltip

* feat(files): better move

* chore: attempt to mitigate excessive stack depth type

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(loader): prevent versions 1.2.4 and below

* feat(dns table): placeholder improvements

* feat(pyroServer): error handling

* fix: intrinsic size on loader icon

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust wording

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: sentence case

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust wording

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: types

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: "implemented" key in preference

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat(connection lost): redesign

* feat(connection error): make icon orange

* fix: cleanup

* chore(connection lost): redesign pt 2

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: OOOOHHH MY GOD

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: implement capacity api on marketing

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: update createdat backup type

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: all of backups

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: update backup types

Signed-off-by: Evan Song <theevansong@gmail.com>

* refactor: backups pt 2

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: comically small icons

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: align designs

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: hide ram graph if ram as bytes enabled

Signed-off-by: Evan Song <theevansong@gmail.com>

* base add content page

* Fix conflict

* feat(content): mobile-compatible header, sticky

* fix(marketing): md instead of sm for custom

* fix: compiler macro warning

Signed-off-by: Evan Song <theevansong@gmail.com>

* again

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: loader type error

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: default uptime seconds prop

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: hydration errors on server listing

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: move custom URL to general

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: indiviudally checkj capacities

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: falsey

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: missing prop

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: Derive On That Thang

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: adjust gap

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: add default name for backups

* fix: the backup number should PROBABLY be computed lol

* fix(backups): truncate text, mobile fixes

* fix(loader): modpack mobile fix

* feat(plans): add vcpus

* fix(backup modal): blank by default, maxlength

* fix(subdomain): separate length & valid chars

* feat: mrpack installs functionality (untested), forbidden handling, backups grammar

* feat(content): make responsive on mobile

* fix: disable plan buttons separately

* fix(backup modal): update name max length

* fix(purchase): wrapping on eula, eula link

* fix: move skeleton

* fix(server mobile header): truncation

* fix(server header): proper alignment

* Finish content page fixes

* fix: who up rinthing

Signed-off-by: Evan Song <theevansong@gmail.com>

* wip

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(staging & email banner): z-index

* feat: make eula tickbox more visible

* fix: move "powered by pyro" below buttons on hero

* fix: oops sorry ellie, also updated the main screenshot

* feat: update content screenshot

* fix: content page card should hide image on lg

* feat: hide total storage for now

* fix: terminal card now uses terminal icon

* fix(marketing): make medium plan card border solid

* feat: modloader card, move pyro BACK below buttons, beta release pill

* fix: spinning logo should be behind hero

* feat: surgically remove the hero's massive forehead

* feat(marketing): mobile UI screenshot

* fix(hero): z-index goes over mobile nav

* fix: consistent borders, files breakpoints

* chore: update turbo

* chore: adjust hero sizing

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: mention region restrictions

* chore: double check if we are at capcity

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: measure twice cut once

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: bro cut twice and measured once 💀

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(marketing): login first

* fix: out of capacity text when logged out

* fix(slider): reset some values for frontend

* feat: wip hero section

Signed-off-by: Evan Song <theevansong@gmail.com>

* New navigation to support the new products (#2879)

* Nav

* oops extra file

* feat: mrpack uploading with existing modpack, fix: choose modpack duplicate

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: update features section

Signed-off-by: Evan Song <theevansong@gmail.com>

* Nav adjustments

* fix: server manager empty state clashing with loading state

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: query param hard

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: do not count uptime if crashed

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: grammar

Signed-off-by: Evan Song <theevansong@gmail.com>

* hide hero img on lg breakpoints

* Make plugins a plug

* chore: prep for buffered text selection terminal

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: marketing responsive stuff, n fixes

* fix hoverable prop

* fix: edit mod spacing

* fix: type error for display name in dropdown

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: custom plans

* fix: no more console.log

* fix: properly linked prop label

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix(install hero mobile): padding

* fix: prevent x overflow on servers page

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix lint oh ym fucking god yal

Signed-off-by: Evan Song <theevansong@gmail.com>

* Migrate modpack install to search

* fix(custom plan): warning icon variable

* fix: loading probally and modal loader things

* fix(marketing): login icon colours

* fix(marketing): responsiveness

* fix(marketing): responsiveness v2

* fix: sync button for icon tm

* fix(marketing): responsiveness v3

* fix: hero image

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: clean

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: switch to cdn links

Signed-off-by: Evan Song <theevansong@gmail.com>

* Remove prod override

---------

Signed-off-by: Evan Song <theevansong@gmail.com>
Co-authored-by: Evan Song <theevansong@gmail.com>
Co-authored-by: TheWander02 <48934424+thewander02@users.noreply.github.com>
Co-authored-by: he3als <65787561+he3als@users.noreply.github.com>
Co-authored-by: Evan Song <52982404+ferothefox@users.noreply.github.com>
Co-authored-by: Lio <git@lio.cat>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: not-nullptr <needhelpwithrift@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: sticks <tanner@teamhydra.dev>
This commit is contained in:
Elizabeth
2024-11-02 23:14:00 -05:00
committed by GitHub
parent f165665a35
commit 185dd47668
126 changed files with 19390 additions and 432 deletions
@@ -0,0 +1,736 @@
<template>
<div data-pyro-file-manager-root class="contents">
<LazyUiServersFilesCreateItemModal
ref="createItemModal"
:type="newItemType"
@create="handleCreateNewItem"
/>
<LazyUiServersFilesRenameItemModal
ref="renameItemModal"
:item="selectedItem"
@rename="handleRenameItem"
/>
<LazyUiServersFilesMoveItemModal
ref="moveItemModal"
:item="selectedItem"
:current-path="currentPath"
@move="handleMoveItem"
/>
<LazyUiServersFilesDeleteItemModal
ref="deleteItemModal"
:item="selectedItem"
@delete="handleDeleteItem"
/>
<div
class="relative flex w-full flex-col rounded-2xl border border-solid border-bg-raised"
@dragenter.prevent="handleDragEnter"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
>
<div ref="mainContent" class="relative isolate flex w-full flex-col">
<UiServersFilesBrowseNavbar
v-if="!isEditing"
:breadcrumb-segments="breadcrumbSegments"
:search-query="searchQuery"
:sort-method="sortMethod"
@navigate="navigateToSegment"
@sort="sortFiles"
@create="showCreateModal"
@upload="initiateFileUpload"
@update:search-query="searchQuery = $event"
/>
<UiServersFilesEditingNavbar
v-else
:file-name="editingFile?.name"
:is-image="isEditingImage"
:file-path="editingFile?.path"
:breadcrumb-segments="breadcrumbSegments"
@cancel="cancelEditing"
@save="() => saveFileContent(true)"
@save-as="saveFileContentAs"
@save-restart="saveFileContentRestart"
@share="requestShareLink"
@navigate="navigateToSegment"
/>
<div v-if="isEditing" class="h-full w-full flex-grow">
<component
:is="VAceEditor"
v-if="!isEditingImage"
v-model:value="fileContent"
lang="json"
theme="one_dark"
:print-margin="false"
style="height: 750px; font-size: 1rem"
class="ace_editor ace_hidpi ace-one-dark ace_dark rounded-b-lg"
@init="onInit"
/>
<UiServersFilesImageViewer v-else :image-blob="imagePreview" />
</div>
<div v-else-if="items.length > 0" class="h-full w-full overflow-hidden rounded-b-2xl">
<UiServersFileVirtualList
:items="filteredItems"
@delete="showDeleteModal"
@rename="showRenameModal"
@download="downloadFile"
@move="showMoveModal"
@edit="editFile"
@contextmenu="showContextMenu"
@load-more="handleLoadMore"
/>
</div>
<div
v-else-if="!isLoading && items.length === 0"
class="flex h-full w-full items-center justify-center p-20"
>
<div class="flex flex-col items-center gap-4 text-center">
<FolderOpenIcon class="h-16 w-16 text-secondary" />
<h3 class="text-2xl font-bold text-contrast">This folder is empty</h3>
<p class="m-0 text-sm text-secondary">There are no files or folders.</p>
</div>
</div>
<LazyUiServersFileManagerError
v-else-if="!isLoading"
title="Unable to list files"
message="Unfortunately, we were unable to list the files in this folder. If this issue persists, contact support."
@refetch="refreshList"
@home="navigateToSegment(-1)"
/>
<LazyUiServersFileManagerError
v-else-if="loadError"
title="Unable to fetch files"
message="This path is invalid or the server is not responding."
@refetch="refreshList"
@home="navigateToSegment(-1)"
/>
</div>
<div
v-if="isDragging"
class="absolute inset-0 flex items-center justify-center rounded-xl bg-black bg-opacity-50 text-white"
>
<div class="text-center">
<UploadIcon class="mx-auto h-16 w-16" />
<p class="mt-2 text-xl">Drop files here to upload</p>
</div>
</div>
</div>
<UiServersFilesContextMenu
ref="contextMenu"
:item="contextMenuInfo.item"
:x="contextMenuInfo.x"
:y="contextMenuInfo.y"
:is-at-bottom="isAtBottom"
@rename="showRenameModal"
@move="showMoveModal"
@download="downloadFile"
@delete="showDeleteModal"
/>
</div>
</template>
<script setup lang="ts">
import { useInfiniteScroll } from "@vueuse/core";
import { UploadIcon, FolderOpenIcon } from "@modrinth/assets";
import type { Server } from "~/composables/pyroServers";
const props = defineProps<{
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
}>();
const route = useRoute();
const router = useRouter();
const VAceEditor = ref();
const mainContent = ref<HTMLElement | null>(null);
const scrollContainer = ref<HTMLElement | null>(null);
const contextMenu = ref();
const searchQuery = ref("");
const sortMethod = ref("default");
const maxResults = 100;
const currentPage = ref(1);
const currentPath = ref(typeof route.query.path === "string" ? route.query.path : "");
const isAtBottom = ref(false);
const contextMenuInfo = ref<any>({ item: null, x: 0, y: 0 });
const createItemModal = ref();
const renameItemModal = ref();
const moveItemModal = ref();
const deleteItemModal = ref();
const newItemType = ref<"file" | "directory">("file");
const selectedItem = ref<any>(null);
const fileContent = ref("");
const isEditing = ref(false);
const editingFile = ref<any>(null);
const closeEditor = ref(false);
const isEditingImage = ref(false);
const imagePreview = ref();
const isDragging = ref(false);
const dragCounter = ref(0);
const data = computed(() => props.server.general);
useHead({
title: computed(() => `Files - ${data.value?.name ?? "Server"} - Modrinth`),
});
const fetchDirectoryContents = async (): Promise<{ items: any[]; total: number }> => {
const path = Array.isArray(currentPath.value) ? currentPath.value.join("") : currentPath.value;
try {
const data = await props.server.fs?.listDirContents(path, currentPage.value, maxResults);
if (!data || !data.items) {
throw new Error("Invalid data structure received from server.");
}
if (currentPage.value === 1) {
return {
items: applyDefaultSort(data.items),
total: data.total,
};
}
return {
items: [...(directoryData.value?.items || []), ...applyDefaultSort(data.items)],
total: data.total,
};
} catch (error) {
console.error("Error fetching directory contents:", error);
if (error instanceof PyroFetchError && error.statusCode === 400) {
return directoryData.value || { items: [], total: 0 };
}
throw error;
}
};
const {
data: directoryData,
refresh: refreshData,
status,
error: loadError,
} = useLazyAsyncData(() => fetchDirectoryContents(), {
watch: [],
default: () => ({ items: [], total: 0 }),
immediate: true,
});
const isLoading = computed(() => status.value === "pending");
const items = computed(() => directoryData.value?.items || []);
const refreshList = () => {
currentPage.value = 1;
refreshData();
reset();
};
const handleCreateNewItem = async (name: string) => {
try {
const path = `${currentPath.value}/${name}`.replace("//", "/");
await props.server.fs?.createFileOrFolder(path, newItemType.value);
refreshList();
addNotification({
group: "files",
title: "File created",
text: "Your file has been created.",
type: "success",
});
} catch (error) {
handleCreateError(error);
}
};
const handleRenameItem = async (newName: string) => {
try {
const path = `${currentPath.value}/${selectedItem.value.name}`.replace("//", "/");
await props.server.fs?.renameFileOrFolder(path, newName);
refreshList();
if (closeEditor.value) {
await props.server.refresh();
isEditing.value = false;
editingFile.value = null;
closeEditor.value = false;
router.push({ query: { ...route.query, path: currentPath.value } });
}
addNotification({
group: "files",
title: "File renamed",
text: "Your file has been renamed.",
type: "success",
});
} catch (error) {
handleRenameError(error);
}
};
const handleMoveItem = async (destination: string) => {
try {
await props.server.fs?.moveFileOrFolder(
`${currentPath.value}/${selectedItem.value.name}`.replace("//", "/"),
`${destination}/${selectedItem.value.name}`.replace("//", "/"),
);
refreshList();
addNotification({
group: "files",
title: "File moved",
text: "Your file has been moved.",
type: "success",
});
} catch (error) {
console.error("Error moving item:", error);
}
};
const handleDeleteItem = async () => {
try {
const path = `${currentPath.value}/${selectedItem.value.name}`.replace("//", "/");
await props.server.fs?.deleteFileOrFolder(path, selectedItem.value.type === "directory");
refreshList();
addNotification({
group: "files",
title: "File deleted",
text: "Your file has been deleted.",
type: "success",
});
} catch (error) {
console.error("Error deleting item:", error);
}
};
const showCreateModal = (type: "file" | "directory") => {
newItemType.value = type;
createItemModal.value?.show();
};
const showRenameModal = (item: any) => {
selectedItem.value = item;
renameItemModal.value?.show(item);
contextMenuInfo.value.item = null;
};
const showMoveModal = (item: any) => {
selectedItem.value = item;
moveItemModal.value?.show();
contextMenuInfo.value.item = null;
};
const showDeleteModal = (item: any) => {
selectedItem.value = item;
deleteItemModal.value?.show();
contextMenuInfo.value.item = null;
};
const handleCreateError = (error: any) => {
console.error("Error creating item:", error);
if (error instanceof PyroFetchError) {
if (error.statusCode === 400) {
addNotification({
group: "files",
title: "Error creating item",
text: "Invalid file",
type: "error",
});
} else if (error.statusCode === 500) {
addNotification({
group: "files",
title: "Error creating item",
text: "File already exists",
type: "error",
});
}
}
};
const handleRenameError = (error: any) => {
console.error("Error renaming item:", error);
if (error instanceof PyroFetchError) {
if (error.statusCode === 400) {
addNotification({
group: "files",
title: "Could not rename item",
text: "This item already exists or is invalid.",
type: "error",
});
} else if (error.statusCode === 500) {
addNotification({
group: "files",
title: "Could not rename item",
text: "Invalid file",
type: "error",
});
}
}
};
const applyDefaultSort = (items: any[]) => {
return items.sort((a: any, b: any) => {
if (a.type === "directory" && b.type !== "directory") return -1;
if (a.type !== "directory" && b.type === "directory") return 1;
if (a.count > b.count) return -1;
if (a.count < b.count) return 1;
return a.name.localeCompare(b.name);
});
};
const filteredItems = computed(() => {
let result = [...items.value];
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
result = result.filter((item) => item.name.toLowerCase().includes(query));
}
switch (sortMethod.value) {
case "modified":
result.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
break;
case "filesOnly":
result = result.filter((item) => item.type !== "directory");
break;
case "foldersOnly":
result = result.filter((item) => item.type === "directory");
break;
default:
result = applyDefaultSort(result);
}
return result;
});
const { reset } = useInfiniteScroll(
scrollContainer,
async () => {
if (status.value === "pending") return;
try {
const totalPages = directoryData.value?.total || 0;
if (currentPage.value < totalPages) {
currentPage.value++;
const newData = await fetchDirectoryContents();
if (newData && newData.items) {
directoryData.value = {
items: [...directoryData.value.items, ...newData.items],
total: newData.total,
};
}
}
} catch (error) {
console.error("Error during infinite scroll:", error);
}
},
{ distance: 1000 },
);
const handleLoadMore = async () => {
if (status.value === "pending") return;
const totalPages = directoryData.value?.total || 0;
if (currentPage.value < totalPages) {
currentPage.value++;
await refreshData();
}
};
const onInit = (editor: any) => {
editor.commands.addCommand({
name: "saveFile",
bindKey: { win: "Ctrl-S", mac: "Command-S" },
exec: () => saveFileContent(false),
});
};
const showContextMenu = async (item: any, x: number, y: number) => {
contextMenuInfo.value = { item, x, y };
selectedItem.value = item;
await nextTick();
if (!contextMenu.value?.ctxRef) return false;
const screenHeight = window.innerHeight;
const ctxRect = contextMenu.value.ctxRef.getBoundingClientRect();
isAtBottom.value = ctxRect.bottom > screenHeight;
};
const onAnywhereClicked = (e: MouseEvent) => {
if (!(e.target as HTMLElement).closest("#item-context-menu")) {
contextMenuInfo.value.item = null;
}
};
const sortFiles = (method: string) => {
sortMethod.value = method;
};
const imageExtensions = ["png", "jpg", "jpeg", "gif", "webp"];
const editFile = async (item: { name: string; type: string; path: string }) => {
try {
const path = `${currentPath.value}/${item.name}`.replace("//", "/");
const content = (await props.server.fs?.downloadFile(path, true)) as any;
window.scrollTo(0, 0);
fileContent.value = await content.text();
editingFile.value = item;
isEditing.value = true;
const extension = item.name.split(".").pop();
if (item.type === "file" && extension && imageExtensions.includes(extension)) {
isEditingImage.value = true;
imagePreview.value = content;
}
router.push({ query: { ...route.query, path: currentPath.value, editing: item.path } });
} catch (error) {
console.error("Error fetching file content:", error);
}
};
onMounted(async () => {
await import("ace-builds");
await import("ace-builds/src-noconflict/mode-json");
await import("ace-builds/src-noconflict/theme-one_dark");
VAceEditor.value = markRaw((await import("vue3-ace-editor")).VAceEditor);
document.addEventListener("click", onAnywhereClicked);
window.addEventListener("scroll", onScroll);
});
onUnmounted(() => {
document.removeEventListener("click", onAnywhereClicked);
window.removeEventListener("scroll", onScroll);
});
watch(
() => route.query,
async (newQuery) => {
currentPage.value = 1;
searchQuery.value = "";
sortMethod.value = "default";
currentPath.value = Array.isArray(newQuery.path)
? newQuery.path.join("")
: newQuery.path || "/";
if (newQuery.editing) {
await editFile({
name: newQuery.editing as string,
type: "file",
path: newQuery.editing as string,
});
} else {
isEditing.value = false;
editingFile.value = null;
}
await refreshData();
reset();
},
{ immediate: true, deep: true },
);
const breadcrumbSegments = computed(() => {
if (typeof currentPath.value === "string") {
return currentPath.value.split("/").filter(Boolean);
}
return [];
});
const navigateToSegment = (index: number) => {
const newPath = breadcrumbSegments.value.slice(0, index + 1).join("/");
router.push({ query: { ...route.query, path: newPath } });
if (isEditing.value) {
isEditing.value = false;
editingFile.value = null;
closeEditor.value = false;
const newQuery = { ...route.query };
delete newQuery.editing;
router.replace({ query: newQuery });
}
};
// const navigateToPage = () => {
// router.push({ query: { path: currentPath.value } });
// };
const requestShareLink = async () => {
try {
const response = (await $fetch("https://api.mclo.gs/1/log", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ content: fileContent.value }),
})) as any;
if (response.success) {
await navigator.clipboard.writeText(response.url);
addNotification({
group: "files",
title: "Log URL copied",
text: "Your log file URL has been copied to your clipboard.",
type: "success",
});
} else {
throw new Error(response.error);
}
} catch (error) {
console.error("Error sharing file:", error);
}
};
const handleDragEnter = (event: DragEvent) => {
if (isEditing.value) return;
event.preventDefault();
dragCounter.value++;
isDragging.value = true;
};
const handleDragOver = (event: DragEvent) => {
if (isEditing.value) return;
event.preventDefault();
};
const handleDragLeave = (event: DragEvent) => {
if (isEditing.value) return;
event.preventDefault();
dragCounter.value--;
if (dragCounter.value === 0) {
isDragging.value = false;
}
};
const handleDrop = async (event: DragEvent) => {
if (isEditing.value) return;
event.preventDefault();
isDragging.value = false;
dragCounter.value = 0;
const files = event.dataTransfer?.files;
if (files) {
for (let i = 0; i < files.length; i++) {
await uploadFile(files[i]);
}
}
};
const uploadFile = async (file: File) => {
try {
const filePath = `${currentPath.value}/${file.name}`.replace("//", "/");
await props.server.fs?.uploadFile(filePath, file);
refreshList();
addNotification({
group: "files",
title: "File uploaded",
text: "Your file has been uploaded.",
type: "success",
});
} catch (error) {
console.error("Error uploading file:", error);
}
};
const initiateFileUpload = () => {
const input = document.createElement("input");
input.type = "file";
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
await uploadFile(file);
}
};
input.click();
};
const downloadFile = async (item: any) => {
if (item.type === "file") {
try {
const path = `${currentPath.value}/${item.name}`.replace("//", "/");
const fileData = await props.server.fs?.downloadFile(path);
if (fileData) {
const blob = new Blob([fileData], { type: "application/octet-stream" });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = item.name;
link.click();
window.URL.revokeObjectURL(link.href);
} else {
throw new Error("File data is undefined");
}
} catch (error) {
console.error("Error downloading file:", error);
}
contextMenuInfo.value.item = null;
}
};
const saveFileContent = async (exit: boolean = true) => {
if (!editingFile.value) return;
try {
await props.server.fs?.updateFile(editingFile.value.path, fileContent.value);
if (exit) {
await props.server.refresh();
isEditing.value = false;
editingFile.value = null;
router.push({ query: { ...route.query, path: currentPath.value } });
}
addNotification({
group: "files",
title: "File saved",
text: "Your file has been saved.",
type: "success",
});
} catch (error) {
console.error("Error saving file content:", error);
}
};
const saveFileContentRestart = async () => {
await saveFileContent();
await props.server.general?.power("Restart");
};
const saveFileContentAs = async () => {
await saveFileContent(false);
closeEditor.value = true;
showRenameModal(editingFile.value);
};
const cancelEditing = () => {
isEditing.value = false;
editingFile.value = null;
fileContent.value = "";
isEditingImage.value = false;
imagePreview.value = null;
router.push({ query: { ...route.query, path: currentPath.value } });
const newQuery = { ...route.query };
delete newQuery.editing;
router.replace({ query: newQuery });
};
const onScroll = () => {
if (contextMenuInfo.value.item) {
contextMenuInfo.value.y = Math.max(0, contextMenuInfo.value.y - window.scrollY);
}
};
</script>