* fix(content): changing mod versions works again
* chore(assets): update pyro logo
* fix(properties): deprecate fetchconfigfile
* Revert "fix(content): changing mod versions works again"
This reverts commit d7c0d1196f.
* feat(files): ability to sort via column click
* chore(startup): update clunky wording
* feat(serverListing): server icons SSR friendly
* fix(servers): if archon fails, display err in listing
* chore(serverlisting): use pyroserver hook to init icon
* chore(servers): much more graceful reinstall
* fix(servers): tw warn
* fix(platform): correctly react when pack reinstalled
* fix(serversroot): explicitly import navigateTo
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(serverlabels): show skeleton instead of hiding
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(platform): install-aware controls
Signed-off-by: Evan Song <theevansong@gmail.com>
* refactor!(platform): rewrite platform page
* fix(platform): regression in autoselecting loader
* chore(platform): prefer version over project modification date
* fix(platform): permanent hang after initial mount
* chore(platform): do not silently fail and hang if modpack fails loading
* oops: remove hardcoded error causer
* fix(platform): switch modpack btn while installing doesnt need class
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(platform): adjust styling in version modal
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(platform): prevent changing project card style
Signed-off-by: Evan Song <theevansong@gmail.com>
* refactor(pyrodropdown): rewrite
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(pyrodropdown): do nopt use deprecated substr
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore: clean
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(network): sentence case
Signed-off-by: Evan Song <theevansong@gmail.com>
* refactor(terminal): initial batch
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): fulllog over fullscreen
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): fullscreen conflict with body scroll
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(terminal): init drag select
* feat(terminal): shift click support
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): double lines limit
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(terminal): copy button
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): protip style
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): improve styles
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(terminal): regex search
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): move icons to icons dir
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): improve drag select autoscroll inertia
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): cancel selection on right click
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): progblur and stb btn disappearing
Signed-off-by: Evan Song <theevansong@gmail.com>
* refactor(serverstats): power efficiency
* fix(subdomainlabel): correct tooltip terminology
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(preferences): users hide subdomain label
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(servers): clean
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): deselect lines on escape
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(serversidebar): type err
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(fileitem): vue server render type
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): disable pointer events on lines if scrolling
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): search result counts style
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): plural
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): clean
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(terminal): view selection
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(terminal): show actively selected lines in scrollbar
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminallog): btn color
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore: clean
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(gamelabel): align to text
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(gamelabel): align to text
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(listing): remove deadcode
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(serverlisting): deprecated process.server
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(platform): correctly disable button
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(backups): do not allow backup creation during server installation
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(platform): flush stale currentversion data on successful install
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(gamelabel): fix gap
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(network): vaporize uppercase
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(info): vaporize uppercase
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(backups): style unification
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(backups): finalize style change
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(servers): catch pyro servers fetch errors during ssr
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(serverstats): ram as bytes graph now works
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(platform): unify attempts and refresh interval
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): input
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(servers): installing ticket + update available notice back in platform
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): dont add bg to scroll track
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): preserve whitespace
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(serversroot): unnest blurred icon query
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(serverstats): clamp memory usage to 100% no matter what
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat(terminal): allow copy of single lines, show btn
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore(terminal): animate copy>view transition
Signed-off-by: Evan Song <theevansong@gmail.com>
* init: search improvements
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix: lint
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore: change log modal title
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix: hide fullscreen when selecting and cancel selection on clickout
Signed-off-by: Evan Song <theevansong@gmail.com>
* refactor(terminal): more reliable jumpToLine
Signed-off-by: Evan Song <theevansong@gmail.com>
* feat: search results separator
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore: remove buggy isScrollable check
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix: style
Signed-off-by: Evan Song <theevansong@gmail.com>
* refactor: correctly store pos to make jump reliable
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix: disparity between search/log dragselect
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix: prevent propagation of click events when clicking on jump btn
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix: switch selection strategies depending on terminal mode
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore: smarter esc handling
Signed-off-by: Evan Song <theevansong@gmail.com>
* finalize
Signed-off-by: Evan Song <theevansong@gmail.com>
* run fix
* fix: ensure lines between cannot be selected
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix: increase initial log batch to 256
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): click on scroll track should take user to new scroll position
Signed-off-by: Evan Song <theevansong@gmail.com>
* fix(terminal): update aria label for view selected logs btn
Signed-off-by: Evan Song <theevansong@gmail.com>
* chore: clean
Signed-off-by: Evan Song <theevansong@gmail.com>
---------
Signed-off-by: Evan Song <theevansong@gmail.com>
179 lines
5.0 KiB
Vue
179 lines
5.0 KiB
Vue
<template>
|
|
<div class="flex h-[calc(100vh-12rem)] w-full flex-col items-center">
|
|
<div
|
|
ref="container"
|
|
class="relative w-full flex-grow cursor-grab overflow-hidden rounded-b-2xl bg-black active:cursor-grabbing"
|
|
@mousedown="startPan"
|
|
@mousemove="handlePan"
|
|
@mouseup="stopPan"
|
|
@mouseleave="stopPan"
|
|
@wheel.prevent="handleWheel"
|
|
>
|
|
<div v-if="state.isLoading" />
|
|
<div
|
|
v-if="state.hasError"
|
|
class="flex h-full w-full flex-col items-center justify-center gap-8"
|
|
>
|
|
<UiServersIconsPanelErrorIcon />
|
|
<p class="m-0">{{ state.errorMessage || "Invalid or empty image file." }}</p>
|
|
</div>
|
|
<img
|
|
v-show="isReady"
|
|
ref="imageRef"
|
|
:src="imageObjectUrl"
|
|
class="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform"
|
|
:style="imageStyle"
|
|
alt="Viewed image"
|
|
@load="handleImageLoad"
|
|
@error="handleImageError"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="!state.hasError"
|
|
class="absolute bottom-0 mb-2 flex w-fit justify-center gap-2 space-x-4 rounded-2xl bg-bg p-2"
|
|
>
|
|
<ButtonStyled type="transparent" @click="zoom(ZOOM_IN_FACTOR)">
|
|
<button v-tooltip="'Zoom in'">
|
|
<ZoomInIcon />
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled type="transparent" @click="zoom(ZOOM_OUT_FACTOR)">
|
|
<button v-tooltip="'Zoom out'">
|
|
<ZoomOutIcon />
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled type="transparent" @click="reset">
|
|
<button>
|
|
<span class="font-mono">{{ Math.round(state.scale * 100) }}%</span>
|
|
<span class="ml-4 text-sm text-blue">Reset</span>
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
|
import { ZoomInIcon, ZoomOutIcon } from "@modrinth/assets";
|
|
import { ButtonStyled } from "@modrinth/ui";
|
|
|
|
const ZOOM_MIN = 0.1;
|
|
const ZOOM_MAX = 5;
|
|
const ZOOM_IN_FACTOR = 1.2;
|
|
const ZOOM_OUT_FACTOR = 0.8;
|
|
const INITIAL_SCALE = 0.5;
|
|
const MAX_IMAGE_DIMENSION = 4096;
|
|
|
|
const props = defineProps<{
|
|
imageBlob: Blob;
|
|
}>();
|
|
|
|
const state = ref({
|
|
scale: INITIAL_SCALE,
|
|
translateX: 0,
|
|
translateY: 0,
|
|
isPanning: false,
|
|
startX: 0,
|
|
startY: 0,
|
|
isLoading: false,
|
|
hasError: false,
|
|
errorMessage: "",
|
|
});
|
|
|
|
const imageRef = ref<HTMLImageElement | null>(null);
|
|
const container = ref<HTMLElement | null>(null);
|
|
const imageObjectUrl = ref("");
|
|
const rafId = ref(0);
|
|
|
|
const isReady = computed(() => !state.value.isLoading && !state.value.hasError);
|
|
|
|
const imageStyle = computed(() => ({
|
|
transform: `translate(-50%, -50%) scale(${state.value.scale}) translate(${state.value.translateX}px, ${state.value.translateY}px)`,
|
|
transition: state.value.isPanning ? "none" : "transform 0.3s ease-out",
|
|
}));
|
|
|
|
const validateImageDimensions = (img: HTMLImageElement): boolean => {
|
|
if (img.naturalWidth > MAX_IMAGE_DIMENSION || img.naturalHeight > MAX_IMAGE_DIMENSION) {
|
|
state.value.hasError = true;
|
|
state.value.errorMessage = `Image too large to view (max ${MAX_IMAGE_DIMENSION}x${MAX_IMAGE_DIMENSION} pixels)`;
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const updateImageUrl = (blob: Blob) => {
|
|
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value);
|
|
imageObjectUrl.value = URL.createObjectURL(blob);
|
|
};
|
|
|
|
const handleImageLoad = () => {
|
|
if (!imageRef.value || !validateImageDimensions(imageRef.value)) {
|
|
state.value.isLoading = false;
|
|
return;
|
|
}
|
|
state.value.isLoading = false;
|
|
reset();
|
|
};
|
|
|
|
const handleImageError = () => {
|
|
state.value.isLoading = false;
|
|
state.value.hasError = true;
|
|
state.value.errorMessage = "Failed to load image";
|
|
};
|
|
|
|
const zoom = (factor: number) => {
|
|
const newScale = state.value.scale * factor;
|
|
state.value.scale = Math.max(ZOOM_MIN, Math.min(newScale, ZOOM_MAX));
|
|
};
|
|
|
|
const reset = () => {
|
|
state.value.scale = INITIAL_SCALE;
|
|
state.value.translateX = 0;
|
|
state.value.translateY = 0;
|
|
};
|
|
|
|
const startPan = (e: MouseEvent) => {
|
|
state.value.isPanning = true;
|
|
state.value.startX = e.clientX - state.value.translateX;
|
|
state.value.startY = e.clientY - state.value.translateY;
|
|
};
|
|
|
|
const handlePan = (e: MouseEvent) => {
|
|
if (!state.value.isPanning) return;
|
|
cancelAnimationFrame(rafId.value);
|
|
rafId.value = requestAnimationFrame(() => {
|
|
state.value.translateX = e.clientX - state.value.startX;
|
|
state.value.translateY = e.clientY - state.value.startY;
|
|
});
|
|
};
|
|
|
|
const stopPan = () => {
|
|
state.value.isPanning = false;
|
|
};
|
|
|
|
const handleWheel = (e: WheelEvent) => {
|
|
const delta = e.deltaY * -0.001;
|
|
const factor = 1 + delta;
|
|
zoom(factor);
|
|
};
|
|
|
|
watch(
|
|
() => props.imageBlob,
|
|
(newBlob) => {
|
|
if (!newBlob) return;
|
|
state.value.isLoading = true;
|
|
state.value.hasError = false;
|
|
updateImageUrl(newBlob);
|
|
},
|
|
);
|
|
|
|
onMounted(() => {
|
|
if (props.imageBlob) updateImageUrl(props.imageBlob);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (imageObjectUrl.value) URL.revokeObjectURL(imageObjectUrl.value);
|
|
cancelAnimationFrame(rafId.value);
|
|
});
|
|
</script>
|