diff options
Diffstat (limited to 'src/apps')
| -rw-r--r-- | src/apps/audio.tsx | 27 | ||||
| -rw-r--r-- | src/apps/browser.tsx | 23 | ||||
| -rw-r--r-- | src/apps/edit_rich.tsx | 28 | ||||
| -rw-r--r-- | src/apps/image.tsx | 61 | ||||
| -rw-r--r-- | src/apps/index.ts | 34 | ||||
| -rw-r--r-- | src/apps/music.tsx | 32 | ||||
| -rw-r--r-- | src/apps/source.ts | 33 | ||||
| -rw-r--r-- | src/apps/video.tsx | 61 |
8 files changed, 299 insertions, 0 deletions
diff --git a/src/apps/audio.tsx b/src/apps/audio.tsx new file mode 100644 index 0000000..45d8716 --- /dev/null +++ b/src/apps/audio.tsx @@ -0,0 +1,27 @@ +import { Signal, useSignalEffect } from "@preact/signals" +import { Address } from "$/apps/source"; +import { memo, Ref, useEffect } from "preact/compat"; + +const filenameRegex = /.*\/(.*)$/; + +export const Audio = memo(function Audio({ source, title, contentReady }: Image.Props) { + useEffect(() => { + contentReady.value = true + }, []) + + useSignalEffect(() => { + const filename = decodeURIComponent(source.value.link.pathname.match(filenameRegex)?.[1] ?? source.value.link.toString()); + title.value = `${filename} - tunez player`; + }) + + + return <audio controls src={source.value.link.toString()}></audio> +}) + +namespace Image { + export type Props = { + source: Signal<Address>, + title: Signal<string | undefined>, + contentReady: Signal<boolean>, + } +} diff --git a/src/apps/browser.tsx b/src/apps/browser.tsx new file mode 100644 index 0000000..71b2f4e --- /dev/null +++ b/src/apps/browser.tsx @@ -0,0 +1,23 @@ +import { Signal, useSignalEffect } from "@preact/signals" +import { Address } from "$/apps/source"; +import { memo, useEffect } from "preact/compat"; + +export const Browser = memo(function Browser({ source, title, contentReady }: Browser.Props) { + useEffect(() => { + contentReady.value = true + }, []) + + useSignalEffect(() => { + title.value = `${source.value.link} - internet exploder`; + }) + + return <iframe src={source.value.link.toString()} allowFullScreen={ source.value.link.hostname.includes("youtube.com") } height="40vh" width="40vw"/> +}) + +namespace Browser { + export type Props = { + source: Signal<Address>, + title: Signal<string | undefined>, + contentReady: Signal<boolean> + } +} diff --git a/src/apps/edit_rich.tsx b/src/apps/edit_rich.tsx new file mode 100644 index 0000000..0b61178 --- /dev/null +++ b/src/apps/edit_rich.tsx @@ -0,0 +1,28 @@ +import { Signal, useSignalEffect } from "@preact/signals" +import { Source, VirtualFile, VirtualTextFile } from "$/apps/source"; +import { memo, useEffect } from "preact/compat"; + +export const Edit = memo(function Edit(props: Edit.Props) { + const source = props.source as Signal<VirtualFile>; + const title = props.title; + + useEffect(() => { + props.contentReady.value = true + }, []) + + useSignalEffect(() => { + title.value = `${source.value.name} - moed`; + }) + + return <div class="text_content"> + {(source.value as VirtualTextFile).content} + </div> +}) + +namespace Edit { + export type Props = { + source: Signal<Source>, + title: Signal<string | undefined>, + contentReady: Signal<boolean> + } +} diff --git a/src/apps/image.tsx b/src/apps/image.tsx new file mode 100644 index 0000000..12623c3 --- /dev/null +++ b/src/apps/image.tsx @@ -0,0 +1,61 @@ +import { effect, Signal, useSignalEffect } from "@preact/signals" +import { Address } from "$/apps/source"; +import { memo, Ref, useRef } from "preact/compat"; + +const filenameRegex = /.*\/(.*)$/; + +export const Image = memo(function Image({ source, title, contentReady, sizingFinished }: Image.Props) { + useSignalEffect(() => { + const filename = decodeURIComponent(source.value.link.pathname.match(filenameRegex)?.[1] ?? source.value.link.toString()); + title.value = `${filename} - picview`; + }) + + const imgRef: Ref<HTMLImageElement> = useRef(null); + + return <img style={{ "image-rendering": (source.value.pixelated ? "crisp-edges" : undefined) }} src={source.value.link.toString()} ref={img => { + imgRef.current = img + const id = setInterval(() => { + if (img && img.naturalWidth) { + clearInterval(id) + contentReady.value = true + const vw = Math.max(document.documentElement.clientWidth, window.innerWidth) + const vh = Math.max(document.documentElement.clientHeight, window.innerHeight) + + const maxImageSize = Math.min(vw, vh) * 0.4; + const smallestImageAxis = Math.min(img.naturalHeight, img.naturalWidth); + + const aspectRatio = img.naturalWidth / img.naturalHeight + if (smallestImageAxis > maxImageSize) { + if (img.naturalWidth > img.naturalHeight) { + img.style.height = `${maxImageSize / aspectRatio}px` + img.style.width = `${maxImageSize}px` + } else { + img.style.height = `${maxImageSize}px` + img.style.width = `${maxImageSize * aspectRatio}px` + } + + effect(() => { + if (sizingFinished.value) { + img!.style.setProperty("height", null) + img!.style.setProperty("width", null) + } + }) + } + } + }, 50) + + return () => { + clearInterval(id) + imgRef.current = null + } + }}/> +}) + +namespace Image { + export type Props = { + source: Signal<Address>, + title: Signal<string | undefined>, + contentReady: Signal<boolean>, + sizingFinished: Signal<boolean> + } +} diff --git a/src/apps/index.ts b/src/apps/index.ts new file mode 100644 index 0000000..ab9453e --- /dev/null +++ b/src/apps/index.ts @@ -0,0 +1,34 @@ +import { Music } from "./music"; +import { Edit } from "./edit_rich"; + +import { JSX } from "preact"; +import { Source, SourceTypes } from "$/apps/source"; +import { Signal } from "@preact/signals"; +import { Browser } from "./browser"; +import { Image } from "./image"; +import { Video } from "./video"; +import { Audio } from "./audio"; + +type AppProps = { + source: Signal<Source>, + title: Signal<string | undefined>, + contentReady: Signal<boolean>, + sizingFinished: Signal<boolean> +} + +// *sigh* i'd like to make props *required*, but ts doesn't let me +// https://stackoverflow.com/questions/75481900/can-i-make-an-argument-required-in-a-typescript-function-type +export type App = (props: AppProps) => JSX.Element; + +type AppRegistry = { + [SourceType in SourceTypes]: App +} + +export const APP_REGISTRY: AppRegistry = Object.create({ + listen: Music, + text: Edit, + link: Browser, + image: Image, + video: Video, + audio: Audio +}); diff --git a/src/apps/music.tsx b/src/apps/music.tsx new file mode 100644 index 0000000..5b1ef27 --- /dev/null +++ b/src/apps/music.tsx @@ -0,0 +1,32 @@ +import { useEffect, useState } from "preact/hooks" +import { Signal } from "@preact/signals"; +import { memo } from "preact/compat"; + +const Iframe = memo(function Iframe() { + return <iframe style="border: 0; height: 30em" src="https://bandcamp.com/EmbeddedPlayer/album=3374731395/size=large/bgcol=000000/linkcol=ff0047/transparent=true/" seamless><a href="https://sunnexo.bandcamp.com/album/netpunk">Netpunk by Sunnexo</a></iframe> +}) + +export const Music = memo(function Music({ title }: Music.Props) { + const [userApproves, setUserApproves] = useState(false); + + useEffect(() => { + title.value = "Stream on BandCamp" + }, []) + + if (userApproves) { + return <Iframe /> + } else { + return <div class="external_warning"> + <div>Click here to load player from <a href="https://bandcamp.com">https://bandcamp.com</a></div> + <button onClick={() => setUserApproves(true)}> + Load player + </button> + </div> + } +}) + +namespace Music { + export type Props = { + title: Signal<string | undefined>, + } +} diff --git a/src/apps/source.ts b/src/apps/source.ts new file mode 100644 index 0000000..66e8f06 --- /dev/null +++ b/src/apps/source.ts @@ -0,0 +1,33 @@ +import { JSX } from "preact/jsx-runtime" + +export interface Source { + type: string, +} + +export interface Listen extends Source { + type: "listen" +} + +export interface Address extends Source { + type: "video" | "image" | "link", + link: URL, + pixelated?: boolean +} + +export interface VirtualFile extends Source { + name: string, + height?: string | number, + width?: string | number +} + +export interface VirtualTextFile extends VirtualFile { + type: "text", + content: JSX.Element +} + +export type Sources = Address | VirtualTextFile | Listen +export type SourceTypes = Sources["type"] +export type GetSource = { + [SourceType in Sources as SourceType["type"]]: SourceType +} + diff --git a/src/apps/video.tsx b/src/apps/video.tsx new file mode 100644 index 0000000..4b556af --- /dev/null +++ b/src/apps/video.tsx @@ -0,0 +1,61 @@ +import { effect, Signal, useSignalEffect } from "@preact/signals" +import { Address } from "$/apps/source"; +import { memo, Ref, useRef } from "preact/compat"; + +const filenameRegex = /.*\/(.*)$/; + +export const Video = memo(function Video({ source, title, contentReady, sizingFinished }: Video.Props) { + useSignalEffect(() => { + const filename = decodeURIComponent(source.value.link.pathname.match(filenameRegex)?.[1] ?? source.value.link.toString()); + title.value = `${filename} - media player`; + }) + + const videoRef: Ref<HTMLVideoElement> = useRef(null); + + return <video controls src={source.value.link.toString()} ref={video => { + videoRef.current = video + const id = setInterval(() => { + if (video && video.videoWidth) { + clearInterval(id) + contentReady.value = true + const vw = Math.max(document.documentElement.clientWidth, window.innerWidth) + const vh = Math.max(document.documentElement.clientHeight, window.innerHeight) + + const maxImageSize = Math.min(vw, vh) * 0.4; + const smallestImageAxis = Math.min(video.videoHeight, video.videoWidth); + + const aspectRatio = video.videoWidth / video.videoHeight + if (smallestImageAxis > maxImageSize) { + if (video.videoWidth > video.videoHeight) { + video.style.height = `${maxImageSize / aspectRatio}px` + video.style.width = `${maxImageSize}px` + } else { + video.style.height = `${maxImageSize}px` + video.style.width = `${maxImageSize * aspectRatio}px` + } + + effect(() => { + if (sizingFinished.value) { + video!.style.setProperty("height", null) + video!.style.setProperty("width", null) + } + }) + } + } + }, 50) + + return () => { + clearInterval(id) + videoRef.current = null + } + }}/> +}) + +namespace Video { + export type Props = { + source: Signal<Address>, + title: Signal<string | undefined>, + contentReady: Signal<boolean>, + sizingFinished: Signal<boolean> + } +} |
