feat(servers content): file upload + extra mod info + misc (#3055)

* feat: only scroll up if scrolled down

* feat: no query results message

* feat: content files support, mobile fixes

* fix(drag & drop): type of file prop

* chore: show number of mods in searchbar

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

* chore: adjust btn styles

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

* feat: prepare for mod author in backend response

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

* fix: external mods & mobile

* chore: adjust edit mod version modal copy

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

* chore: add tooltips for version/filename

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

* chore: swap delete/change version btn

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

* fix: dont allow mod link to be dragged

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

* fix: oops

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

* chore: remove author field

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

* chore: drill down tooltip

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

* fix: fighting types

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

* prepare for owner field

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

---------

Signed-off-by: Evan Song <theevansong@gmail.com>
Co-authored-by: Evan Song <theevansong@gmail.com>
Co-authored-by: Evan Song <52982404+ferothefox@users.noreply.github.com>
This commit is contained in:
he3als
2024-12-29 00:31:52 +00:00
committed by GitHub
parent 2fea772ffb
commit 0437503b75
7 changed files with 696 additions and 457 deletions
@@ -15,12 +15,11 @@
</div>
</div>
<div>
<div v-if="props.server.general?.upstream" class="flex items-center gap-2">
<div v-if="props.server.general?.upstream" class="flex gap-2">
<InfoIcon class="hidden sm:block" />
<span class="text-sm text-secondary">
Your server was created from a modpack. Changing the mod version may cause unexpected
issues. You can update the modpack version in your server's Options > Platform
settings.
Changing the mod version may cause unexpected issues. Because your server was created
from a modpack, it is recommended to use the modpack's version of the mod.
</span>
</div>
</div>
@@ -57,9 +56,9 @@
<div v-if="server.general && localMods" class="relative isolate flex h-full w-full flex-col">
<div ref="pyroContentSentinel" class="sentinel" data-pyro-content-sentinel />
<div class="relative flex h-full w-full flex-col">
<div class="sticky top-0 z-20 -mt-4 flex items-center justify-between bg-bg py-4">
<div class="flex w-full flex-col items-center gap-2 sm:flex-row sm:gap-4">
<div class="flex w-full items-center gap-2 sm:gap-4">
<div class="sticky top-0 z-20 -mt-3 flex items-center justify-between bg-bg py-3">
<div class="flex w-full flex-col-reverse items-center gap-2 sm:flex-row">
<div class="flex w-full items-center gap-2">
<div class="relative flex-1 text-sm">
<label class="sr-only" for="search">Search</label>
<SearchIcon
@@ -73,7 +72,7 @@
type="search"
name="search"
autocomplete="off"
:placeholder="`Search ${type.toLocaleLowerCase()}s...`"
:placeholder="`Search ${localMods.length} ${type.toLocaleLowerCase()}s...`"
@input="debouncedSearch"
/>
</div>
@@ -88,7 +87,7 @@
{ id: 'disabled', action: () => (filterMethod = 'disabled') },
]"
>
<span class="whitespace-pre text-sm font-medium">
<span class="hidden whitespace-pre sm:block">
{{ filterMethodLabel }}
</span>
<FilterIcon aria-hidden="true" />
@@ -99,45 +98,71 @@
</UiServersTeleportOverflowMenu>
</ButtonStyled>
</div>
<ButtonStyled v-if="hasMods" color="brand" type="outlined">
<nuxt-link
class="w-full text-nowrap sm:w-fit"
:to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`"
>
<PlusIcon />
Add {{ type.toLocaleLowerCase() }}
</nuxt-link>
</ButtonStyled>
<div v-if="hasMods" class="flex w-full items-center gap-2 sm:w-fit">
<ButtonStyled>
<button class="w-full text-nowrap sm:w-fit" @click="initiateFileUpload">
<FileIcon />
Add file
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<nuxt-link
class="w-full text-nowrap sm:w-fit"
:to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`"
>
<PlusIcon />
Add {{ type.toLocaleLowerCase() }}
</nuxt-link>
</ButtonStyled>
</div>
</div>
</div>
<div v-if="hasMods" class="flex flex-col gap-2 transition-all">
<div ref="listContainer" class="relative w-full">
<div :style="{ position: 'relative', height: `${totalHeight}px` }">
<div :style="{ position: 'absolute', top: `${visibleTop}px`, width: '100%' }">
<template v-for="mod in visibleItems.items" :key="mod.filename">
<div
class="relative mb-2 flex w-full items-center justify-between rounded-xl bg-bg-raised"
:class="mod.disabled ? 'bg-table-alternateRow text-secondary' : ''"
style="height: 64px"
>
<NuxtLink
:to="
mod.project_id
? `/project/${mod.project_id}/version/${mod.version_id}`
: `files?path=mods`
"
class="group flex min-w-0 items-center rounded-xl p-2"
<FilesUploadDropdown
v-if="props.server.fs"
ref="uploadDropdownRef"
class="rounded-xl bg-bg-raised"
:margin-bottom="16"
:file-type="type"
:current-path="'/mods'"
:fs="props.server.fs"
:accepted-types="acceptFileFromProjectType(type.toLocaleLowerCase()).split(',')"
@upload-complete="() => props.server.refresh(['content'])"
/>
<FilesUploadDragAndDrop
v-if="server.general && localMods"
class="relative min-h-[50vh]"
overlay-class="rounded-xl border-2 border-dashed border-secondary"
:type="type"
@files-dropped="handleDroppedFiles"
>
<div v-if="hasFilteredMods" class="flex flex-col gap-2 transition-all">
<div ref="listContainer" class="relative w-full">
<div :style="{ position: 'relative', height: `${totalHeight}px` }">
<div :style="{ position: 'absolute', top: `${visibleTop}px`, width: '100%' }">
<template v-for="mod in visibleItems.items" :key="mod.filename">
<div
class="relative mb-2 flex w-full items-center justify-between rounded-xl bg-bg-raised"
:class="mod.disabled ? 'bg-table-alternateRow text-secondary' : ''"
style="height: 64px"
>
<div class="flex min-w-0 items-center gap-2">
<NuxtLink
:to="
mod.project_id
? `/project/${mod.project_id}/version/${mod.version_id}`
: `files?path=mods`
"
class="flex min-w-0 flex-1 items-center gap-2 rounded-xl p-2"
draggable="false"
>
<UiAvatar
:src="mod.icon_url"
size="sm"
alt="Server Icon"
:class="mod.disabled ? 'grayscale' : ''"
:class="mod.disabled ? 'opacity-75 grayscale' : ''"
/>
<div class="flex min-w-0 flex-col">
<span class="flex min-w-0 items-center gap-2 text-lg font-bold">
<span class="truncate">{{
<div class="flex min-w-0 flex-col gap-1">
<span class="text-md flex min-w-0 items-center gap-2 font-bold">
<span class="truncate text-contrast">{{
mod.name || mod.filename.replace(".disabled", "")
}}</span>
<span
@@ -146,132 +171,180 @@
>Disabled</span
>
</span>
<span class="min-w-0 text-xs text-secondary">{{
<div class="min-w-0 text-xs text-secondary">
<span v-if="mod.owner" class="hidden sm:block"> by {{ mod.owner }} </span>
<span class="block font-semibold sm:hidden">
{{ mod.version_number || "External mod" }}
</span>
</div>
</div>
</NuxtLink>
<div class="ml-2 hidden min-w-0 flex-1 flex-col text-sm sm:flex">
<div class="truncate font-semibold text-contrast">
<span v-tooltip="'Mod version'">{{
mod.version_number || "External mod"
}}</span>
</div>
<div class="truncate">
<span v-tooltip="'Mod file name'">{{ mod.filename }}</span>
</div>
</div>
</NuxtLink>
<div class="flex items-center gap-2 pr-4 font-semibold text-contrast">
<ButtonStyled v-if="mod.project_id" type="transparent">
<button
v-tooltip="'Edit mod version'"
:disabled="mod.changing"
class="!hidden sm:!block"
@click="beginChangeModVersion(mod)"
>
<template v-if="mod.changing">
<UiServersIconsLoadingIcon />
</template>
<template v-else>
<EditIcon />
</template>
</button>
</ButtonStyled>
<ButtonStyled type="transparent">
<button
v-tooltip="'Delete mod'"
:disabled="mod.changing"
class="!hidden sm:!block"
@click="removeMod(mod)"
>
<TrashIcon />
</button>
</ButtonStyled>
<!-- Dropdown for mobile -->
<div class="mr-2 flex items-center sm:hidden">
<UiServersIconsLoadingIcon
v-if="mod.changing"
class="mr-2 h-5 w-5 animate-spin"
style="color: var(--color-base)"
/>
<ButtonStyled v-else circular type="transparent">
<UiServersTeleportOverflowMenu
:options="[
{
id: 'edit',
action: () => beginChangeModVersion(mod),
shown: !!(mod.project_id && !mod.changing),
},
{
id: 'delete',
action: () => removeMod(mod),
},
]"
<div
class="flex items-center justify-end gap-2 pr-4 font-semibold text-contrast sm:min-w-44"
>
<ButtonStyled color="red" type="transparent">
<button
v-tooltip="'Delete mod'"
:disabled="mod.changing"
class="!hidden sm:!block"
@click="removeMod(mod)"
>
<MoreVerticalIcon aria-hidden="true" />
<template #edit>
<EditIcon class="h-5 w-5" />
<span>Edit</span>
</template>
<template #delete>
<TrashIcon class="h-5 w-5" />
<span>Delete</span>
</template>
</UiServersTeleportOverflowMenu>
<TrashIcon />
</button>
</ButtonStyled>
<ButtonStyled type="transparent">
<button
v-tooltip="
mod.project_id ? 'Edit mod version' : 'External mods cannot be edited'
"
:disabled="mod.changing || !mod.project_id"
class="!hidden sm:!block"
@click="beginChangeModVersion(mod)"
>
<template v-if="mod.changing">
<UiServersIconsLoadingIcon />
</template>
<template v-else>
<EditIcon />
</template>
</button>
</ButtonStyled>
</div>
<input
:id="`toggle-${mod.filename}`"
:checked="!mod.disabled"
:disabled="mod.changing"
class="switch stylized-toggle"
type="checkbox"
@change="toggleMod(mod)"
/>
<!-- Dropdown for mobile -->
<div class="mr-2 flex items-center sm:hidden">
<UiServersIconsLoadingIcon
v-if="mod.changing"
class="mr-2 h-5 w-5 animate-spin"
style="color: var(--color-base)"
/>
<ButtonStyled v-else circular type="transparent">
<UiServersTeleportOverflowMenu
:options="[
{
id: 'edit',
action: () => beginChangeModVersion(mod),
shown: !!(mod.project_id && !mod.changing),
},
{
id: 'delete',
action: () => removeMod(mod),
},
]"
>
<MoreVerticalIcon aria-hidden="true" />
<template #edit>
<EditIcon class="h-5 w-5" />
<span>Edit</span>
</template>
<template #delete>
<TrashIcon class="h-5 w-5" />
<span>Delete</span>
</template>
</UiServersTeleportOverflowMenu>
</ButtonStyled>
</div>
<input
:id="`toggle-${mod.filename}`"
:checked="!mod.disabled"
:disabled="mod.changing"
class="switch stylized-toggle"
type="checkbox"
@change="toggleMod(mod)"
/>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
</div>
</div>
</div>
<!-- no mods has platform -->
<div
v-else-if="
!hasMods &&
props.server.general?.loader &&
props.server.general?.loader.toLocaleLowerCase() !== 'vanilla'
"
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
>
<PackageClosedIcon class="size-24" />
<p class="m-0 font-bold text-contrast">No {{ type.toLocaleLowerCase() }}s found!</p>
<p class="m-0">
Add some {{ type.toLocaleLowerCase() }}s to your server to manage them here.
</p>
<ButtonStyled color="brand">
<NuxtLink :to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`">
<PlusIcon />
Add {{ type.toLocaleLowerCase() }}
</NuxtLink>
</ButtonStyled>
</div>
<div v-else class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center">
<UiServersIconsLoaderIcon loader="Vanilla" class="size-24" />
<p class="m-0 pt-3 font-bold text-contrast">Your server is running Vanilla Minecraft</p>
<p class="m-0">
Add content to your server by installing a modpack or choosing a different platform that
supports {{ type }}s.
</p>
<div class="flex flex-row items-center gap-4">
<ButtonStyled class="mt-8">
<NuxtLink :to="`/modpacks?sid=${props.server.serverId}`">
<CompassIcon />
Find a modpack
</NuxtLink>
</ButtonStyled>
<div>or</div>
<ButtonStyled class="mt-8">
<NuxtLink :to="`/servers/manage/${props.server.serverId}/options/loader`">
<WrenchIcon />
Change platform
</NuxtLink>
</ButtonStyled>
<!-- no mods has platform -->
<div
v-else-if="
props.server.general?.loader &&
props.server.general?.loader.toLocaleLowerCase() !== 'vanilla'
"
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
>
<div
v-if="!hasFilteredMods && hasMods"
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
>
<SearchIcon class="size-24" />
<p class="m-0 font-bold text-contrast">
No {{ type.toLocaleLowerCase() }}s found for your query!
</p>
<p class="m-0">Try another query, or show everything.</p>
<ButtonStyled>
<button @click="showAll">
<ListIcon />
Show everything
</button>
</ButtonStyled>
</div>
<div
v-else
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
>
<PackageClosedIcon class="size-24" />
<p class="m-0 font-bold text-contrast">No {{ type.toLocaleLowerCase() }}s found!</p>
<p class="m-0">
Add some {{ type.toLocaleLowerCase() }}s to your server to manage them here.
</p>
<div class="flex flex-row items-center gap-4">
<ButtonStyled type="outlined">
<button class="w-full text-nowrap sm:w-fit" @click="initiateFileUpload">
<FileIcon />
Add file
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<nuxt-link
class="w-full text-nowrap sm:w-fit"
:to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`"
>
<PlusIcon />
Add {{ type.toLocaleLowerCase() }}
</nuxt-link>
</ButtonStyled>
</div>
</div>
</div>
</div>
<div v-else class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center">
<UiServersIconsLoaderIcon loader="Vanilla" class="size-24" />
<p class="m-0 pt-3 font-bold text-contrast">Your server is running Vanilla Minecraft</p>
<p class="m-0">
Add content to your server by installing a modpack or choosing a different platform that
supports {{ type }}s.
</p>
<div class="flex flex-row items-center gap-4">
<ButtonStyled class="mt-8">
<NuxtLink :to="`/modpacks?sid=${props.server.serverId}`">
<CompassIcon />
Find a modpack
</NuxtLink>
</ButtonStyled>
<div>or</div>
<ButtonStyled class="mt-8">
<NuxtLink :to="`/servers/manage/${props.server.serverId}/options/loader`">
<WrenchIcon />
Change platform
</NuxtLink>
</ButtonStyled>
</div>
</div>
</FilesUploadDragAndDrop>
</div>
</div>
</template>
@@ -290,10 +363,15 @@ import {
MoreVerticalIcon,
CompassIcon,
WrenchIcon,
ListIcon,
FileIcon,
} from "@modrinth/assets";
import { ButtonStyled, NewModal } from "@modrinth/ui";
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
import type { Server } from "~/composables/pyroServers";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
const props = defineProps<{
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
@@ -304,14 +382,7 @@ const type = computed(() => {
return loader === "paper" || loader === "purpur" ? "Plugin" : "Mod";
});
interface Mod {
name?: string;
filename: string;
project_id?: string;
version_id?: string;
version_number?: string;
icon_url?: string;
disabled: boolean;
interface ContentItem extends Mod {
changing?: boolean;
}
@@ -322,12 +393,41 @@ const listContainer = ref<HTMLElement | null>(null);
const windowScrollY = ref(0);
const windowHeight = ref(0);
const localMods = ref<Mod[]>([]);
const localMods = ref<ContentItem[]>([]);
const searchInput = ref("");
const modSearchInput = ref("");
const filterMethod = ref("all");
const uploadDropdownRef = ref();
const handleDroppedFiles = (files: File[]) => {
files.forEach((file) => {
uploadDropdownRef.value?.uploadFile(file);
});
};
const initiateFileUpload = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = acceptFileFromProjectType(type.value.toLowerCase());
input.multiple = true;
input.onchange = () => {
if (input.files) {
Array.from(input.files).forEach((file) => {
uploadDropdownRef.value?.uploadFile(file);
});
}
};
input.click();
};
const showAll = () => {
searchInput.value = "";
modSearchInput.value = "";
filterMethod.value = "all";
};
const filterMethodLabel = computed(() => {
switch (filterMethod.value) {
case "disabled":
@@ -419,14 +519,17 @@ const debouncedSearch = debounce(() => {
modSearchInput.value = searchInput.value;
if (pyroContentSentinel.value) {
pyroContentSentinel.value.scrollIntoView({
behavior: "smooth",
block: "start",
});
const sentinelRect = pyroContentSentinel.value.getBoundingClientRect();
if (sentinelRect.top < 0 || sentinelRect.bottom > window.innerHeight) {
pyroContentSentinel.value.scrollIntoView({
// behavior: "smooth",
block: "start",
});
}
}
}, 300);
async function toggleMod(mod: Mod) {
async function toggleMod(mod: ContentItem) {
mod.changing = true;
const originalFilename = mod.filename;
@@ -458,7 +561,7 @@ async function toggleMod(mod: Mod) {
mod.changing = false;
}
async function removeMod(mod: Mod) {
async function removeMod(mod: ContentItem) {
mod.changing = true;
try {
@@ -515,6 +618,10 @@ async function changeModVersion() {
}
const hasMods = computed(() => {
return localMods.value?.length > 0;
});
const hasFilteredMods = computed(() => {
return filteredMods.value?.length > 0;
});