file viewer work
This commit is contained in:
parent
a367dfdb29
commit
71a072b0be
19 changed files with 343 additions and 88 deletions
|
@ -3,7 +3,9 @@ import "#debug";
|
||||||
|
|
||||||
const protocol = "http";
|
const protocol = "http";
|
||||||
|
|
||||||
const server = serve(app, ({ address, port }) => {
|
const server = serve({
|
||||||
|
fetch: app.fetch,
|
||||||
|
}, ({ address, port }) => {
|
||||||
if (address === "::") address = "::1";
|
if (address === "::") address = "::1";
|
||||||
console.info(url.format({
|
console.info(url.format({
|
||||||
protocol,
|
protocol,
|
||||||
|
|
|
@ -17,13 +17,21 @@ export function jsxDEV(
|
||||||
_key: string,
|
_key: string,
|
||||||
// Unused with the clover engine
|
// Unused with the clover engine
|
||||||
_isStaticChildren: boolean,
|
_isStaticChildren: boolean,
|
||||||
// Unused with the clover engine
|
source: engine.SrcLoc,
|
||||||
_source: unknown,
|
|
||||||
): engine.Element {
|
): engine.Element {
|
||||||
|
const { fileName, lineNumber, columnNumber } = source;
|
||||||
|
|
||||||
|
// Assert the component type is valid to render.
|
||||||
if (typeof type !== "function" && typeof type !== "string") {
|
if (typeof type !== "function" && typeof type !== "string") {
|
||||||
throw new Error("Invalid component type: " + engine.inspect(type));
|
throw new Error(
|
||||||
|
`Invalid component type at ${fileName}:${lineNumber}:${columnNumber}: ` +
|
||||||
|
engine.inspect(type) +
|
||||||
|
". Clover SSR element must be a function or string",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [engine.kElement, type, props];
|
|
||||||
|
// Construct an `ssr.Element`
|
||||||
|
return [engine.kElement, type, props, "", source];
|
||||||
}
|
}
|
||||||
|
|
||||||
// jsxs
|
// jsxs
|
||||||
|
@ -37,7 +45,7 @@ declare global {
|
||||||
interface ElementChildrenAttribute {
|
interface ElementChildrenAttribute {
|
||||||
children: Node;
|
children: Node;
|
||||||
}
|
}
|
||||||
type Element = engine.Node;
|
type Element = engine.Element;
|
||||||
type ElementType = keyof IntrinsicElements | engine.Component;
|
type ElementType = keyof IntrinsicElements | engine.Component;
|
||||||
type ElementClass = ReturnType<engine.Component>;
|
type ElementClass = ReturnType<engine.Component>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,14 +123,15 @@ export function escapeXML(input: unknown) {
|
||||||
// The rationale of this check is that the default toString method
|
// The rationale of this check is that the default toString method
|
||||||
// creating `[object Object]` is universally useless to any end user.
|
// creating `[object Object]` is universally useless to any end user.
|
||||||
if (
|
if (
|
||||||
|
input == null ||
|
||||||
(typeof input === "object" && input &&
|
(typeof input === "object" && input &&
|
||||||
// only block this if it's the default `toString`
|
// only block this if it's the default `toString`
|
||||||
input.toString === Object.prototype.toString)
|
input.toString === Object.prototype.toString)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unexpected object in template placeholder: '` +
|
`Unexpected value in template placeholder: '` +
|
||||||
engine.inspect({ name: "clover" }) + "'. " +
|
engine.inspect(input) + "'. " +
|
||||||
`To emit a literal '[object Object]', use \${String(value)}`,
|
`To emit a literal '${input}', use \${String(value)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return marko.escapeXML(input);
|
return marko.escapeXML(input);
|
||||||
|
|
|
@ -79,6 +79,8 @@ export type Element = [
|
||||||
tag: typeof kElement,
|
tag: typeof kElement,
|
||||||
type: string | Component,
|
type: string | Component,
|
||||||
props: Record<string, unknown>,
|
props: Record<string, unknown>,
|
||||||
|
_?: "",
|
||||||
|
source?: SrcLoc,
|
||||||
];
|
];
|
||||||
export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode];
|
export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode];
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +90,12 @@ export type DirectHtml = [tag: typeof kDirectHtml, html: ResolvedNode];
|
||||||
export type Component = (
|
export type Component = (
|
||||||
props: Record<any, any>,
|
props: Record<any, any>,
|
||||||
) => Exclude<Node, undefined>;
|
) => Exclude<Node, undefined>;
|
||||||
|
/** Emitted by JSX runtime */
|
||||||
|
export interface SrcLoc {
|
||||||
|
fileName: string;
|
||||||
|
lineNumber: number;
|
||||||
|
columnNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolution narrows the type 'Node' into 'ResolvedNode'. Async trees are
|
* Resolution narrows the type 'Node' into 'ResolvedNode'. Async trees are
|
||||||
|
@ -130,9 +138,15 @@ export function resolveNode(r: Render, node: unknown): ResolvedNode {
|
||||||
const { 1: tag, 2: props } = node;
|
const { 1: tag, 2: props } = node;
|
||||||
if (typeof tag === "function") {
|
if (typeof tag === "function") {
|
||||||
currentRender = r;
|
currentRender = r;
|
||||||
const result = tag(props);
|
try {
|
||||||
currentRender = null;
|
return resolveNode(r, tag(props));
|
||||||
return resolveNode(r, result);
|
} catch (e) {
|
||||||
|
const { 4: src } = node;
|
||||||
|
if (e && typeof e === "object") {
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
currentRender = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (typeof tag !== "string") throw new Error("Unexpected " + typeof type);
|
if (typeof tag !== "string") throw new Error("Unexpected " + typeof type);
|
||||||
const children = props?.children;
|
const children = props?.children;
|
||||||
|
|
|
@ -145,6 +145,7 @@ function loadEsbuildCode(
|
||||||
target: "esnext",
|
target: "esnext",
|
||||||
jsx: "automatic",
|
jsx: "automatic",
|
||||||
jsxImportSource: "#ssr",
|
jsxImportSource: "#ssr",
|
||||||
|
jsxDev: true,
|
||||||
sourcefile: filepath,
|
sourcefile: filepath,
|
||||||
}).code;
|
}).code;
|
||||||
return module._compile(src, filepath, "commonjs");
|
return module._compile(src, filepath, "commonjs");
|
||||||
|
|
|
@ -11,7 +11,6 @@ app.use(admin.middleware);
|
||||||
|
|
||||||
app.route("", require("./q+a/backend.ts").app);
|
app.route("", require("./q+a/backend.ts").app);
|
||||||
app.route("", require("./file-viewer/backend.tsx").app);
|
app.route("", require("./file-viewer/backend.tsx").app);
|
||||||
// app.route("", require("./friends/backend.tsx").app);
|
|
||||||
|
|
||||||
app.use(assets.middleware);
|
app.use(assets.middleware);
|
||||||
|
|
||||||
|
|
|
@ -42,9 +42,15 @@ app.post("/file/cotyledon", async (c) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/file/*", async (c, next) => {
|
app.get("/file/*", async (c, next) => {
|
||||||
if (c.req.header("User-Agent")?.toLowerCase()?.includes("discordbot")) {
|
const ua = c.req.header("User-Agent")?.toLowerCase() ?? "";
|
||||||
|
const lofi = ua.includes("msie") || ua.includes("rv:") || false;
|
||||||
|
|
||||||
|
// Discord ignores 'robots.txt' which violates the license agreement.
|
||||||
|
if (ua.includes("discordbot")) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
console.log(ua, lofi);
|
||||||
|
|
||||||
let rawFilePath = c.req.path.slice(5) || "/";
|
let rawFilePath = c.req.path.slice(5) || "/";
|
||||||
if (rawFilePath.endsWith("$partial")) {
|
if (rawFilePath.endsWith("$partial")) {
|
||||||
return getPartialPage(c, rawFilePath.slice(0, -"$partial".length));
|
return getPartialPage(c, rawFilePath.slice(0, -"$partial".length));
|
||||||
|
@ -88,7 +94,7 @@ app.get("/file/*", async (c, next) => {
|
||||||
} satisfies APIDirectoryList;
|
} satisfies APIDirectoryList;
|
||||||
return c.json(json);
|
return c.json(json);
|
||||||
}
|
}
|
||||||
c.res = await renderView(c, "file-viewer/clofi", {
|
c.res = await renderView(c, `file-viewer/${lofi ? "lofi" : "clofi"}`, {
|
||||||
file,
|
file,
|
||||||
hasCotyledonCookie,
|
hasCotyledonCookie,
|
||||||
});
|
});
|
||||||
|
@ -100,7 +106,11 @@ app.get("/file/*", async (c, next) => {
|
||||||
if (c.req.query("dl") !== undefined) {
|
if (c.req.query("dl") !== undefined) {
|
||||||
viewMode = "download";
|
viewMode = "download";
|
||||||
}
|
}
|
||||||
if (viewMode == undefined && c.req.header("Accept")?.includes("text/html")) {
|
if (
|
||||||
|
viewMode == undefined &&
|
||||||
|
c.req.header("Accept")?.includes("text/html") &&
|
||||||
|
!lofi
|
||||||
|
) {
|
||||||
prefetchFile(file.path);
|
prefetchFile(file.path);
|
||||||
c.res = await renderView(c, "file-viewer/clofi", {
|
c.res = await renderView(c, "file-viewer/clofi", {
|
||||||
file,
|
file,
|
||||||
|
|
|
@ -9,15 +9,15 @@ export function formatSize(bytes: number) {
|
||||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(date: Date) {
|
// export function formatDateDefined(date: Date) {
|
||||||
// YYYY-MM-DD, format in PST timezone
|
// // YYYY-MM-DD, format in PST timezone
|
||||||
return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" });
|
// return date.toLocaleDateString("sv", { timeZone: "America/Los_Angeles" });
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
export function formatShortDate(date: Date) {
|
// export function formatShortDate(date: Date) {
|
||||||
// YY-MM-DD, format in PST timezone
|
// // YY-MM-DD, format in PST timezone
|
||||||
return formatDate(date).slice(2);
|
// return formatDate(date).slice(2);
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function formatDuration(seconds: number) {
|
export function formatDuration(seconds: number) {
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
@ -255,5 +255,27 @@ export function highlightHashComments(text: string) {
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unknownDate = new Date("1970-01-03");
|
||||||
|
const unknownDateWithKnownYear = new Date("1970-02-20");
|
||||||
|
|
||||||
|
export function formatDate(dateTime: Date) {
|
||||||
|
return dateTime < unknownDateWithKnownYear
|
||||||
|
? (
|
||||||
|
dateTime < unknownDate
|
||||||
|
? (
|
||||||
|
"??.??.??"
|
||||||
|
)
|
||||||
|
: `xx.xx.${21 + Math.floor(dateTime.getTime() / 86400000)}`
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
`${(dateTime.getMonth() + 1).toString().padStart(2, "0")}.${
|
||||||
|
dateTime
|
||||||
|
.getDate()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")
|
||||||
|
}.${dateTime.getFullYear().toString().slice(2)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
import type { MediaFile } from "@/file-viewer/models/MediaFile.ts";
|
import type { MediaFile } from "@/file-viewer/models/MediaFile.ts";
|
||||||
import { escapeHtml } from "#ssr";
|
import { escapeHtml } from "#ssr";
|
||||||
|
|
34
src/file-viewer/pages/file.404.tsx
Normal file
34
src/file-viewer/pages/file.404.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { MediaFile } from "../models/MediaFile.ts";
|
||||||
|
import { MediaPanel } from "../views/clofi.tsx";
|
||||||
|
import { addScript } from "#sitegen";
|
||||||
|
|
||||||
|
export const theme = {
|
||||||
|
bg: "#312652",
|
||||||
|
fg: "#f0f0ff",
|
||||||
|
primary: "#fabe32",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const meta = { title: "file not found" };
|
||||||
|
|
||||||
|
export default function CotyledonPage() {
|
||||||
|
addScript("../scripts/canvas_cotyledon.client.ts");
|
||||||
|
return (
|
||||||
|
<div class="files ctld ctld-sb">
|
||||||
|
<MediaPanel
|
||||||
|
file={MediaFile.getByPath("/")!}
|
||||||
|
isLast={false}
|
||||||
|
activeFilename={null}
|
||||||
|
hasCotyledonCookie={false}
|
||||||
|
/>
|
||||||
|
<div class="panel last">
|
||||||
|
<div className="header"></div>
|
||||||
|
<div className="content file-view notfound">
|
||||||
|
<p>this file does not exist ...</p>
|
||||||
|
<p>
|
||||||
|
<a href="/file">return</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { MediaFile } from "../models/MediaFile.ts";
|
import { MediaFile } from "../models/MediaFile.ts";
|
||||||
import { Speedbump } from "../cotyledon.tsx";
|
|
||||||
import { MediaPanel } from "../views/clofi.tsx";
|
import { MediaPanel } from "../views/clofi.tsx";
|
||||||
import { addScript } from "#sitegen";
|
import { addScript } from "#sitegen";
|
||||||
|
import { Speedbump } from "../cotyledon.tsx";
|
||||||
|
|
||||||
export const theme = {
|
export const theme = {
|
||||||
bg: "#312652",
|
bg: "#312652",
|
||||||
|
|
58
src/file-viewer/sort.ts
Normal file
58
src/file-viewer/sort.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
export function splitRootDirFiles(dir: MediaFile, hasCotyledonCookie: boolean) {
|
||||||
|
const children = dir.getPublicChildren();
|
||||||
|
let readme: MediaFile | null = null;
|
||||||
|
|
||||||
|
const groups = {
|
||||||
|
// years 2025 and onwards
|
||||||
|
years: [] as MediaFile[],
|
||||||
|
// named categories
|
||||||
|
categories: [] as MediaFile[],
|
||||||
|
// years 2017 to 2024
|
||||||
|
cotyledon: [] as MediaFile[],
|
||||||
|
};
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
years: "#a2ff91",
|
||||||
|
categories: "#9c91ff",
|
||||||
|
cotyledon: "#ff91ca",
|
||||||
|
};
|
||||||
|
for (const child of children) {
|
||||||
|
const basename = child.basename;
|
||||||
|
if (basename === "readme.txt") {
|
||||||
|
readme = child;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = basename.match(/^(\d{4})/);
|
||||||
|
if (year) {
|
||||||
|
const n = parseInt(year[1]);
|
||||||
|
if (n >= 2025) {
|
||||||
|
groups.years.push(child);
|
||||||
|
} else {
|
||||||
|
groups.cotyledon.push(child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
groups.categories.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sections = [];
|
||||||
|
for (const [key, files] of Object.entries(groups)) {
|
||||||
|
if (key === "cotyledon" && !hasCotyledonCookie) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "years" || key === "cotyledon") {
|
||||||
|
files.sort((a, b) => {
|
||||||
|
return b.basename.localeCompare(a.basename);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
files.sort((a, b) => {
|
||||||
|
return a.basename.localeCompare(b.basename);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sections.push({ key, titleColor: colorMap[key], files });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { readme, sections };
|
||||||
|
}
|
||||||
|
|
||||||
|
import { MediaFile } from "./models/MediaFile.ts";
|
|
@ -1,12 +0,0 @@
|
||||||
---
|
|
||||||
import { useInlineScript } from "../framework/page-resources.ts";
|
|
||||||
|
|
||||||
const { script } = Astro.props;
|
|
||||||
useInlineScript('canvas_' + script as any);
|
|
||||||
useInlineScript('canvas_demo');
|
|
||||||
---
|
|
||||||
<canvas id="canvas"
|
|
||||||
data-canvas={script}
|
|
||||||
data-standalone="true"
|
|
||||||
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;"
|
|
||||||
></canvas>
|
|
17
src/file-viewer/views/canvas.marko
Normal file
17
src/file-viewer/views/canvas.marko
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export interface Input {
|
||||||
|
script: string;
|
||||||
|
}
|
||||||
|
export const meta = { title: "canvas" };
|
||||||
|
|
||||||
|
<const/{ script } = input />
|
||||||
|
|
||||||
|
<canvas id="canvas"
|
||||||
|
data-canvas=script
|
||||||
|
data-standalone="true"
|
||||||
|
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;"
|
||||||
|
></canvas>
|
||||||
|
<AddScript="file-viewer/scripts/canvas_" + script + ".client.ts" />
|
||||||
|
|
||||||
|
client import "./canvas.client.ts";
|
||||||
|
|
||||||
|
import { addScript as AddScript } from '#sitegen';
|
|
@ -687,6 +687,15 @@ h3 {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.notfound {
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* MOBILE */
|
/* MOBILE */
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
html, body {
|
html, body {
|
||||||
|
|
|
@ -406,7 +406,7 @@ function ListItem({
|
||||||
noDate?: boolean;
|
noDate?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const dateTime = file.date;
|
const dateTime = file.date;
|
||||||
let shortDate = dateTime < unknownDateWithKnownYear
|
const shortDate = dateTime < unknownDateWithKnownYear
|
||||||
? (
|
? (
|
||||||
dateTime < unknownDate
|
dateTime < unknownDate
|
||||||
? (
|
? (
|
||||||
|
@ -565,40 +565,7 @@ function RootDirView({
|
||||||
hasCotyledonCookie: boolean;
|
hasCotyledonCookie: boolean;
|
||||||
}) {
|
}) {
|
||||||
const children = dir.getPublicChildren();
|
const children = dir.getPublicChildren();
|
||||||
let readme: MediaFile | null = null;
|
const { readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie);
|
||||||
|
|
||||||
const groups = {
|
|
||||||
// years 2025 and onwards
|
|
||||||
years: [] as MediaFile[],
|
|
||||||
// named categories
|
|
||||||
categories: [] as MediaFile[],
|
|
||||||
// years 2017 to 2024
|
|
||||||
cotyledon: [] as MediaFile[],
|
|
||||||
};
|
|
||||||
const colorMap = {
|
|
||||||
years: "#a2ff91",
|
|
||||||
categories: "#9c91ff",
|
|
||||||
cotyledon: "#ff91ca",
|
|
||||||
};
|
|
||||||
for (const child of children) {
|
|
||||||
const basename = child.basename;
|
|
||||||
if (basename === readmeFile) {
|
|
||||||
readme = child;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = basename.match(/^(\d{4})/);
|
|
||||||
if (year) {
|
|
||||||
const n = parseInt(year[1]);
|
|
||||||
if (n >= 2025) {
|
|
||||||
groups.years.push(child);
|
|
||||||
} else {
|
|
||||||
groups.cotyledon.push(child);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
groups.categories.push(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readme && isLast) activeFilename ||= readmeFile;
|
if (readme && isLast) activeFilename ||= readmeFile;
|
||||||
|
|
||||||
|
@ -613,22 +580,10 @@ function RootDirView({
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{Object.entries(groups).map(([key, files]) => {
|
{sections.map(({ key, titleColor, files }) => {
|
||||||
if (key === "cotyledon" && !hasCotyledonCookie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (key === "years" || key === "cotyledon") {
|
|
||||||
files.sort((a, b) => {
|
|
||||||
return b.basename.localeCompare(a.basename);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
files.sort((a, b) => {
|
|
||||||
return a.basename.localeCompare(b.basename);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div class="group" data-group={key}>
|
<div class="group" data-group={key}>
|
||||||
<h3 style={`color:${colorMap[key as keyof typeof colorMap]};`}>
|
<h3 style={{ color: titleColor }}>
|
||||||
{key}
|
{key}
|
||||||
</h3>
|
</h3>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -1001,3 +956,4 @@ import {
|
||||||
} from "@/file-viewer/format.ts";
|
} from "@/file-viewer/format.ts";
|
||||||
import { ForEveryone } from "@/file-viewer/cotyledon.tsx";
|
import { ForEveryone } from "@/file-viewer/cotyledon.tsx";
|
||||||
import * as mime from "#sitegen/mime";
|
import * as mime from "#sitegen/mime";
|
||||||
|
import * as sort from "../sort.ts";
|
||||||
|
|
43
src/file-viewer/views/lofi.css
Normal file
43
src/file-viewer/views/lofi.css
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#lofi {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 3em;
|
||||||
|
color: var(--primary);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
ul, li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-right: 4em;
|
||||||
|
}
|
||||||
|
li a {
|
||||||
|
display: block;
|
||||||
|
color: white;
|
||||||
|
line-height: 2em;
|
||||||
|
padding: 0 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
li a:hover {
|
||||||
|
background-color: rgba(255,255,255,0.2);
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none!important;
|
||||||
|
}
|
||||||
|
.dir a {
|
||||||
|
color: #99eeFF
|
||||||
|
}
|
||||||
|
.ext {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
margin-left: 1em;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
71
src/file-viewer/views/lofi.marko
Normal file
71
src/file-viewer/views/lofi.marko
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import "./lofi.css";
|
||||||
|
export interface Input {
|
||||||
|
file: MediaFile;
|
||||||
|
hasCotyledonCookie: boolean;
|
||||||
|
}
|
||||||
|
export { meta, theme } from "./clofi.tsx";
|
||||||
|
|
||||||
|
<const/{ file: dir, hasCotyledonCookie } = input />
|
||||||
|
<const/{ path: fullPath, dirname, basename, kind } = dir />
|
||||||
|
<const/isRoot = fullPath == '/'>
|
||||||
|
|
||||||
|
<div#lofi>
|
||||||
|
|
||||||
|
<define/ListItem|{ value: file }|>
|
||||||
|
<const/dir=file.kind === MediaFileKind.directory/>
|
||||||
|
<const/meta=(
|
||||||
|
dir
|
||||||
|
? formatSize(file.size)
|
||||||
|
: (file.duration ?? 0) > 0
|
||||||
|
? formatDuration(file.duration!)
|
||||||
|
: null
|
||||||
|
)/>
|
||||||
|
<li class={ dir }>
|
||||||
|
<a href=`/file${escapeUri(file.path)}` >
|
||||||
|
<code>${formatDate(file.date)}</>${" "}
|
||||||
|
${file.basenameWithoutExt}<span class="ext">${file.extension}</>${dir ? '/' : ''}
|
||||||
|
<if=meta><span class='meta'>(${meta})</></>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</define>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
<if=isRoot>clo's files</>
|
||||||
|
<else>${fullPath}</>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<if=isRoot>
|
||||||
|
<const/{ readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie) />
|
||||||
|
<if=readme><ul><ListItem=readme/></ul></>
|
||||||
|
<for|{key, files, titleColor }| of=sections>
|
||||||
|
<h2 style={color: titleColor }>${key}</>
|
||||||
|
<ul>
|
||||||
|
<for|item| of=files><ListItem=item /></>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
<if=!hasCotyledonCookie>
|
||||||
|
<br><br><br><br><br><br>
|
||||||
|
<p style={ opacity: 0.3 }>
|
||||||
|
would you like to
|
||||||
|
<a
|
||||||
|
href="/file/cotyledon"
|
||||||
|
style={
|
||||||
|
color: 'white',
|
||||||
|
'text-decoration': 'underline',
|
||||||
|
}
|
||||||
|
>dive deeper?</a>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
</><else>
|
||||||
|
<ul>
|
||||||
|
<li><a href=`/file${escapeUri(path.posix.dirname(fullPath))}`>[up one...]</a></li>
|
||||||
|
<for|item| of=dir.getChildren()> <ListItem=item /> </>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { escapeUri, formatDuration, formatSize, formatDate } from "@/file-viewer/format.ts";
|
||||||
|
import { MediaFileKind } from "@/file-viewer/models/MediaFile.ts";
|
||||||
|
import * as sort from "@/file-viewer/sort.ts";
|
22
src/file-viewer/views/lofideon.marko
Normal file
22
src/file-viewer/views/lofideon.marko
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export interface Input {
|
||||||
|
stage: number
|
||||||
|
}
|
||||||
|
export const meta = { title: 'C O T Y L E D O N' };
|
||||||
|
export const theme = {
|
||||||
|
bg: '#ff00ff',
|
||||||
|
fg: '#000000',
|
||||||
|
};
|
||||||
|
|
||||||
|
<h1>co<br>ty<br>le<br>don</h1>
|
||||||
|
|
||||||
|
<if=input.stage==0>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts.
|
||||||
|
</p><p>
|
||||||
|
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
|
||||||
|
</p><p>
|
||||||
|
<sub>(in english: please do not store downloads after you're done viewing them)</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</>
|
|
@ -3,7 +3,7 @@
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsxdev",
|
||||||
"jsxImportSource": "#ssr",
|
"jsxImportSource": "#ssr",
|
||||||
"lib": ["dom", "esnext", "esnext.iterator"],
|
"lib": ["dom", "esnext", "esnext.iterator"],
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
|
|
Loading…
Reference in a new issue