Skip to content

Commit

Permalink
WIP new asset search UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Kruptein committed Dec 28, 2024
1 parent 7127683 commit 01cb82e
Show file tree
Hide file tree
Showing 42 changed files with 1,347 additions and 523 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ tech changes will usually be stripped from release notes for the public
- Notes can now be popped out to a separate window
- NoteManager:
- Added a button to clear the current search
- In-Game Assets UI:
- Option to search through assets
- Option to add folder shortcuts per campaign
- These allow quicker navigation to frequently used folders
- A "All assets" shortcut is always available
- [server] Assets:
- limits:
- Added limits to the total size of assets a user can upload and the size of a single asset
Expand All @@ -44,6 +49,11 @@ tech changes will usually be stripped from release notes for the public
- The images shown in the asset manager will now use the thumbnail of the asset if available
- This should reduce load times and improve general performance
- This also applies to the preview when hovering over assets in the in-game assets sidebar
- Remove initiated from the context menu now removes the entire selection
- Context menu retains selection unless an item not in the current selection is clicked
- In-game assets:
- Sidebar is removed and replaced with a new Assets dialog similar to notes
- The new UI has almost full compatibility with the assets in the dashboard
- Notes:
- Add filtering option 'All' to note manager to show both global and local notes
- Note popouts for clients without edit access now show 'view source' instead of 'edit'
Expand Down
2 changes: 1 addition & 1 deletion client/src/apiTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AssetId } from "./assetManager/models";
import type { AssetId } from "./assets/models";
import type { GlobalId } from "./core/id";
import type { LayerName } from "./game/models/floor";
import type { Role } from "./game/models/role";
Expand Down
11 changes: 0 additions & 11 deletions client/src/assetManager/context.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ function wrapSocket<T>(event: string): (data: T) => void {
};
}

function wrapSocketWithAck<T, Y>(event: string): (data: T) => Promise<Y> {
return async (data: T): Promise<Y> => {
return (await socket.emitWithAck(event, data)) as Y;
};
}

export const sendFolderGet = wrapSocket<AssetId | undefined>("Folder.Get");
export const sendFolderGetByPath = wrapSocket<string>("Folder.GetByPath");
export const sendInodeMove = wrapSocket<ApiAssetInodeMove>("Inode.Move");
Expand All @@ -24,3 +30,4 @@ export const sendCreateFolder = wrapSocket<ApiAssetCreateFolder>("Folder.Create"
export const sendRemoveShare = wrapSocket<ApiAssetRemoveShare>("Asset.Share.Remove");
export const sendEditShareRight = wrapSocket<ApiAssetCreateShare>("Asset.Share.Edit");
export const sendCreateShare = wrapSocket<ApiAssetCreateShare>("Asset.Share.Create");
export const getFolderPath = wrapSocketWithAck<AssetId, { id: AssetId; name: string }[]>("Asset.FolderPath");
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useToast } from "vue-toastification";

import type { ApiAssetAdd, ApiAssetCreateShare, ApiAssetFolder, ApiAssetRemoveShare } from "../apiTypes";
import { baseAdjust } from "../core/http";
import { router } from "../router";
import { coreStore } from "../store/core";

Expand Down Expand Up @@ -44,7 +43,7 @@ socket.on("Folder.Set", async (data: ApiAssetFolder) => {
assetSystem.setFolderData(data.folder.id, data.folder);
assetState.mutableReactive.sharedParent = data.sharedParent;
assetState.mutableReactive.sharedRight = data.sharedRight;
if (!assetState.readonly.modalActive) {
if (router.currentRoute.value.name === "assets") {
if (data.path) assetSystem.setPath(data.path);
const path = `/assets${assetState.currentFilePath.value}/`;
if (path !== router.currentRoute.value.path) {
Expand All @@ -62,10 +61,6 @@ socket.on("Asset.Upload.Finish", (data: ApiAssetAdd) => {
assetSystem.resolveUpload(data.asset.name);
});

socket.on("Asset.Export.Finish", (uuid: string) => {
window.open(baseAdjust(`/static/temp/${uuid}.paa`));
});

socket.on("Asset.Import.Finish", (name: string) => {
assetSystem.resolveUpload(name);
sendFolderGet(assetState.currentFolder.value);
Expand Down
29 changes: 23 additions & 6 deletions client/src/assetManager/index.ts → client/src/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ApiAsset, ApiAssetUpload } from "../apiTypes";
import { callbackProvider, uuidv4 } from "../core/utils";
import { router } from "../router";

import { sendAssetRemove, sendAssetRename, sendFolderGet, sendInodeMove } from "./emits";
import { sendAssetRemove, sendAssetRename, sendFolderGet, getFolderPath, sendInodeMove } from "./emits";
import type { AssetId } from "./models";
import { socket } from "./socket";
import { assetState } from "./state";
Expand Down Expand Up @@ -55,7 +55,7 @@ class AssetSystem {
sendInodeMove({ inode, target: targetFolder });
}

changeDirectory(targetFolder: AssetId | "POP"): void {
async changeDirectory(targetFolder: AssetId | "POP"): Promise<void> {
if (targetFolder === "POP") {
$.folderPath.pop();
} else if (targetFolder === raw.root) {
Expand All @@ -64,7 +64,14 @@ class AssetSystem {
while (assetState.currentFolder.value !== targetFolder) $.folderPath.pop();
} else {
const asset = raw.idMap.get(targetFolder);
if (asset !== undefined) $.folderPath.push({ id: targetFolder, name: asset.name });
if (asset !== undefined) {
if (raw.root && ($.idMap.get(raw.root)?.children?.some((c) => c.id === targetFolder) ?? false)) {
$.folderPath.push({ id: targetFolder, name: asset.name });
} else {
const path = await getFolderPath(targetFolder);
$.folderPath = path.slice(1);
}
}
}
this.clearSelected();
sendFolderGet(assetState.currentFolder.value);
Expand Down Expand Up @@ -161,19 +168,29 @@ class AssetSystem {
await assetSystem.rootCallback.wait();
}

const limit = await socket.emitWithAck("Asset.Upload.Limit") as { single: number; total: number; used: number };
const limit = (await socket.emitWithAck("Asset.Upload.Limit")) as {
single: number;
total: number;
used: number;
};

// First check limits
let totalSize = 0;
for (const file of fls) {
totalSize += file.size;
if (limit.single > 0 && file.size > limit.single) {
toast.error(`File ${file.name} is too large. Max size is ${limit.single} bytes. Contact the server admin if you need to upload larger files.`, { timeout: 0 });
toast.error(
`File ${file.name} is too large. Max size is ${limit.single} bytes. Contact the server admin if you need to upload larger files.`,
{ timeout: 0 },
);
return [];
}
if (limit.total > 0 && totalSize > limit.total - limit.used) {
const remaining = Math.max(0, limit.total - limit.used);
toast.error(`Total size of files is too large. You have ${remaining} bytes remaining and attempted to upload ${totalSize} bytes. Contact the server admin if you need to upload larger files.`, { timeout: 0 });
toast.error(
`Total size of files is too large. You have ${remaining} bytes remaining and attempted to upload ${totalSize} bytes. Contact the server admin if you need to upload larger files.`,
{ timeout: 0 },
);
return [];
}
}
Expand Down
File renamed without changes.
44 changes: 44 additions & 0 deletions client/src/assets/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ref, watch, type Ref } from "vue";

import type { ApiAsset } from "../apiTypes";

import { socket } from "./socket";
import { assetState } from "./state";

export interface AssetSearch {

Check failure on line 8 in client/src/assets/search.ts

View workflow job for this annotation

GitHub Actions / CLIENT-lint

exported declaration 'AssetSearch' not used within other modules
clear: () => void;
filter: Ref<string>;
results: Ref<ApiAsset[]>;
loading: Ref<boolean>;
}

export function useAssetSearch(searchBar: Ref<HTMLInputElement | null>): AssetSearch {
const filter = ref("");
const results = ref<ApiAsset[]>([]);
const loading = ref(false);
watch(assetState.currentFolder, () => {
filter.value = "";
});

function clear(): void {
filter.value = "";
searchBar.value?.focus();
}

watch(filter, async (filter) => {
if (filter.length < 3) {
results.value = [];
return;
}

loading.value = true;
const data = (await socket.emitWithAck("Asset.Search", filter)) as ApiAsset[];
for (const asset of data) {
assetState.mutableReactive.idMap.set(asset.id, asset);
}
results.value = data;
loading.value = false;
});

return { clear, filter, results, loading };
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,58 +1,51 @@
<script setup lang="ts">
import { computed, defineEmits, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { computed, defineEmits, nextTick, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import ContextMenu from "../core/components/contextMenu/ContextMenu.vue";
import type { Section } from "../core/components/contextMenu/types";
import { useModal } from "../core/plugins/modals/plugin";
import { coreStore } from "../store/core";
import { assetSystem } from "..";
import ContextMenu from "../../core/components/contextMenu/ContextMenu.vue";
import type { Section } from "../../core/components/contextMenu/types";
import { useModal } from "../../core/plugins/modals/plugin";
import { coreStore } from "../../store/core";
import type { AssetId } from "../models";
import { assetState } from "../state";
import AssetShare from "./AssetShare.vue";
import { assetContextLeft, assetContextTop, showAssetContextMenu } from "./context";
import type { AssetId } from "./models";
import { assetState } from "./state";
import type { AssetContextMenu } from "./context";
import { assetSystem } from ".";
const emit = defineEmits<{(event: "rename", payload: AssetId): void;}>();
const emit = defineEmits<{ (event: "rename", payload: AssetId): void; (event: "close"): void }>();
const props = defineProps<AssetContextMenu["state"]>();
const cm = ref<{ $el: HTMLDivElement } | null>(null);
const modals = useModal();
const { t } = useI18n();
const showAssetShare = ref(false);
onMounted(() => {
window.addEventListener("scroll", close);
});
onUnmounted(() => {
window.removeEventListener("scroll", close);
});
watch(showAssetContextMenu, async () => {
if (showAssetContextMenu.value) {
watch(props.visible, async () => {
if (props.visible.value) {
await nextTick(() => cm.value!.$el.focus());
}
});
function close(): void {
emit("close");
}
const multiSelect = computed(() => assetState.reactive.selected.length > 1);
const asset = computed(() => assetState.reactive.selected.at(0));
const canShare = computed(() => {
if (assetState.reactive.selected.length !== 1) return false;
if (multiSelect.value) return false;
if (asset.value === undefined) return false;
const data = assetState.reactive.idMap.get(asset.value);
if (data === undefined) return false;
const username = coreStore.state.username;
console.log(data.shares);
return data.owner === username || data.shares.some((s) => s.user === username && s.right === "edit");
});
function close(): void {
showAssetContextMenu.value = false;
}
function rename(): void {
if (assetState.raw.selected.length !== 1) return;
if (multiSelect.value) return;
const asset = assetState.raw.idMap.get(assetState.raw.selected[0]!);
if (asset === undefined) {
console.error("Attempt to rename unknown file");
Expand Down Expand Up @@ -82,6 +75,7 @@ const sections = computed<Section[]>(() => [
{
title: t("common.rename"),
action: rename,
disabled: multiSelect.value,
},
{
title: "Share",
Expand All @@ -99,10 +93,10 @@ const sections = computed<Section[]>(() => [
<AssetShare :visible="showAssetShare" :asset="asset" @close="showAssetShare = false" />
<ContextMenu
ref="cm"
:visible="showAssetContextMenu"
:left="assetContextLeft"
:top="assetContextTop"
:left="left.value"
:top="top.value"
:visible="visible.value"
:sections="sections"
@cm:close="close"
@cm:close="emit('close')"
/>
</template>
Loading

0 comments on commit 01cb82e

Please sign in to comment.