206 lines
5.3 KiB
Vue
206 lines
5.3 KiB
Vue
<template>
|
||
<div
|
||
class="remote-screen"
|
||
:class="{ 'remote-screen--dragover': isDragOver }"
|
||
:style="{ aspectRatio: `${imageSink?.resolution?.width ?? 1}/${imageSink?.resolution?.height ?? 1}` }"
|
||
@drop="onDrop"
|
||
@dragover.prevent
|
||
@dragleave="isDragOver = false"
|
||
@dragenter="isDragOver = true"
|
||
@wheel.prevent="onWheel"
|
||
@mousedown="onMouseDown"
|
||
>
|
||
<div class="remote-screen__info">
|
||
{{ screen.name }} ({{ imageSink?.resolution?.width ?? '?' }}x{{ imageSink?.resolution?.height ?? '?' }})
|
||
<span style="position:absolute;right:0.25rem;">⚙️</span>
|
||
</div>
|
||
<div class="remote-screen__loading" v-if="isLoading"><span>⟳</span></div>
|
||
<div class="remote-screen__connect" v-if="!screen.isConnected">
|
||
<button @click="screen.connect()">Connect</button>
|
||
</div>
|
||
<canvas
|
||
ref="canvas"
|
||
style="max-width: 100%;"
|
||
:width="imageSink?.resolution?.width / 4"
|
||
:height="imageSink?.resolution?.height / 4"
|
||
:style="{
|
||
// transform: `scale(${scale / 100}) translate(${(x)}%, ${(y)}%)`,
|
||
}"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import {computed, inject, ref, watch} from 'vue';
|
||
import {ImageSink, RemoteDevice} from '../../model/Remote/types';
|
||
import {MountedFsSymbol} from '../../model/injectionSymbols';
|
||
import {VirtualFile} from '../../model/FileSystem/types';
|
||
import {useLoading} from '../../helpers/withLoading';
|
||
import {useMouseDragging} from '../../helpers/mouseDragging';
|
||
import {MapLayerDrawable} from '../Map/object';
|
||
|
||
const props = defineProps<{
|
||
screen: RemoteDevice;
|
||
}>();
|
||
|
||
const imageSink = computed(() => props.screen.imageSink as ImageSink);
|
||
|
||
const fileSystem = inject(MountedFsSymbol)!;
|
||
const canvas = ref<HTMLCanvasElement>();
|
||
const isDragOver = ref(false);
|
||
const scale = ref(100);
|
||
|
||
const {isLoading, withLoading} = useLoading();
|
||
|
||
const {onMouseDown, x, y} = useMouseDragging()
|
||
|
||
function onWheel(event: WheelEvent) {
|
||
scale.value = Math.max(10, Math.min(1000, scale.value + event.deltaY / 100));
|
||
console.log('scroll', event.deltaY, scale.value);
|
||
}
|
||
|
||
watch(() => [x.value, y.value, scale.value], () => {
|
||
imageSink.value?.transform?.({
|
||
x: x.value,
|
||
y: y.value,
|
||
scale: scale.value / 100,
|
||
});
|
||
})
|
||
|
||
watch(() => imageSink.value?.display, ()=> {
|
||
if (imageSink.value?.display) {
|
||
imageSink.value?.display.connectTarget(canvas.value!);
|
||
}
|
||
});
|
||
|
||
|
||
async function drawImage(blob: Blob) {
|
||
if (blob.type.startsWith('image/')) {
|
||
const url = URL.createObjectURL(blob);
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
const ctx = canvas.value!.getContext('2d')!;
|
||
canvas.value!.width = img.width;
|
||
canvas.value!.height = img.height;
|
||
ctx.drawImage(img, 0, 0);
|
||
}
|
||
img.src = url;
|
||
} else if (blob.type.startsWith('video/')) {
|
||
const url = URL.createObjectURL(blob);
|
||
const video = document.createElement('video');
|
||
video.muted = true;
|
||
video.autoplay = true;
|
||
video.src = url;
|
||
video.oncanplaythrough = () => {
|
||
const ctx = canvas.value!.getContext('2d')!;
|
||
canvas.value!.width = video.videoWidth;
|
||
canvas.value!.height = video.videoHeight;
|
||
ctx.drawImage(video, 0, 0);
|
||
video.pause();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadFile(meta: VirtualFile, file: File) {
|
||
if (imageSink.value?.display) {
|
||
const map = new MapLayerDrawable(file, meta.map?.width);
|
||
await map.preloadFile();
|
||
imageSink.value.display.mapLayer = map
|
||
console.log(imageSink.value?.display);
|
||
}
|
||
// await withLoading(async () => {
|
||
// await Promise.all([
|
||
// imageSink.value?.transferFile(meta, file),
|
||
// drawImage(file),
|
||
// ])
|
||
// await imageSink.value?.show(meta);
|
||
// })
|
||
}
|
||
|
||
async function onDrop(event: DragEvent) {
|
||
event.preventDefault();
|
||
isDragOver.value = false;
|
||
const file = event.dataTransfer?.files?.[0];
|
||
if (file) {
|
||
const meta = {
|
||
type: 'file',
|
||
source: 'drop',
|
||
path: [] as string[],
|
||
name: file.name,
|
||
mimeType: file.type
|
||
} as const;
|
||
await loadFile(meta, file)
|
||
} else if (event.dataTransfer?.getData('application/json')) {
|
||
const meta = JSON.parse(event.dataTransfer?.getData('application/json'));
|
||
if (meta.type === 'file') {
|
||
const file = await fileSystem.download(meta);
|
||
await loadFile(meta, file);
|
||
}
|
||
}
|
||
console.log('files', event.dataTransfer?.files);
|
||
console.log('drop', event.dataTransfer?.getData('application/json'));
|
||
}
|
||
|
||
</script>
|
||
<style>
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.remote-screen {
|
||
position: relative;
|
||
display: inline-block;
|
||
border: 2px solid white;
|
||
border-radius: 4px;
|
||
background-color: black;
|
||
aspect-ratio: 16 / 9;
|
||
overflow: hidden;
|
||
max-height: 100%;
|
||
}
|
||
|
||
.remote-screen__info {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
padding: 4px;
|
||
text-align: center;
|
||
}
|
||
|
||
.remote-screen__loading {
|
||
pointer-events: none;
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 2rem;
|
||
color: white;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
|
||
span {
|
||
transform-origin: 44% 54%;
|
||
animation: spin 1000ms linear infinite;
|
||
}
|
||
}
|
||
|
||
.remote-screen__connect {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
padding: 4px;
|
||
text-align: center;
|
||
}
|
||
|
||
.remote-screen--dragover {
|
||
border-color: orange;
|
||
}
|
||
</style>
|