PEE/src/Components/Embedding.svelte

429 lines
11 KiB
Svelte
Raw Normal View History

2022-01-04 20:26:05 +00:00
<script lang="ts">
2022-04-12 23:56:18 +00:00
import { fileTypeFromBuffer, FileTypeResult } from "file-type";
import { settings, appState } from "../stores";
import { beforeUpdate, tick } from "svelte";
import type { EmbeddedFile } from "../main";
import { createEventDispatcher } from "svelte";
import { Buffer } from "buffer";
import { getHeaders, Platform } from "../platform";
import { peeTarget } from "../utils";
2022-04-12 23:56:18 +00:00
export const dispatch = createEventDispatcher();
export let file: EmbeddedFile;
2022-04-12 23:56:18 +00:00
let isVideo = false;
let isImage = false;
let isAudio = false;
let isText = false;
let url = "";
let settled = false;
let contracted = true;
let hovering = false;
let ftype = "";
let place: HTMLDivElement;
let hoverElem: HTMLDivElement;
let imgElem: HTMLImageElement;
let videoElem: HTMLVideoElement;
let hoverVideo: HTMLVideoElement;
let dims: [number, number] = [0, 0];
let furl: string | undefined = undefined;
let visible = false;
export const isNotChrome = !navigator.userAgent.includes("Chrome/");
export let id = "";
document.addEventListener("reveal", (e: any) => {
if (e.detail.id == id) visible = !visible;
});
2022-01-05 20:50:44 +00:00
2022-01-07 04:43:28 +00:00
export function isContracted() {
2022-04-12 23:56:18 +00:00
return contracted;
2022-01-07 04:43:28 +00:00
}
2022-04-12 23:56:18 +00:00
let content: Blob;
2022-01-04 20:26:05 +00:00
beforeUpdate(async () => {
2022-04-12 23:56:18 +00:00
if (settled) return;
settled = true;
2022-01-12 08:09:30 +00:00
2022-04-12 23:56:18 +00:00
const thumb = file.thumbnail || file.data;
let type: FileTypeResult | undefined;
2022-01-29 20:01:45 +00:00
if (typeof thumb != "string") {
let buff = Buffer.isBuffer(thumb) ? thumb : await thumb();
2022-04-12 23:56:18 +00:00
type = await fileTypeFromBuffer(buff);
if (
!type &&
2022-04-12 23:56:18 +00:00
file.filename.endsWith(".txt") &&
file.filename.startsWith("message")
) {
2022-04-12 23:56:18 +00:00
type = { ext: "txt", mime: "text/plain" } as any;
}
2022-04-12 23:56:18 +00:00
content = new Blob([buff], { type: type?.mime });
url = URL.createObjectURL(content);
if (!type) return;
2022-01-12 08:09:30 +00:00
} else {
2022-04-12 23:56:18 +00:00
let head = await getHeaders(thumb);
url = thumb;
2022-01-29 20:01:45 +00:00
type = {
2022-04-12 23:56:18 +00:00
ext: "" as any,
mime: head["content-type"].split(";")[0].trim() as any,
};
2022-01-12 08:09:30 +00:00
}
2022-04-12 23:56:18 +00:00
ftype = type.mime;
isVideo = type.mime.startsWith("video/");
isAudio = type.mime.startsWith("audio/");
isImage = type.mime.startsWith("image/");
isText = type.mime.startsWith("text/plain");
dispatch("fileinfo", { type });
2022-01-04 20:26:05 +00:00
2022-01-07 04:43:28 +00:00
if (isImage) {
2022-04-12 23:56:18 +00:00
contracted = !$settings.xpi;
2022-01-07 04:43:28 +00:00
}
2022-01-04 20:26:05 +00:00
if (isVideo) {
2022-04-12 23:56:18 +00:00
contracted = !$settings.xpv && !$appState.isCatalog;
2022-01-07 04:43:28 +00:00
}
2022-04-12 23:56:18 +00:00
if ($appState.isCatalog) contracted = true;
2022-01-07 04:43:28 +00:00
if ($settings.pre) {
2022-04-12 23:56:18 +00:00
unzip(); // not awaiting on purpose
2022-01-04 20:26:05 +00:00
}
2022-01-07 04:43:28 +00:00
if ($settings.prev) {
2022-01-12 08:09:30 +00:00
let obs = new IntersectionObserver(
(entries, obs) => {
for (const item of entries) {
2022-04-12 23:56:18 +00:00
if (!item.isIntersecting) continue;
unzip();
obs.unobserve(place);
2022-01-12 08:09:30 +00:00
}
},
2022-04-12 23:56:18 +00:00
{ root: null, rootMargin: "0px", threshold: 0.01 }
);
obs.observe(place);
2022-01-07 04:43:28 +00:00
}
2022-04-12 23:56:18 +00:00
});
2022-01-04 20:26:05 +00:00
2022-04-12 23:56:18 +00:00
let unzipping = false;
let progress = [0, 0];
2022-01-05 01:14:23 +00:00
async function unzip() {
2022-04-12 23:56:18 +00:00
if (!file.thumbnail) return;
if (unzipping) return;
let type: FileTypeResult | undefined;
if (typeof file.data != "string") {
unzipping = true;
let lisn = new peeTarget();
2022-04-12 23:56:18 +00:00
lisn.addEventListener("progress", (e: any) => {
progress = e.detail;
});
let full = Buffer.isBuffer(file.data) ? file.data : await file.data(lisn);
type = await fileTypeFromBuffer(full);
if (
!type &&
2022-04-12 23:56:18 +00:00
file.filename.endsWith(".txt") &&
file.filename.startsWith("message")
) {
2022-04-12 23:56:18 +00:00
type = { ext: "txt", mime: "text/plain" } as any;
}
2022-04-12 23:56:18 +00:00
content = new Blob([full], { type: type?.mime });
furl = URL.createObjectURL(content);
2022-01-12 08:09:30 +00:00
} else {
2022-04-12 23:56:18 +00:00
url = file.data;
furl = file.data;
let head = await getHeaders(file.data);
2022-01-29 20:01:45 +00:00
type = {
2022-04-12 23:56:18 +00:00
ext: "" as any,
mime: head["content-type"].split(";")[0].trim() as any,
};
2022-01-12 08:09:30 +00:00
}
2022-04-12 23:56:18 +00:00
if (!type) return;
ftype = type.mime;
2022-04-12 23:56:18 +00:00
isVideo = type.mime.startsWith("video/");
isAudio = type.mime.startsWith("audio/");
isImage = type.mime.startsWith("image/");
isText = type.mime.startsWith("text/plain");
unzipping = false;
2022-01-12 08:09:30 +00:00
2022-04-12 23:56:18 +00:00
dispatch("fileinfo", { type });
2022-01-07 04:43:28 +00:00
2022-01-05 01:14:23 +00:00
if (hovering) {
// reset hovering to recompute proper image coordinates
2022-01-12 10:12:26 +00:00
setTimeout(async () => {
2022-01-13 09:42:40 +00:00
do {
2022-04-12 23:56:18 +00:00
hoverUpdate();
await new Promise((_) => setTimeout(_, 20));
} while (dims[0] == 0 && dims[1] == 0);
}, 20);
2022-01-05 01:14:23 +00:00
}
}
2022-01-04 20:26:05 +00:00
function hasAudio(video: any) {
return (
video.mozHasAudio ||
2022-01-12 08:09:30 +00:00
!!video.webkitAudioDecodedByteCount ||
2022-01-05 01:14:23 +00:00
!!(video.audioTracks && video.audioTracks.length)
2022-04-12 23:56:18 +00:00
);
2022-01-04 20:26:05 +00:00
}
export let inhibitExpand: boolean = false;
2022-01-07 04:43:28 +00:00
export async function bepis(ev: MouseEvent) {
dispatch("click");
if (inhibitExpand) return;
2022-04-12 23:56:18 +00:00
if ($appState.isCatalog) return;
2022-01-07 04:43:28 +00:00
if (ev.button == 0) {
2022-04-12 23:56:18 +00:00
contracted = !contracted;
if (hovering) hoverStop();
2022-01-07 04:43:28 +00:00
if (contracted && isVideo) {
2022-04-12 23:56:18 +00:00
videoElem.controls = false;
videoElem.pause();
2022-01-07 04:43:28 +00:00
}
if (!contracted && isVideo) {
2022-04-12 23:56:18 +00:00
videoElem.controls = true;
2022-01-07 04:43:28 +00:00
// has to be delayed
setTimeout(async () => {
2022-04-12 23:56:18 +00:00
videoElem.currentTime = hoverVideo.currentTime || 0;
await videoElem.play();
}, 10);
2022-01-07 04:43:28 +00:00
}
if (file.thumbnail && !furl) {
// don't know how you managed to click before hovering but oh well
2022-04-12 23:56:18 +00:00
unzip();
}
2022-04-12 23:56:18 +00:00
ev.preventDefault();
2022-01-12 08:09:30 +00:00
} else if (ev.button == 1) {
// middle click
2022-04-12 23:56:18 +00:00
let src = furl || url;
2022-01-07 04:43:28 +00:00
if (ev.altKey && file.source) {
2022-04-12 23:56:18 +00:00
src = file.source;
2022-01-07 04:43:28 +00:00
}
if (ev.shiftKey && file.page) {
2022-04-12 23:56:18 +00:00
src = file.page.url;
2022-01-07 04:43:28 +00:00
}
2022-04-12 23:56:18 +00:00
ev.preventDefault();
2022-01-07 04:43:28 +00:00
if (isNotChrome) {
2022-04-12 23:56:18 +00:00
window.open(src, "_blank");
} else await Platform.openInTab(src, { active: false, insert: true });
2022-01-04 20:26:05 +00:00
}
}
2022-01-12 08:09:30 +00:00
const getViewport = () =>
2022-04-12 23:56:18 +00:00
(typeof visualViewport != "undefined"
2022-01-12 08:09:30 +00:00
? () => [visualViewport.width, visualViewport.height]
: () => [
document.documentElement.clientWidth,
document.documentElement.clientHeight,
2022-04-12 23:56:18 +00:00
])();
2022-01-05 01:14:23 +00:00
function recompute() {
2022-04-12 23:56:18 +00:00
const [sw, sh] = getViewport();
2022-01-04 20:26:05 +00:00
2022-04-12 23:56:18 +00:00
let [iw, ih] = [0, 0];
2022-01-04 20:26:05 +00:00
if (isImage) {
2022-04-12 23:56:18 +00:00
[iw, ih] = [imgElem.naturalWidth, imgElem.naturalHeight];
} else if (isVideo) {
2022-04-12 23:56:18 +00:00
[iw, ih] = [videoElem.videoWidth, videoElem.videoHeight];
2022-01-04 20:26:05 +00:00
}
2022-04-12 23:56:18 +00:00
let scale = Math.min(1, sw / iw, sh / ih);
dims = [~~(iw * scale), ~~(ih * scale)];
2022-01-04 20:26:05 +00:00
2022-04-12 23:56:18 +00:00
hoverElem.style.width = `${dims[0]}px`;
hoverElem.style.height = `${dims[1]}px`;
2022-01-05 01:14:23 +00:00
}
async function hoverStart(ev?: MouseEvent) {
2022-04-12 23:56:18 +00:00
if (!(isVideo || isImage)) return;
if ($settings.dh) return;
2022-01-05 01:14:23 +00:00
if (file.thumbnail && !furl) {
2022-04-12 23:56:18 +00:00
unzip();
2022-01-05 01:14:23 +00:00
}
2022-04-12 23:56:18 +00:00
if (!isImage && !isVideo) return;
if (!contracted) return;
2022-01-05 01:14:23 +00:00
2022-04-12 23:56:18 +00:00
recompute();
hovering = true;
2022-01-05 01:14:23 +00:00
2022-01-12 08:09:30 +00:00
if (isVideo) {
2022-01-05 15:56:45 +00:00
try {
2022-04-12 23:56:18 +00:00
await hoverVideo.play();
2022-01-12 08:09:30 +00:00
} catch (e) {
// probably didn't interact with document error, mute the video and try again?
2022-04-12 23:56:18 +00:00
hoverVideo.muted = true;
hoverVideo.volume = 0;
await hoverVideo.play();
2022-01-05 15:56:45 +00:00
}
2022-01-05 01:14:23 +00:00
}
2022-01-12 08:09:30 +00:00
}
2022-01-04 20:26:05 +00:00
function hoverStop(ev?: MouseEvent) {
2022-04-12 23:56:18 +00:00
if ($settings.dh) return;
hovering = false;
if (isVideo) hoverVideo.pause();
2022-01-04 20:26:05 +00:00
}
2022-04-12 23:56:18 +00:00
let lastev: MouseEvent | undefined;
function hoverUpdate(ev?: MouseEvent) {
2022-04-12 23:56:18 +00:00
lastev = lastev || ev;
if ($settings.dh) return;
if (!contracted) return;
if (!(isVideo || isImage)) return;
recompute(); // yeah I gave up
const [sw, sh] = [visualViewport.width, visualViewport.height];
2022-01-04 20:26:05 +00:00
// shamelessly stolen from 4chanX
2022-04-12 23:56:18 +00:00
if (dims[0] == 0 && dims[1] == 0) recompute();
let width = dims[0];
let height = dims[1] + 25;
let { clientX, clientY } = ev || lastev!;
let top = Math.max(0, (clientY * (sh - height)) / sh);
let threshold = sw / 2;
2022-01-04 20:26:05 +00:00
let marginX: number | string =
2022-04-12 23:56:18 +00:00
(clientX <= threshold ? clientX : sw - clientX) + 45;
marginX = Math.min(marginX, sw - width);
marginX = marginX + "px";
let [left, right] = clientX <= threshold ? [marginX, ""] : ["", marginX];
let { style } = hoverElem;
style.top = top + "px";
style.left = left;
style.right = right;
2022-01-04 20:26:05 +00:00
}
function adjustAudio(ev: WheelEvent) {
2022-04-12 23:56:18 +00:00
if (!$settings.ca) return;
if (!isVideo) return;
if ($settings.dh && contracted) return;
if (!hasAudio(videoElem)) return;
let vol = videoElem.volume * (ev.deltaY > 0 ? 0.9 : 1.1);
vol = Math.max(0, Math.min(1, vol));
videoElem.volume = vol;
hoverVideo.volume = videoElem.volume;
hoverVideo.muted = vol < 0;
ev.preventDefault();
2022-01-04 20:26:05 +00:00
}
</script>
2022-04-12 23:56:18 +00:00
{#if !isText}
{#if !$settings.eye || visible}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<div
class:contract={contracted}
2022-04-28 10:35:52 +00:00
class="plaace"
2022-04-12 23:56:18 +00:00
on:click={(e) => e.preventDefault()}
on:auxclick={(e) => e.preventDefault()}
on:mousedown={bepis}
on:mouseover={hoverStart}
on:mouseout={hoverStop}
on:mousemove={hoverUpdate}
on:wheel={adjustAudio}
bind:this={place}
>
{#if isImage}
<!-- svelte-ignore a11y-missing-attribute -->
<img
referrerpolicy="no-referrer"
bind:this={imgElem}
alt={file.filename}
src={furl || url}
/>
{/if}
{#if isAudio}
<audio
referrerpolicy="no-referrer"
controls
src={furl || url}
loop={$settings.loop}
alt={file.filename}
>
<source src={furl || url} type={ftype} />
</audio>
{/if}
{#if isVideo}
<!-- svelte-ignore a11y-media-has-caption -->
<!-- svelte-ignore a11y-missing-attribute -->
<video
type={ftype}
referrerpolicy="no-referrer"
loop={$settings.loop}
bind:this={videoElem}
>
<source referrerpolicy="no-referrer" src={furl || url} />
</video>
<!-- assoom videos will never be loaded from thumbnails -->
{/if}
</div>
<div
bind:this={hoverElem}
class:visible={hovering && contracted}
class:unzipping
class="hoverer"
>
{#if unzipping}<span class="progress"
>[{progress[0]} / {progress[1]}]</span
>{/if}
{#if isImage}
<img alt={file.filename} src={furl || url} />
{/if}
{#if isVideo}
<!-- svelte-ignore a11y-media-has-caption -->
<video loop={$settings.loop} bind:this={hoverVideo}>
<source type={ftype} src={furl || url} data-test />
</video>
<!-- assoom videos will never be loaded from thumbnails -->
{/if}
</div>
{/if}
2022-01-05 19:12:12 +00:00
{/if}
2022-01-04 20:26:05 +00:00
<style scoped>
2022-04-28 10:35:52 +00:00
.plaace {
2022-01-04 20:26:05 +00:00
cursor: pointer;
max-width: 100vw;
max-height: 100vh;
}
.unzipping > img {
filter: brightness(0.5) blur(10px);
}
.progress {
color: black;
-webkit-text-stroke: 0.7px white;
font-weight: bold;
left: 50%;
top: 50%;
font-size: larger;
display: inline-block;
position: absolute;
z-index: 10;
2022-01-04 20:26:05 +00:00
}
.hoverer {
display: none;
position: fixed;
pointer-events: none;
}
.visible {
display: block;
2022-01-05 15:56:45 +00:00
z-index: 9;
2022-01-04 20:26:05 +00:00
}
.contract img,
.contract video {
2022-01-07 07:05:59 +00:00
max-width: 125px !important;
max-height: 125px !important;
2022-01-04 20:26:05 +00:00
width: auto;
height: auto;
}
2022-04-28 10:35:52 +00:00
.plaace:not(.contract) video,
.plaace:not(.contract) img,
2022-01-04 20:26:05 +00:00
.hoverer > video,
.hoverer > img {
2022-01-05 15:56:45 +00:00
max-width: 100vw;
max-height: 100vh;
2022-01-04 20:26:05 +00:00
}
</style>