Can embed any file in a PNG/WebM/GIF/JPEG and upload it to a third-party host through 4chan
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

417 lines
11 KiB

<script lang="ts">
import { fileTypeFromBuffer, FileTypeResult } from 'file-type'
import { settings, appState } from './stores'
import { beforeUpdate, tick } from 'svelte'
import { EmbeddedFile, EMBED_TYPES } from './main.js';
import { createEventDispatcher } from 'svelte';
import { GM_head, headerStringToObject } from './requests.js';
export const dispatch = createEventDispatcher()
export let file: EmbeddedFile
// export let embed_enum: EMBED_ENUM = EMBED_ENUM.NONE;
let isVideo = false
let isImage = false
let isAudio = 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
})
export function isContracted() {
return contracted
}
beforeUpdate(async () => {
if(file?.isBlacklisted === true) return
if (settled) return
settled = true
if(file.thumbnail == null) return
const thumb = typeof file.thumbnail === 'function' ? (await file.thumbnail()) : file.data;
// const thumb = file.thumbnail || file.data
let type: FileTypeResult | undefined
if (thumb instanceof Buffer) {
type = await fileTypeFromBuffer(thumb);
url = URL.createObjectURL(new Blob([thumb], { type: type?.mime }))
// if (!type) return; // extracted out below for TypeScript
} else if (typeof thumb === 'string'){
let head = headerStringToObject(await GM_head(thumb, undefined))
// TODO write a proper getExt() and getMime()
type = { ext: '' as any, mime: head['content-type'].split(';')[0].trim() }
}
if (!type) return
ftype = type.mime;
isVideo = type.mime.startsWith('video/')
isAudio = type.mime.startsWith('audio/')
isImage = type.mime.startsWith('image/')
dispatch('fileinfo', { type })
if (isImage) {
contracted = !$settings.xpi
}
if (isVideo) {
contracted = !$settings.xpv && !$appState.isCatalog
}
if ($appState.isCatalog) contracted = true
if ($settings.pre) {
unzip() // not awaiting on purpose
}
if ($settings.prev) {
let obs = new IntersectionObserver(
(entries, obs) => {
for (const item of entries) {
if (!item.isIntersecting) continue
unzip()
obs.unobserve(place)
}
},
{ root: null, rootMargin: '1000px', threshold: 0.01 },
)
obs.observe(place)
}
})
let unzipping = false
let progress = [0, 0]
async function unzip() {
if(file?.isBlacklisted === true) return
if (!file.thumbnail) return
if (unzipping) return
let type: FileTypeResult | undefined
if (typeof file.data === 'function') {
unzipping = true;
let lisn = new EventTarget();
lisn.addEventListener("progress", (e: any) => {
progress = e.detail
});
let full = await file.data(lisn);
const type = await fileTypeFromBuffer(full);
furl = URL.createObjectURL(new Blob([full], { type: type?.mime }));
} else if (typeof file.data === 'string') {
url = file.data
furl = file.data
let head = headerStringToObject(await GM_head(file.data, undefined))
// TODO write a proper getExt() and getMime()
type = { ext: '' as any, mime: head['content-type'].split(';')[0].trim() } as FileTypeResult
}
// unzipping = false;
if (!type) return
isVideo = type.mime.startsWith('video/')
isAudio = type.mime.startsWith('audio/')
isImage = type.mime.startsWith('image/')
unzipping = false
dispatch('fileinfo', { type })
if (hovering) {
// reset hovering to recompute proper image coordinates
setTimeout(async () => {
while (dims[0] == 0 && dims[1] == 0) {
hoverUpdate()
await new Promise(_ => setTimeout(_, 20));
}
}, 20)
}
}
function hasAudio(video: any) {
return (
video.mozHasAudio ||
!!video.webkitAudioDecodedByteCount ||
!!(video.audioTracks && video.audioTracks.length)
)
}
export async function bepis(ev: MouseEvent) {
if (file?.isBlacklisted === true) return
if ($appState.isCatalog) return
if (ev.button == 0) {
contracted = !contracted
if (hovering) hoverStop()
if (contracted && isVideo) {
videoElem.controls = false
videoElem.pause()
}
if (!contracted && isVideo) {
videoElem.controls = true
// has to be delayed
setTimeout(async () => {
videoElem.currentTime = hoverVideo.currentTime || 0
await videoElem.play()
}, 10)
}
if (file.thumbnail && !furl) {
// don't know how you managed to click before hovering but oh well
unzip()
}
ev.preventDefault()
} else if (ev.button == 1) {
// middle click
let src = furl || url
if (ev.altKey && file.source) {
src = file.source
}
if (ev.shiftKey && file.page) {
src = file.page.url
}
ev.preventDefault()
if (isNotChrome) {
window.open(src, '_blank')
} else await GM.openInTab(src, { active: false, insert: true })
}
}
const getViewport = () =>
(typeof visualViewport != 'undefined'
? () => [visualViewport.width, visualViewport.height]
: () => [
document.documentElement.clientWidth,
document.documentElement.clientHeight,
])()
function recompute() {
const [sw, sh] = getViewport()
let [iw, ih] = [0, 0]
if (isImage) {
;[iw, ih] = [imgElem.naturalWidth, imgElem.naturalHeight]
} else {
;[iw, ih] = [videoElem.videoWidth, videoElem.videoHeight]
}
let scale = Math.min(1, sw / iw, sh / ih)
dims = [~~(iw * scale), ~~(ih * scale)]
hoverElem.style.width = `${dims[0]}px`
hoverElem.style.height = `${dims[1]}px`
}
async function hoverStart(ev?: MouseEvent) {
if (file?.isBlacklisted === true) return;
if ($settings.dh) return
if (file.thumbnail && !furl) {
unzip()
}
if (!isImage && !isVideo) return
if (!contracted) return
recompute()
hovering = true
if (isVideo) {
try {
await hoverVideo.play()
} catch (e) {
// probably didn't interact with document error, mute the video and try again?
hoverVideo.muted = true
hoverVideo.volume = 0
await hoverVideo.play()
}
}
}
function hoverStop(ev?: MouseEvent) {
if ($settings.dh) return
hovering = false
if (isVideo) hoverVideo.pause()
}
let lastev: MouseEvent | undefined
function hoverUpdate(ev?: MouseEvent) {
lastev = lastev || ev
if ($settings.dh) return
if (!contracted) return
recompute(); // yeah I gave up
const [sw, sh] = [visualViewport.width, visualViewport.height]
// shamelessly stolen from 4chanX
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
let marginX: number | string =
(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
}
function adjustAudio(ev: WheelEvent) {
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()
}
</script>
{#if !file.isBlacklisted && (!$settings.eye || visible)}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- class:hasblack={file?.isBlacklisted === true} -->
<div
class:contract={contracted}
class="place"
class:hasembed={file?.embed_type === EMBED_TYPES.MEDIA_EMBED}
class:hasext={file?.embed_type === EMBED_TYPES.THIRD_EYE}
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
referrerpolicy="no-referrer"
loop={$settings.loop}
bind:this={videoElem}
src={furl || url}
/>
<!-- assoom videos will never be loaded from thumbnails -->
{/if}
</div>
<div
bind:this={hoverElem}
class:visible={hovering && contracted}
class:unzipping
class="hoverer"
id="ihover"
>
{#if unzipping}<span class="progress">[{progress[0]} / {progress[1]}]</span
>{/if}
{#if isImage}
<img referrerpolicy="no-referrer" alt={file.filename} src={furl || url} />
{/if}
{#if isVideo}
<!-- svelte-ignore a11y-media-has-caption -->
<video
referrerpolicy="no-referrer"
loop={$settings.loop}
bind:this={hoverVideo}
src={furl || url}
/>
<!-- assoom videos will never be loaded from thumbnails -->
{/if}
</div>
{/if}
<style scoped>
.place {
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;
}
.hoverer {
display: none;
position: fixed;
pointer-events: none;
}
.visible {
display: block;
z-index: 9;
}
.contract img,
.contract video {
max-width: 125px !important;
max-height: 125px !important;
width: auto;
height: auto;
}
.place:not(.contract) video,
.place:not(.contract) img,
.hoverer > video,
.hoverer > img {
max-width: 100vw;
max-height: 100vh;
}
.place.hasembed video,
.place.hasembed img {
/* border: solid 1px deeppink; */
box-shadow: 0 0 2px 2px deeppink;
}
.place.hasext video,
.place.hasext img {
/* border: solid 1px goldenrod; */
box-shadow: 0 0 2px 2px goldenrod;
}
</style>