summaryrefslogtreecommitdiff
path: root/src/apps
diff options
context:
space:
mode:
Diffstat (limited to 'src/apps')
-rw-r--r--src/apps/audio.tsx27
-rw-r--r--src/apps/browser.tsx23
-rw-r--r--src/apps/edit_rich.tsx28
-rw-r--r--src/apps/image.tsx61
-rw-r--r--src/apps/index.ts34
-rw-r--r--src/apps/music.tsx32
-rw-r--r--src/apps/source.ts33
-rw-r--r--src/apps/video.tsx61
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>
+ }
+}