| 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,7 +3,9 @@ import "#debug"; |
| 3 | 3 | ||
| 4 | const protocol = "http"; | 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 | if (address === "::") address = "::1"; | 9 | if (address === "::") address = "::1"; |
| 8 | console.info(url.format({ | 10 | console.info(url.format({ |
| 9 | protocol, | 11 | protocol, |
framework/engine/jsx-runtime.ts+13-5| ... | @@ -17,13 +17,21 @@ export function jsxDEV( | ... | @@ -17,13 +17,21 @@ export function jsxDEV( |
| 17 | _key: string, | 17 | _key: string, |
| 18 | // Unused with the clover engine | 18 | // Unused with the clover engine |
| 19 | _isStaticChildren: boolean, | 19 | _isStaticChildren: boolean, |
| 20 | // Unused with the clover engine | 20 | source: engine.SrcLoc, |
| 21 | _source: unknown, | ||
| 22 | ): engine.Element { | 21 | ): engine.Element { |
| 22 | const { fileName, lineNumber, columnNumber } = source; | ||
| 23 | |||
| 24 | // Assert the component type is valid to render. | ||
| 23 | if (typeof type !== "function" && typeof type !== "string") { | 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 | // jsxs | 37 | // jsxs |
| ... | @@ -37,7 +45,7 @@ declare global { | ... | @@ -37,7 +45,7 @@ declare global { |
| 37 | interface ElementChildrenAttribute { | 45 | interface ElementChildrenAttribute { |
| 38 | children: Node; | 46 | children: Node; |
| 39 | } | 47 | } |
| 40 | type Element = engine.Node; | 48 | type Element = engine.Element; |
| 41 | type ElementType = keyof IntrinsicElements | engine.Component; | 49 | type ElementType = keyof IntrinsicElements | engine.Component; |
| 42 | type ElementClass = ReturnType<engine.Component>; | 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,14 +123,15 @@ export function escapeXML(input: unknown) { |
| 123 | // The rationale of this check is that the default toString method | 123 | // The rationale of this check is that the default toString method |
| 124 | // creating `[object Object]` is universally useless to any end user. | 124 | // creating `[object Object]` is universally useless to any end user. |
| 125 | if ( | 125 | if ( |
| 126 | input == null || | ||
| 126 | (typeof input === "object" && input && | 127 | (typeof input === "object" && input && |
| 127 | // only block this if it's the default `toString` | 128 | // only block this if it's the default `toString` |
| 128 | input.toString === Object.prototype.toString) | 129 | input.toString === Object.prototype.toString) |
| 129 | ) { | 130 | ) { |
| 130 | throw new Error( | 131 | throw new Error( |
| 131 | `Unexpected object in template placeholder: '` + | 132 | `Unexpected value in template placeholder: '` + |
| 132 | engine.inspect({ name: "clover" }) + "'. " + | 133 | engine.inspect(input) + "'. " + |
| 133 | `To emit a literal '[object Object]', use \${String(value)}`, | 134 | `To emit a literal '${input}', use \${String(value)}`, |
| 134 | ); | 135 | ); |
| 135 | } | 136 | } |
| 136 | return marko.escapeXML(input); | 137 | return marko.escapeXML(input); |
framework/engine/ssr.ts+17-3| ... | @@ -79,6 +79,8 @@ export type Element = [ | ... | @@ -79,6 +79,8 @@ export type Element = [ |
| 79 | tag: typeof kElement, | 79 | tag: typeof kElement, |
| 80 | type: string | Component, | 80 | type: string | Component, |
| 81 | props: Record<string, unknown>, | 81 | props: Record<string, unknown>, |
| 82 | _?: "", | ||
| 83 | source?: SrcLoc, | ||
| 82 | ]; | 84 | ]; |
| 83 | export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; | 85 | export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; |
| 84 | /** | 86 | /** |
| ... | @@ -88,6 +90,12 @@ export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; | ... | @@ -88,6 +90,12 @@ export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode]; |
| 88 | export type Component = ( | 90 | export type Component = ( |
| 89 | props: Record<any, any>, | 91 | props: Record<any, any>, |
| 90 | ) => Exclude<Node, undefined>; | 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 | * Resolution narrows the type 'Node' into 'ResolvedNode'. Async trees are | 101 | * Resolution narrows the type 'Node' into 'ResolvedNode'. Async trees are |
| ... | @@ -130,9 +138,15 @@ export function resolveNode(r: Render, node: unknown): ResolvedNode { | ... | @@ -130,9 +138,15 @@ export function resolveNode(r: Render, node: unknown): ResolvedNode { |
| 130 | const { 1: tag, 2: props } = node; | 138 | const { 1: tag, 2: props } = node; |
| 131 | if (typeof tag === "function") { | 139 | if (typeof tag === "function") { |
| 132 | currentRender = r; | 140 | currentRender = r; |
| 133 | const result = tag(props); | 141 | try { |
| 134 | currentRender = null; | 142 | return resolveNode(r, tag(props)); |
| 135 | return resolveNode(r, result); | 143 | } catch (e) { |
| 144 | const { 4: src } = node; | ||
| 145 | if (e && typeof e === "object") { | ||
| 146 | } | ||
| 147 | } finally { | ||
| 148 | currentRender = null; | ||
| 149 | } | ||
| 136 | } | 150 | } |
| 137 | if (typeof tag !== "string") throw new Error("Unexpected " + typeof type); | 151 | if (typeof tag !== "string") throw new Error("Unexpected " + typeof type); |
| 138 | const children = props?.children; | 152 | const children = props?.children; |
framework/hot.ts+1| ... | @@ -145,6 +145,7 @@ function loadEsbuildCode( | ... | @@ -145,6 +145,7 @@ function loadEsbuildCode( |
| 145 | target: "esnext", | 145 | target: "esnext", |
| 146 | jsx: "automatic", | 146 | jsx: "automatic", |
| 147 | jsxImportSource: "#ssr", | 147 | jsxImportSource: "#ssr", |
| 148 | jsxDev: true, | ||
| 148 | sourcefile: filepath, | 149 | sourcefile: filepath, |
| 149 | }).code; | 150 | }).code; |
| 150 | return module._compile(src, filepath, "commonjs"); | 151 | return module._compile(src, filepath, "commonjs"); |
src/backend.ts-1| ... | @@ -11,7 +11,6 @@ app.use(admin.middleware); | ... | @@ -11,7 +11,6 @@ app.use(admin.middleware); |
| 11 | 11 | ||
| 12 | app.route("", require("./q+a/backend.ts").app); | 12 | app.route("", require("./q+a/backend.ts").app); |
| 13 | app.route("", require("./file-viewer/backend.tsx").app); | 13 | app.route("", require("./file-viewer/backend.tsx").app); |
| 14 | // app.route("", require("./friends/backend.tsx").app); | ||
| 15 | 14 | ||
| 16 | app.use(assets.middleware); | 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,9 +42,15 @@ app.post("/file/cotyledon", async (c) => { |
| 42 | }); | 42 | }); |
| 43 | 43 | ||
| 44 | app.get("/file/*", async (c, next) => { | 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 | return next(); | 50 | return next(); |
| 47 | } | 51 | } |
| 52 | console.log(ua, lofi); | ||
| 53 | |||
| 48 | let rawFilePath = c.req.path.slice(5) || "/"; | 54 | let rawFilePath = c.req.path.slice(5) || "/"; |
| 49 | if (rawFilePath.endsWith("$partial")) { | 55 | if (rawFilePath.endsWith("$partial")) { |
| 50 | return getPartialPage(c, rawFilePath.slice(0, -"$partial".length)); | 56 | return getPartialPage(c, rawFilePath.slice(0, -"$partial".length)); |
| ... | @@ -88,7 +94,7 @@ app.get("/file/*", async (c, next) => { | ... | @@ -88,7 +94,7 @@ app.get("/file/*", async (c, next) => { |
| 88 | } satisfies APIDirectoryList; | 94 | } satisfies APIDirectoryList; |
| 89 | return c.json(json); | 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 | file, | 98 | file, |
| 93 | hasCotyledonCookie, | 99 | hasCotyledonCookie, |
| 94 | }); | 100 | }); |
| ... | @@ -100,7 +106,11 @@ app.get("/file/*", async (c, next) => { | ... | @@ -100,7 +106,11 @@ app.get("/file/*", async (c, next) => { |
| 100 | if (c.req.query("dl") !== undefined) { | 106 | if (c.req.query("dl") !== undefined) { |
| 101 | viewMode = "download"; | 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 | prefetchFile(file.path); | 114 | prefetchFile(file.path); |
| 105 | c.res = await renderView(c, "file-viewer/clofi", { | 115 | c.res = await renderView(c, "file-viewer/clofi", { |
| 106 | file, | 116 | file, |
src/file-viewer/format.ts+31-9| ... | @@ -9,15 +9,15 @@ export function formatSize(bytes: number) { | ... | @@ -9,15 +9,15 @@ export function formatSize(bytes: number) { |
| 9 | return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; | 9 | return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | export function formatDate(date: Date) { | 12 | // export function formatDateDefined(date: Date) { |
| 13 | // YYYY-MM-DD, format in PST timezone | 13 | // // YYYY-MM-DD, format in PST timezone |
| 14 | return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" }); | 14 | // return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" }); |
| 15 | } | 15 | // } |
| 16 | 16 | // | |
| 17 | export function formatShortDate(date: Date) { | 17 | // export function formatShortDate(date: Date) { |
| 18 | // YY-MM-DD, format in PST timezone | 18 | // // YY-MM-DD, format in PST timezone |
| 19 | return formatDate(date).slice(2); | 19 | // return formatDate(date).slice(2); |
| 20 | } | 20 | // } |
| 21 | 21 | ||
| 22 | export function formatDuration(seconds: number) { | 22 | export function formatDuration(seconds: number) { |
| 23 | const minutes = Math.floor(seconds / 60); | 23 | const minutes = Math.floor(seconds / 60); |
| ... | @@ -255,5 +255,27 @@ export function highlightHashComments(text: string) { | ... | @@ -255,5 +255,27 @@ export function highlightHashComments(text: string) { |
| 255 | .join("\n"); | 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 | import type { MediaFile } from "@/file-viewer/models/MediaFile.ts"; | 280 | import type { MediaFile } from "@/file-viewer/models/MediaFile.ts"; |
| 259 | import { escapeHtml } from "#ssr"; | 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,7 +1,7 @@ |
| 1 | import { MediaFile } from "../models/MediaFile.ts"; | 1 | import { MediaFile } from "../models/MediaFile.ts"; |
| 2 | import { Speedbump } from "../cotyledon.tsx"; | ||
| 3 | import { MediaPanel } from "../views/clofi.tsx"; | 2 | import { MediaPanel } from "../views/clofi.tsx"; |
| 4 | import { addScript } from "#sitegen"; | 3 | import { addScript } from "#sitegen"; |
| 4 | import { Speedbump } from "../cotyledon.tsx"; | ||
| 5 | 5 | ||
| 6 | export const theme = { | 6 | export const theme = { |
| 7 | bg: "#312652", | 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,6 +687,15 @@ h3 { |
| 687 | transform: translateY(0); | 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 | /* MOBILE */ | 699 | /* MOBILE */ |
| 691 | @media (max-width: 1000px) { | 700 | @media (max-width: 1000px) { |
| 692 | html, body { | 701 | html, body { |
src/file-viewer/views/clofi.tsx+5-49| ... | @@ -406,7 +406,7 @@ function ListItem({ | ... | @@ -406,7 +406,7 @@ function ListItem({ |
| 406 | noDate?: boolean; | 406 | noDate?: boolean; |
| 407 | }) { | 407 | }) { |
| 408 | const dateTime = file.date; | 408 | const dateTime = file.date; |
| 409 | let shortDate = dateTime < unknownDateWithKnownYear | 409 | const shortDate = dateTime < unknownDateWithKnownYear |
| 410 | ? ( | 410 | ? ( |
| 411 | dateTime < unknownDate | 411 | dateTime < unknownDate |
| 412 | ? ( | 412 | ? ( |
| ... | @@ -565,40 +565,7 @@ function RootDirView({ | ... | @@ -565,40 +565,7 @@ function RootDirView({ |
| 565 | hasCotyledonCookie: boolean; | 565 | hasCotyledonCookie: boolean; |
| 566 | }) { | 566 | }) { |
| 567 | const children = dir.getPublicChildren(); | 567 | const children = dir.getPublicChildren(); |
| 568 | let readme: MediaFile | null = null; | 568 | const { readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie); |
| 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 | } | ||
| 602 | 569 | ||
| 603 | if (readme && isLast) activeFilename ||= readmeFile; | 570 | if (readme && isLast) activeFilename ||= readmeFile; |
| 604 | 571 | ||
| ... | @@ -613,22 +580,10 @@ function RootDirView({ | ... | @@ -613,22 +580,10 @@ function RootDirView({ |
| 613 | /> | 580 | /> |
| 614 | </ul> | 581 | </ul> |
| 615 | )} | 582 | )} |
| 616 | {Object.entries(groups).map(([key, files]) => { | 583 | {sections.map(({ key, titleColor, 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 | } | ||
| 629 | return ( | 584 | return ( |
| 630 | <div class="group" data-group={key}> | 585 | <div class="group" data-group={key}> |
| 631 | <h3 style={`color:${colorMap[key as keyof typeof colorMap]};`}> | 586 | <h3 style={{ color: titleColor }}> |
| 632 | {key} | 587 | {key} |
| 633 | </h3> | 588 | </h3> |
| 634 | <ul> | 589 | <ul> |
| ... | @@ -1001,3 +956,4 @@ import { | ... | @@ -1001,3 +956,4 @@ import { |
| 1001 | } from "@/file-viewer/format.ts"; | 956 | } from "@/file-viewer/format.ts"; |
| 1002 | import { ForEveryone } from "@/file-viewer/cotyledon.tsx"; | 957 | import { ForEveryone } from "@/file-viewer/cotyledon.tsx"; |
| 1003 | import * as mime from "#sitegen/mime"; | 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,7 +3,7 @@ |
| 3 | "allowImportingTsExtensions": true, | 3 | "allowImportingTsExtensions": true, |
| 4 | "baseUrl": ".", | 4 | "baseUrl": ".", |
| 5 | "incremental": true, | 5 | "incremental": true, |
| 6 | "jsx": "react-jsx", | 6 | "jsx": "react-jsxdev", |
| 7 | "jsxImportSource": "#ssr", | 7 | "jsxImportSource": "#ssr", |
| 8 | "lib": ["dom", "esnext", "esnext.iterator"], | 8 | "lib": ["dom", "esnext", "esnext.iterator"], |
| 9 | "module": "nodenext", | 9 | "module": "nodenext", |