Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Assets): Generate & Use thumbnails for assets #1524

Merged
merged 3 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 10 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ tech changes will usually be stripped from release notes for the public
- NoteManager:
- Added a button to clear the current search
- [server] Assets:
- Added limits to the total size of assets a user can upload and the size of a single asset
- These limits can be configured in the server config
- By default there are no limits, it's up to the server admin to configure them
- These limits will only apply to new assets, existing assets are not affected
- limits:
- Added limits to the total size of assets a user can upload and the size of a single asset
- These limits can be configured in the server config
- By default there are no limits, it's up to the server admin to configure them
- These limits will only apply to new assets, existing assets are not affected
- Thumbnails:
- The server will now generate thumbnails for all assets

### Changed

Expand All @@ -38,6 +41,9 @@ tech changes will usually be stripped from release notes for the public
- This fixes some of the entries in the Fixed section
- AssetManager:
- Changed UI of renaming assets, allowing inline editing rather than opening a popup
- 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
- 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
19 changes: 14 additions & 5 deletions client/src/assetManager/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ export function showIdName(dir: AssetId): string {
return assetState.raw.idMap.get(dir)?.name ?? "";
}

export function getImageSrcFromAssetId(file: AssetId, addBaseUrl = true): string {
export function getImageSrcFromAssetId(
file: AssetId,
options?: { addBaseUrl?: boolean; thumbnailFormat?: string },
): string {
const fileHash = assetState.raw.idMap.get(file)!.fileHash ?? "";
return getImageSrcFromHash(fileHash, addBaseUrl);
return getImageSrcFromHash(fileHash, options);
}

export function getImageSrcFromHash(fileHash: string, addBaseUrl = true): string {
const path = `/static/assets/${fileHash.slice(0, 2)}/${fileHash.slice(2, 4)}/${fileHash}`;
return addBaseUrl ? baseAdjust(path) : path;
export function getImageSrcFromHash(
fileHash: string,
options?: { addBaseUrl?: boolean; thumbnailFormat?: string },
): string {
let path = `/static/assets/${fileHash.slice(0, 2)}/${fileHash.slice(2, 4)}/${fileHash}`;
if (options?.thumbnailFormat !== undefined) {
path = `${path}.thumb.${options.thumbnailFormat}`;
}
return (options?.addBaseUrl ?? true) ? baseAdjust(path) : path;
}
21 changes: 19 additions & 2 deletions client/src/core/components/modals/AssetPicker.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { watchEffect } from "vue";
import { ref, watchEffect } from "vue";

import { assetSystem } from "../../../assetManager";
import type { AssetId } from "../../../assetManager/models";
Expand All @@ -14,6 +14,8 @@ import Modal from "./Modal.vue";
const props = defineProps<{ visible: boolean }>();
const emit = defineEmits(["submit", "close"]);

const thumbnailMisses = ref(new Set<AssetId>());

const { t } = i18n.global;

watchEffect(() => {
Expand Down Expand Up @@ -82,7 +84,18 @@ function select(event: MouseEvent, inode: AssetId): void {
:class="{ 'inode-selected': assetState.reactive.selected.includes(file) }"
@click="select($event, file)"
>
<img :src="getImageSrcFromAssetId(file)" width="50" alt="" />
<picture v-if="!thumbnailMisses.has(file)">
<source
:srcset="getImageSrcFromAssetId(file, { thumbnailFormat: 'webp' })"
type="image/webp"
/>
<source
:srcset="getImageSrcFromAssetId(file, { thumbnailFormat: 'jpeg' })"
type="image/jpeg"
/>
<img alt="" loading="lazy" @error="thumbnailMisses.add(file)" />
</picture>
<img v-else :src="getImageSrcFromAssetId(file)" alt="" loading="lazy" />
<div class="title">{{ showIdName(file) }}</div>
</div>
</div>
Expand Down Expand Up @@ -196,6 +209,10 @@ function select(event: MouseEvent, inode: AssetId): void {
* {
pointer-events: none;
}

img {
width: 50px;
}
}

.inode:hover,
Expand Down
4 changes: 3 additions & 1 deletion client/src/core/models/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AssetId } from "../../assetManager/models";

export type AssetListMap = Map<string, AssetListMap | AssetFile[]>;
export type ReadonlyAssetListMap = ReadonlyMap<string, ReadonlyAssetListMap | AssetFile[]>;

Expand All @@ -6,7 +8,7 @@ export interface AssetList {
}

export interface AssetFile {
id: number;
id: AssetId;
name: string;
hash: string;
}
Expand Down
36 changes: 21 additions & 15 deletions client/src/dashboard/Assets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const modals = useModal();
const route = useRoute();
const toast = useToast();

const thumbnailMisses = ref(new Set<AssetId>());

const body = document.getElementsByTagName("body")[0];

const activeSelectionUrl = `url(${baseAdjust("/static/img/assetmanager/active_selection.png")})`;
Expand Down Expand Up @@ -178,12 +180,12 @@ function openContextMenu(event: MouseEvent, key: AssetId): void {
let draggingSelection = false;

function renameAsset(event: FocusEvent, file: AssetId, oldName: string): void {
if(!canEdit(file, false)) {
if (!canEdit(file, false)) {
return;
}

const target = event.target as HTMLElement;
if(target.textContent === null){
if (target.textContent === null) {
target.textContent = oldName;
currentRenameAsset.value = null;
return;
Expand Down Expand Up @@ -295,17 +297,17 @@ function canEdit(data: AssetId | DeepReadonly<ApiAsset> | undefined, includeRoot
return true;
}

function selectElementContents(el: HTMLElement) : void {
function selectElementContents(el: HTMLElement): void {
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
if(sel) {
if (sel) {
sel.removeAllRanges();
sel.addRange(range);
}
}

async function showRenameUI(id: AssetId) : Promise<void> {
async function showRenameUI(id: AssetId): Promise<void> {
const el = contextTargetElement.value;
contextTargetElement.value = null;
if (el) {
Expand All @@ -316,7 +318,6 @@ async function showRenameUI(id: AssetId) : Promise<void> {
});
}
}

</script>

<template>
Expand Down Expand Up @@ -392,10 +393,10 @@ async function showRenameUI(id: AssetId) : Promise<void> {
<font-awesome-icon v-if="isShared(folder)" icon="user-tag" class="asset-link" />
<font-awesome-icon icon="folder" style="font-size: 12.5em" />
<div
:contenteditable="folder.id === currentRenameAsset"
class="title"
@keydown.enter="($event!.target as HTMLElement).blur()"
@blur="renameAsset($event, folder.id, folder.name)"
:contenteditable="folder.id === currentRenameAsset"
class="title"
@keydown.enter="($event!.target as HTMLElement).blur()"
@blur="renameAsset($event, folder.id, folder.name)"
>
{{ folder.name }}
</div>
Expand All @@ -415,12 +416,17 @@ async function showRenameUI(id: AssetId) : Promise<void> {
@dragstart="startDrag($event, file.id)"
>
<font-awesome-icon v-if="isShared(file)" icon="user-tag" class="asset-link" />
<img :src="getImageSrcFromAssetId(file.id)" width="50" alt="" />
<picture v-if="!thumbnailMisses.has(file.id)">
<source :srcset="getImageSrcFromAssetId(file.id, { thumbnailFormat: 'webp' })" type="image/webp" />
<source :srcset="getImageSrcFromAssetId(file.id, { thumbnailFormat: 'jpeg' })" type="image/jpeg" />
<img alt="" loading="lazy" @error="thumbnailMisses.add(file.id)" />
</picture>
<img v-else :src="getImageSrcFromAssetId(file.id)" alt="" loading="lazy" />
<div
:contenteditable="file.id === currentRenameAsset"
class="title"
@keydown.enter="($event!.target as HTMLElement).blur()"
@blur="renameAsset($event, file.id, file.name)"
:contenteditable="file.id === currentRenameAsset"
class="title"
@keydown.enter="($event!.target as HTMLElement).blur()"
@blur="renameAsset($event, file.id, file.name)"
>
{{ file.name }}
</div>
Expand Down
2 changes: 1 addition & 1 deletion client/src/game/dropAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async function dropHelper(
return;
}
await dropAsset(
{ assetId: assetInfo.assetId, imageSource: getImageSrcFromHash(assetInfo.assetHash, false) },
{ assetId: assetInfo.assetId, imageSource: getImageSrcFromHash(assetInfo.assetHash, { addBaseUrl: false }) },
location,
);
}
Expand Down
12 changes: 10 additions & 2 deletions client/src/game/ui/menu/AssetNode.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, reactive } from "vue";
import { computed, reactive, ref } from "vue";

import type { AssetId } from "../../../assetManager/models";
import { getImageSrcFromHash } from "../../../assetManager/utils";
import { filter } from "../../../core/iter";
import type { AssetFile, AssetListMap } from "../../../core/models/types";
Expand All @@ -16,6 +17,8 @@ const state: State = reactive({
openFolders: new Set(),
});

const thumbnailMisses = ref(new Set<AssetId>());

function childAssets(folder: string): AssetListMap {
return props.assets.get(folder) as AssetListMap;
}
Expand Down Expand Up @@ -64,7 +67,12 @@ function dragStart(event: DragEvent, assetHash: string, assetId: number): void {
>
{{ file.name }}
<div v-if="state.hoveredHash == file.hash" class="preview">
<img class="asset-preview-image" :src="getImageSrcFromHash(file.hash)" alt="" />
<picture v-if="!thumbnailMisses.has(file.id)">
<source :srcset="getImageSrcFromHash(file.hash, { thumbnailFormat: 'webp' })" type="image/webp" />
<source :srcset="getImageSrcFromHash(file.hash, { thumbnailFormat: 'jpeg' })" type="image/jpeg" />
<img alt="" loading="lazy" class="asset-preview-image" @error="thumbnailMisses.add(file.id)" />
</picture>
<img v-else :src="getImageSrcFromHash(file.hash)" alt="" loading="lazy" class="asset-preview-image" />
</div>
</li>
</ul>
Expand Down
6 changes: 3 additions & 3 deletions client/src/game/ui/settings/shape/PropertySettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,13 @@ async function changeAsset(): Promise<void> {
if (data === undefined || data.fileHash === undefined) return;
const shape = getShape(activeShapeStore.state.id);
if (shape === undefined || shape.type !== "assetrect") return;
(shape as Asset).setImage(getImageSrcFromHash(data.fileHash, false), true);
(shape as Asset).setImage(getImageSrcFromHash(data.fileHash, { addBaseUrl: false }), true);
}
</script>

<template>
<div v-if="shapeProps" class="panel restore-panel">
<div class="spanrow header">{{ t('game.ui.selection.edit_dialog.properties.common') }}</div>
<div class="spanrow header">{{ t("game.ui.selection.edit_dialog.properties.common") }}</div>
<div class="row">
<label for="shapeselectiondialog-name">{{ t("common.name") }}</label>
<input
Expand Down Expand Up @@ -226,7 +226,7 @@ async function changeAsset(): Promise<void> {
<label></label>
<button @click="changeAsset">Change asset</button>
</div>
<div class="spanrow header">{{ t('game.ui.selection.edit_dialog.properties.advanced') }}</div>
<div class="spanrow header">{{ t("game.ui.selection.edit_dialog.properties.advanced") }}</div>
<div class="row">
<label for="shapeselectiondialog-visionblocker">
{{ t("game.ui.selection.edit_dialog.dialog.block_vision_light") }}
Expand Down
2 changes: 1 addition & 1 deletion client/src/game/ui/settings/shape/VariantSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ async function addVariant(): Promise<void> {
if (name === undefined) return;

const newShape = await dropAsset(
{ imageSource: getImageSrcFromHash(asset.fileHash, false), assetId: asset.id },
{ imageSource: getImageSrcFromHash(asset.fileHash, { addBaseUrl: false }), assetId: asset.id },
shape.refPoint,
);
if (newShape === undefined) {
Expand Down
1 change: 1 addition & 0 deletions server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ aiohttp_security==0.5.0
aiohttp_session==2.12.1
bcrypt==4.2.0
cryptography==43.0.1
pillow==11.0.0
python-engineio==4.9.1
python-socketio==5.11.4
peewee==3.17.5
Expand Down
2 changes: 2 additions & 0 deletions server/src/api/socket/asset_manager/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ async def handle_regular_file(upload_data: ApiAssetUpload, data: bytes, sid: str
parent=target,
)

asset.generate_thumbnails()

asset_dict = transform_asset(asset, user)
await sio.emit(
"Asset.Upload.Finish",
Expand Down
5 changes: 5 additions & 0 deletions server/src/db/models/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from peewee import ForeignKeyField, TextField
from typing_extensions import Self, TypedDict

from ...thumbnail import generate_thumbnail_for_asset
from ..base import BaseDbModel
from ..typed import SelectSequence
from .asset_share import AssetShare
Expand Down Expand Up @@ -79,6 +80,10 @@ def get_shared_parent(self, user: User) -> AssetShare | None:
asset = asset.parent
return None

def generate_thumbnails(self) -> None:
if self.file_hash:
generate_thumbnail_for_asset(self.name, self.file_hash)

@classmethod
def get_root_folder(cls, user) -> Self:
try:
Expand Down
2 changes: 1 addition & 1 deletion server/src/planarserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def server_main(args):
mimetypes.types_map[".js"] = "application/javascript; charset=utf-8"

if not save_newly_created:
save.upgrade_save()
save.upgrade_save(loop=loop)

loop.create_task(start_servers())

Expand Down
Loading
Loading