| author | |
| committer | |
| log | e258365f5bba27170707594f3548c6a9753532a5 |
| tree | 3542a526d12afc339b137f2dd1fc80f6b86678c1 |
| parent | 579f44c68f4fd91813fbb5adcac82a4736b6c12d |
| signature | Commit is signed but in an unrecognized format. |
19 files changed, 343 insertions(+), 88 deletions(-)
framework/backend/entry-node.ts+3-1| ... | ... | @@ -3,7 +3,9 @@ import "#debug"; |
| 3 | 3 | |
| 4 | 4 | const protocol = "http"; |
| 5 | 5 | |
| 6 | const server = serve(app, ({ address, port }) => { | |
| 6 | const server = serve({ | |
| 7 | fetch: app.fetch, | |
| 8 | }, ({ address, port }) => { | |
| 7 | 9 | if (address === "::") address = "::1"; |
| 8 | 10 | console.info(url.format({ |
| 9 | 11 | protocol, |
framework/engine/jsx-runtime.ts+13-5| ... | ... | @@ -17,13 +17,21 @@ export function jsxDEV( |
| 17 | 17 | _key: string, |
| 18 | 18 | // Unused with the clover engine |
| 19 | 19 | _isStaticChildren: boolean, |
| 20 | // Unused with the clover engine | |
| 21 | _source: unknown, | |
| 20 | source: engine.SrcLoc, | |
| 22 | 21 | ): engine.Element { |
| 22 | const { fileName, lineNumber, columnNumber } = source; | |
| 23 | ||
| 24 | // Assert the component type is valid to render. | |
| 23 | 25 | if (typeof type !== "function" && typeof type !== "string") { |
| 24 | throw new Error("Invalid component type: " + engine.inspect(type)); | |
| 26 | throw new Error( | |
| 27 | `Invalid component type at ${fileName}:${lineNumber}:${columnNumber}: ` + | |
| 28 | engine.inspect(type) + | |
| 29 | ". Clover SSR element must be a function or string", | |
| 30 | ); | |
| 25 | 31 | } |
| 26 | return [engine.kElement, type, props]; | |
| 32 | ||
| 33 | // Construct an `ssr.Element` | |
| 34 | return [engine.kElement, type, props, "", source]; | |
| 27 | 35 | } |
| 28 | 36 | |
| 29 | 37 | // jsxs |
| ... | ... | @@ -37,7 +45,7 @@ declare global { |
| 37 | 45 | interface ElementChildrenAttribute { |
| 38 | 46 | children: Node; |
| 39 | 47 | } |
| 40 | type Element = engine.Node; | |
| 48 | type Element = engine.Element; | |
| 41 | 49 | type ElementType = keyof IntrinsicElements | engine.Component; |
| 42 | 50 | type ElementClass = ReturnType<engine.Component>; |
| 43 | 51 | } |
framework/engine/marko-runtime.ts+4-3| ... | ... | @@ -123,14 +123,15 @@ export function escapeXML(input: unknown) { |
| 123 | 123 | // The rationale of this check is that the default toString method |
| 124 | 124 | // creating `[object Object]` is universally useless to any end user. |
| 125 | 125 | if ( |
| 126 | input == null || | |
| 126 | 127 | (typeof input === "object" && input && |
| 127 | 128 | // only block this if it's the default `toString` |
| 128 | 129 | input.toString === Object.prototype.toString) |
| 129 | 130 | ) { |
| 130 | 131 | throw new Error( |
| 131 | `Unexpected object in template placeholder: '` + | |
| 132 | engine.inspect({ name: "clover" }) + "'. " + | |
| 133 | `To emit a literal '[object Object]', use \${String(value)}`, | |
| 132 | `Unexpected value in template placeholder: '` + | |
| 133 | engine.inspect(input) + "'. " + | |
| 134 | `To emit a literal '${input}', use \${String(value)}`, | |
| 134 | 135 | ); |
| 135 | 136 | } |
| 136 | 137 | return marko.escapeXML(input); |
framework/engine/ssr.ts+17-3| ... | ... | @@ -79,6 +79,8 @@ export type Element = [ |
| 79 | 79 | tag: typeof kElement, |
| 80 | 80 | type: string | Component, |
| 81 | 81 | props: Record<string, unknown>, |
| 82 | _?: "", | |
| 83 | source?: SrcLoc, | |
| 82 | 84 | ]; |
| 83 | 85 | export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; |
| 84 | 86 | /** |
| ... | ... | @@ -88,6 +90,12 @@ export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; |
| 88 | 90 | export type Component = ( |
| 89 | 91 | props: Record<any, any>, |
| 90 | 92 | ) => Exclude<Node, undefined>; |
| 93 | /** Emitted by JSX runtime */ | |
| 94 | export interface SrcLoc { | |
| 95 | fileName: string; | |
| 96 | lineNumber: number; | |
| 97 | columnNumber: number; | |
| 98 | } | |
| 91 | 99 | |
| 92 | 100 | /** |
| 93 | 101 | * Resolution narrows the type 'Node' into 'ResolvedNode'. Async trees are |
| ... | ... | @@ -130,9 +138,15 @@ export function resolveNode(r: Render, node: unknown): ResolvedNode { |
| 130 | 138 | const { 1: tag, 2: props } = node; |
| 131 | 139 | if (typeof tag === "function") { |
| 132 | 140 | currentRender = r; |
| 133 | const result = tag(props); | |
| 134 | currentRender = null; | |
| 135 | return resolveNode(r, result); | |
| 141 | try { | |
| 142 | return resolveNode(r, tag(props)); | |
| 143 | } catch (e) { | |
| 144 | const { 4: src } = node; | |
| 145 | if (e && typeof e === "object") { | |
| 146 | } | |
| 147 | } finally { | |
| 148 | currentRender = null; | |
| 149 | } | |
| 136 | 150 | } |
| 137 | 151 | if (typeof tag !== "string") throw new Error("Unexpected " + typeof type); |
| 138 | 152 | const children = props?.children; |
framework/hot.ts+1| ... | ... | @@ -145,6 +145,7 @@ function loadEsbuildCode( |
| 145 | 145 | target: "esnext", |
| 146 | 146 | jsx: "automatic", |
| 147 | 147 | jsxImportSource: "#ssr", |
| 148 | jsxDev: true, | |
| 148 | 149 | sourcefile: filepath, |
| 149 | 150 | }).code; |
| 150 | 151 | return module._compile(src, filepath, "commonjs"); |
src/backend.ts-1| ... | ... | @@ -11,7 +11,6 @@ app.use(admin.middleware); |
| 11 | 11 | |
| 12 | 12 | app.route("", require("./q+a/backend.ts").app); |
| 13 | 13 | app.route("", require("./file-viewer/backend.tsx").app); |
| 14 | // app.route("", require("./friends/backend.tsx").app); | |
| 15 | 14 | |
| 16 | 15 | app.use(assets.middleware); |
| 17 | 16 |
src/file-viewer/backend.tsx+13-3| ... | ... | @@ -42,9 +42,15 @@ app.post("/file/cotyledon", async (c) => { |
| 42 | 42 | }); |
| 43 | 43 | |
| 44 | 44 | app.get("/file/*", async (c, next) => { |
| 45 | if (c.req.header("User-Agent")?.toLowerCase()?.includes("discordbot")) { | |
| 45 | const ua = c.req.header("User-Agent")?.toLowerCase() ?? ""; | |
| 46 | const lofi = ua.includes("msie") || ua.includes("rv:") || false; | |
| 47 | ||
| 48 | // Discord ignores 'robots.txt' which violates the license agreement. | |
| 49 | if (ua.includes("discordbot")) { | |
| 46 | 50 | return next(); |
| 47 | 51 | } |
| 52 | console.log(ua, lofi); | |
| 53 | ||
| 48 | 54 | let rawFilePath = c.req.path.slice(5) || "/"; |
| 49 | 55 | if (rawFilePath.endsWith("$partial")) { |
| 50 | 56 | return getPartialPage(c, rawFilePath.slice(0, -"$partial".length)); |
| ... | ... | @@ -88,7 +94,7 @@ app.get("/file/*", async (c, next) => { |
| 88 | 94 | } satisfies APIDirectoryList; |
| 89 | 95 | return c.json(json); |
| 90 | 96 | } |
| 91 | c.res = await renderView(c, "file-viewer/clofi", { | |
| 97 | c.res = await renderView(c, `file-viewer/${lofi ? "lofi" : "clofi"}`, { | |
| 92 | 98 | file, |
| 93 | 99 | hasCotyledonCookie, |
| 94 | 100 | }); |
| ... | ... | @@ -100,7 +106,11 @@ app.get("/file/*", async (c, next) => { |
| 100 | 106 | if (c.req.query("dl") !== undefined) { |
| 101 | 107 | viewMode = "download"; |
| 102 | 108 | } |
| 103 | if (viewMode == undefined && c.req.header("Accept")?.includes("text/html")) { | |
| 109 | if ( | |
| 110 | viewMode == undefined && | |
| 111 | c.req.header("Accept")?.includes("text/html") && | |
| 112 | !lofi | |
| 113 | ) { | |
| 104 | 114 | prefetchFile(file.path); |
| 105 | 115 | c.res = await renderView(c, "file-viewer/clofi", { |
| 106 | 116 | file, |
src/file-viewer/format.ts+31-9| ... | ... | @@ -9,15 +9,15 @@ export function formatSize(bytes: number) { |
| 9 | 9 | return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; |
| 10 | 10 | } |
| 11 | 11 | |
| 12 | export function formatDate(date: Date) { | |
| 13 | // YYYY-MM-DD, format in PST timezone | |
| 14 | return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" }); | |
| 15 | } | |
| 16 | ||
| 17 | export function formatShortDate(date: Date) { | |
| 18 | // YY-MM-DD, format in PST timezone | |
| 19 | return formatDate(date).slice(2); | |
| 20 | } | |
| 12 | // export function formatDateDefined(date: Date) { | |
| 13 | // // YYYY-MM-DD, format in PST timezone | |
| 14 | // return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" }); | |
| 15 | // } | |
| 16 | // | |
| 17 | // export function formatShortDate(date: Date) { | |
| 18 | // // YY-MM-DD, format in PST timezone | |
| 19 | // return formatDate(date).slice(2); | |
| 20 | // } | |
| 21 | 21 | |
| 22 | 22 | export function formatDuration(seconds: number) { |
| 23 | 23 | const minutes = Math.floor(seconds / 60); |
| ... | ... | @@ -255,5 +255,27 @@ export function highlightHashComments(text: string) { |
| 255 | 255 | .join("\n"); |
| 256 | 256 | } |
| 257 | 257 | |
| 258 | const unknownDate = new Date("1970-01-03"); | |
| 259 | const unknownDateWithKnownYear = new Date("1970-02-20"); | |
| 260 | ||
| 261 | export function formatDate(dateTime: Date) { | |
| 262 | return dateTime < unknownDateWithKnownYear | |
| 263 | ? ( | |
| 264 | dateTime < unknownDate | |
| 265 | ? ( | |
| 266 | "??.??.??" | |
| 267 | ) | |
| 268 | : `xx.xx.${21 + Math.floor(dateTime.getTime() / 86400000)}` | |
| 269 | ) | |
| 270 | : ( | |
| 271 | `${(dateTime.getMonth() + 1).toString().padStart(2, "0")}.${ | |
| 272 | dateTime | |
| 273 | .getDate() | |
| 274 | .toString() | |
| 275 | .padStart(2, "0") | |
| 276 | }.${dateTime.getFullYear().toString().slice(2)}` | |
| 277 | ); | |
| 278 | } | |
| 279 | ||
| 258 | 280 | import type { MediaFile } from "@/file-viewer/models/MediaFile.ts"; |
| 259 | 281 | import { escapeHtml } from "#ssr"; |
src/file-viewer/pages/file.404.tsx created+34| ... | ... | @@ -0,0 +1,34 @@ |
| 1 | import { MediaFile } from "../models/MediaFile.ts"; | |
| 2 | import { MediaPanel } from "../views/clofi.tsx"; | |
| 3 | import { addScript } from "#sitegen"; | |
| 4 | ||
| 5 | export const theme = { | |
| 6 | bg: "#312652", | |
| 7 | fg: "#f0f0ff", | |
| 8 | primary: "#fabe32", | |
| 9 | }; | |
| 10 | ||
| 11 | export const meta = { title: "file not found" }; | |
| 12 | ||
| 13 | export default function CotyledonPage() { | |
| 14 | addScript("../scripts/canvas_cotyledon.client.ts"); | |
| 15 | return ( | |
| 16 | <div class="files ctld ctld-sb"> | |
| 17 | <MediaPanel | |
| 18 | file={MediaFile.getByPath("/")!} | |
| 19 | isLast={false} | |
| 20 | activeFilename={null} | |
| 21 | hasCotyledonCookie={false} | |
| 22 | /> | |
| 23 | <div class="panel last"> | |
| 24 | <div className="header"></div> | |
| 25 | <div className="content file-view notfound"> | |
| 26 | <p>this file does not exist ...</p> | |
| 27 | <p> | |
| 28 | <a href="/file">return</a> | |
| 29 | </p> | |
| 30 | </div> | |
| 31 | </div> | |
| 32 | </div> | |
| 33 | ); | |
| 34 | } |
src/file-viewer/pages/file.cotyledon_speedbump.tsx+1-1| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import { MediaFile } from "../models/MediaFile.ts"; |
| 2 | import { Speedbump } from "../cotyledon.tsx"; | |
| 3 | 2 | import { MediaPanel } from "../views/clofi.tsx"; |
| 4 | 3 | import { addScript } from "#sitegen"; |
| 4 | import { Speedbump } from "../cotyledon.tsx"; | |
| 5 | 5 | |
| 6 | 6 | export const theme = { |
| 7 | 7 | bg: "#312652", |
src/file-viewer/sort.ts created+58| ... | ... | @@ -0,0 +1,58 @@ |
| 1 | export function splitRootDirFiles(dir: MediaFile, hasCotyledonCookie: boolean) { | |
| 2 | const children = dir.getPublicChildren(); | |
| 3 | let readme: MediaFile | null = null; | |
| 4 | ||
| 5 | const groups = { | |
| 6 | // years 2025 and onwards | |
| 7 | years: [] as MediaFile[], | |
| 8 | // named categories | |
| 9 | categories: [] as MediaFile[], | |
| 10 | // years 2017 to 2024 | |
| 11 | cotyledon: [] as MediaFile[], | |
| 12 | }; | |
| 13 | const colorMap: Record<string, string> = { | |
| 14 | years: "#a2ff91", | |
| 15 | categories: "#9c91ff", | |
| 16 | cotyledon: "#ff91ca", | |
| 17 | }; | |
| 18 | for (const child of children) { | |
| 19 | const basename = child.basename; | |
| 20 | if (basename === "readme.txt") { | |
| 21 | readme = child; | |
| 22 | continue; | |
| 23 | } | |
| 24 | ||
| 25 | const year = basename.match(/^(\d{4})/); | |
| 26 | if (year) { | |
| 27 | const n = parseInt(year[1]); | |
| 28 | if (n >= 2025) { | |
| 29 | groups.years.push(child); | |
| 30 | } else { | |
| 31 | groups.cotyledon.push(child); | |
| 32 | } | |
| 33 | } else { | |
| 34 | groups.categories.push(child); | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | let sections = []; | |
| 39 | for (const [key, files] of Object.entries(groups)) { | |
| 40 | if (key === "cotyledon" && !hasCotyledonCookie) { | |
| 41 | continue; | |
| 42 | } | |
| 43 | if (key === "years" || key === "cotyledon") { | |
| 44 | files.sort((a, b) => { | |
| 45 | return b.basename.localeCompare(a.basename); | |
| 46 | }); | |
| 47 | } else { | |
| 48 | files.sort((a, b) => { | |
| 49 | return a.basename.localeCompare(b.basename); | |
| 50 | }); | |
| 51 | } | |
| 52 | sections.push({ key, titleColor: colorMap[key], files }); | |
| 53 | } | |
| 54 | ||
| 55 | return { readme, sections }; | |
| 56 | } | |
| 57 | ||
| 58 | import { MediaFile } from "./models/MediaFile.ts"; |
src/file-viewer/views/canvas.astro deleted-12| ... | ... | @@ -1,12 +0,0 @@ |
| 1 | --- | |
| 2 | import { useInlineScript } from "../framework/page-resources.ts"; | |
| 3 | ||
| 4 | const { script } = Astro.props; | |
| 5 | useInlineScript('canvas_' + script as any); | |
| 6 | useInlineScript('canvas_demo'); | |
| 7 | --- | |
| 8 | <canvas id="canvas" | |
| 9 | data-canvas={script} | |
| 10 | data-standalone="true" | |
| 11 | style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;" | |
| 12 | ></canvas> |
src/file-viewer/views/canvas.marko created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | export interface Input { | |
| 2 | script: string; | |
| 3 | } | |
| 4 | export const meta = { title: "canvas" }; | |
| 5 | ||
| 6 | <const/{ script } = input /> | |
| 7 | ||
| 8 | <canvas id="canvas" | |
| 9 | data-canvas=script | |
| 10 | data-standalone="true" | |
| 11 | style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;" | |
| 12 | ></canvas> | |
| 13 | <AddScript="file-viewer/scripts/canvas_" + script + ".client.ts" /> | |
| 14 | ||
| 15 | client import "./canvas.client.ts"; | |
| 16 | ||
| 17 | import { addScript as AddScript } from '#sitegen'; |
src/file-viewer/views/clofi.css+9| ... | ... | @@ -687,6 +687,15 @@ h3 { |
| 687 | 687 | transform: translateY(0); |
| 688 | 688 | } |
| 689 | 689 | } |
| 690 | .notfound { | |
| 691 | p { | |
| 692 | text-align: center; | |
| 693 | } | |
| 694 | a { | |
| 695 | text-decoration: underline; | |
| 696 | } | |
| 697 | } | |
| 698 | ||
| 690 | 699 | /* MOBILE */ |
| 691 | 700 | @media (max-width: 1000px) { |
| 692 | 701 | html, body { |
src/file-viewer/views/clofi.tsx+5-49| ... | ... | @@ -406,7 +406,7 @@ function ListItem({ |
| 406 | 406 | noDate?: boolean; |
| 407 | 407 | }) { |
| 408 | 408 | const dateTime = file.date; |
| 409 | let shortDate = dateTime < unknownDateWithKnownYear | |
| 409 | const shortDate = dateTime < unknownDateWithKnownYear | |
| 410 | 410 | ? ( |
| 411 | 411 | dateTime < unknownDate |
| 412 | 412 | ? ( |
| ... | ... | @@ -565,40 +565,7 @@ function RootDirView({ |
| 565 | 565 | hasCotyledonCookie: boolean; |
| 566 | 566 | }) { |
| 567 | 567 | const children = dir.getPublicChildren(); |
| 568 | let readme: MediaFile | null = null; | |
| 569 | ||
| 570 | const groups = { | |
| 571 | // years 2025 and onwards | |
| 572 | years: [] as MediaFile[], | |
| 573 | // named categories | |
| 574 | categories: [] as MediaFile[], | |
| 575 | // years 2017 to 2024 | |
| 576 | cotyledon: [] as MediaFile[], | |
| 577 | }; | |
| 578 | const colorMap = { | |
| 579 | years: "#a2ff91", | |
| 580 | categories: "#9c91ff", | |
| 581 | cotyledon: "#ff91ca", | |
| 582 | }; | |
| 583 | for (const child of children) { | |
| 584 | const basename = child.basename; | |
| 585 | if (basename === readmeFile) { | |
| 586 | readme = child; | |
| 587 | continue; | |
| 588 | } | |
| 589 | ||
| 590 | const year = basename.match(/^(\d{4})/); | |
| 591 | if (year) { | |
| 592 | const n = parseInt(year[1]); | |
| 593 | if (n >= 2025) { | |
| 594 | groups.years.push(child); | |
| 595 | } else { | |
| 596 | groups.cotyledon.push(child); | |
| 597 | } | |
| 598 | } else { | |
| 599 | groups.categories.push(child); | |
| 600 | } | |
| 601 | } | |
| 568 | const { readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie); | |
| 602 | 569 | |
| 603 | 570 | if (readme && isLast) activeFilename ||= readmeFile; |
| 604 | 571 | |
| ... | ... | @@ -613,22 +580,10 @@ function RootDirView({ |
| 613 | 580 | /> |
| 614 | 581 | </ul> |
| 615 | 582 | )} |
| 616 | {Object.entries(groups).map(([key, files]) => { | |
| 617 | if (key === "cotyledon" && !hasCotyledonCookie) { | |
| 618 | return null; | |
| 619 | } | |
| 620 | if (key === "years" || key === "cotyledon") { | |
| 621 | files.sort((a, b) => { | |
| 622 | return b.basename.localeCompare(a.basename); | |
| 623 | }); | |
| 624 | } else { | |
| 625 | files.sort((a, b) => { | |
| 626 | return a.basename.localeCompare(b.basename); | |
| 627 | }); | |
| 628 | } | |
| 583 | {sections.map(({ key, titleColor, files }) => { | |
| 629 | 584 | return ( |
| 630 | 585 | <div class="group" data-group={key}> |
| 631 | <h3 style={`color:${colorMap[key as keyof typeof colorMap]};`}> | |
| 586 | <h3 style={{ color: titleColor }}> | |
| 632 | 587 | {key} |
| 633 | 588 | </h3> |
| 634 | 589 | <ul> |
| ... | ... | @@ -1001,3 +956,4 @@ import { |
| 1001 | 956 | } from "@/file-viewer/format.ts"; |
| 1002 | 957 | import { ForEveryone } from "@/file-viewer/cotyledon.tsx"; |
| 1003 | 958 | import * as mime from "#sitegen/mime"; |
| 959 | import * as sort from "../sort.ts"; |
src/file-viewer/views/lofi.css created+43| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | body { | |
| 2 | margin: 0; | |
| 3 | padding: 0; | |
| 4 | } | |
| 5 | #lofi { | |
| 6 | padding: 32px; | |
| 7 | } | |
| 8 | h1 { | |
| 9 | margin-top: 0; | |
| 10 | font-size: 3em; | |
| 11 | color: var(--primary); | |
| 12 | font-family: monospace; | |
| 13 | } | |
| 14 | ul, li { | |
| 15 | margin: 0; | |
| 16 | padding: 0; | |
| 17 | list-style-type: none; | |
| 18 | } | |
| 19 | ul { | |
| 20 | padding-right: 4em; | |
| 21 | } | |
| 22 | li a { | |
| 23 | display: block; | |
| 24 | color: white; | |
| 25 | line-height: 2em; | |
| 26 | padding: 0 1em; | |
| 27 | border-radius: 4px; | |
| 28 | } | |
| 29 | li a:hover { | |
| 30 | background-color: rgba(255,255,255,0.2); | |
| 31 | font-weight: bold; | |
| 32 | text-decoration: none!important; | |
| 33 | } | |
| 34 | .dir a { | |
| 35 | color: #99eeFF | |
| 36 | } | |
| 37 | .ext { | |
| 38 | opacity: 0.5; | |
| 39 | } | |
| 40 | .meta { | |
| 41 | margin-left: 1em; | |
| 42 | opacity: 0.75; | |
| 43 | } |
src/file-viewer/views/lofi.marko created+71| ... | ... | @@ -0,0 +1,71 @@ |
| 1 | import "./lofi.css"; | |
| 2 | export interface Input { | |
| 3 | file: MediaFile; | |
| 4 | hasCotyledonCookie: boolean; | |
| 5 | } | |
| 6 | export { meta, theme } from "./clofi.tsx"; | |
| 7 | ||
| 8 | <const/{ file: dir, hasCotyledonCookie } = input /> | |
| 9 | <const/{ path: fullPath, dirname, basename, kind } = dir /> | |
| 10 | <const/isRoot = fullPath == '/'> | |
| 11 | ||
| 12 | <div#lofi> | |
| 13 | ||
| 14 | <define/ListItem|{ value: file }|> | |
| 15 | <const/dir=file.kind === MediaFileKind.directory/> | |
| 16 | <const/meta=( | |
| 17 | dir | |
| 18 | ? formatSize(file.size) | |
| 19 | : (file.duration ?? 0) > 0 | |
| 20 | ? formatDuration(file.duration!) | |
| 21 | : null | |
| 22 | )/> | |
| 23 | <li class={ dir }> | |
| 24 | <a href=`/file${escapeUri(file.path)}` > | |
| 25 | <code>${formatDate(file.date)}</>${" "} | |
| 26 | ${file.basenameWithoutExt}<span class="ext">${file.extension}</>${dir ? '/' : ''} | |
| 27 | <if=meta><span class='meta'>(${meta})</></> | |
| 28 | </a> | |
| 29 | </li> | |
| 30 | </define> | |
| 31 | ||
| 32 | <h1> | |
| 33 | <if=isRoot>clo's files</> | |
| 34 | <else>${fullPath}</> | |
| 35 | </h1> | |
| 36 | ||
| 37 | <if=isRoot> | |
| 38 | <const/{ readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie) /> | |
| 39 | <if=readme><ul><ListItem=readme/></ul></> | |
| 40 | <for|{key, files, titleColor }| of=sections> | |
| 41 | <h2 style={color: titleColor }>${key}</> | |
| 42 | <ul> | |
| 43 | <for|item| of=files><ListItem=item /></> | |
| 44 | </ul> | |
| 45 | </> | |
| 46 | <if=!hasCotyledonCookie> | |
| 47 | <br><br><br><br><br><br> | |
| 48 | <p style={ opacity: 0.3 }> | |
| 49 | would you like to | |
| 50 | <a | |
| 51 | href="/file/cotyledon" | |
| 52 | style={ | |
| 53 | color: 'white', | |
| 54 | 'text-decoration': 'underline', | |
| 55 | } | |
| 56 | >dive deeper?</a> | |
| 57 | </p> | |
| 58 | </> | |
| 59 | </><else> | |
| 60 | <ul> | |
| 61 | <li><a href=`/file${escapeUri(path.posix.dirname(fullPath))}`>[up one...]</a></li> | |
| 62 | <for|item| of=dir.getChildren()> <ListItem=item /> </> | |
| 63 | </ul> | |
| 64 | </> | |
| 65 | ||
| 66 | </div> | |
| 67 | ||
| 68 | import * as path from "node:path"; | |
| 69 | import { escapeUri, formatDuration, formatSize, formatDate } from "@/file-viewer/format.ts"; | |
| 70 | import { MediaFileKind } from "@/file-viewer/models/MediaFile.ts"; | |
| 71 | import * as sort from "@/file-viewer/sort.ts"; |
src/file-viewer/views/lofideon.marko created+22| ... | ... | @@ -0,0 +1,22 @@ |
| 1 | export interface Input { | |
| 2 | stage: number | |
| 3 | } | |
| 4 | export const meta = { title: 'C O T Y L E D O N' }; | |
| 5 | export const theme = { | |
| 6 | bg: '#ff00ff', | |
| 7 | fg: '#000000', | |
| 8 | }; | |
| 9 | ||
| 10 | <h1>co<br>ty<br>le<br>don</h1> | |
| 11 | ||
| 12 | <if=input.stage==0> | |
| 13 | ||
| 14 | <p> | |
| 15 | this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts. | |
| 16 | </p><p> | |
| 17 | by entering our museum, you agree not to use your camera. flash off isn't enough; the bits and bytes are alergic even to a camera's sensor | |
| 18 | </p><p> | |
| 19 | <sub>(in english: please do not store downloads after you're done viewing them)</sub> | |
| 20 | </p> | |
| 21 | ||
| 22 | </> |
tsconfig.json+1-1| ... | ... | @@ -3,7 +3,7 @@ |
| 3 | 3 | "allowImportingTsExtensions": true, |
| 4 | 4 | "baseUrl": ".", |
| 5 | 5 | "incremental": true, |
| 6 | "jsx": "react-jsx", | |
| 6 | "jsx": "react-jsxdev", | |
| 7 | 7 | "jsxImportSource": "#ssr", |
| 8 | 8 | "lib": ["dom", "esnext", "esnext.iterator"], |
| 9 | 9 | "module": "nodenext", |