| author | |
| committer | |
| log | 22ebf23fcec9685339e4fa903ed984d374f06948 |
| tree | 47315d7b1cf8918f9f15d381c82b0cebd3624077 |
| parent | ec1448f737d8aed1c7f7fdd8a02fd6232b19eff8 |
| signature | Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs |
sad to see prettier but its whatever44 files changed, 847 insertions(+), 689 deletions(-)
dprint.jsonc+7-7| ... | @@ -1,14 +1,13 @@ | ... | @@ -1,14 +1,13 @@ |
| 1 | { | 1 | { |
| 2 | "typescript": { | ||
| 3 | }, | ||
| 4 | "json": { | ||
| 5 | }, | ||
| 6 | "markdown": { | 2 | "markdown": { |
| 7 | "textWrap": "always", | 3 | "textWrap": "always", |
| 8 | }, | 4 | }, |
| 9 | "malva": { | 5 | "exec": { |
| 10 | }, | 6 | "cwd": "${configDir}", |
| 11 | "markup": { | 7 | "commands": [{ |
| 8 | "command": "node_modules/.bin/prettier --plugin prettier-plugin-marko {{file_path}}", | ||
| 9 | "exts": ["marko"], | ||
| 10 | }], | ||
| 12 | }, | 11 | }, |
| 13 | "excludes": [ | 12 | "excludes": [ |
| 14 | "**/node_modules", | 13 | "**/node_modules", |
| ... | @@ -20,5 +19,6 @@ | ... | @@ -20,5 +19,6 @@ |
| 20 | "https://plugins.dprint.dev/markdown-0.21.1.wasm", | 19 | "https://plugins.dprint.dev/markdown-0.21.1.wasm", |
| 21 | "https://plugins.dprint.dev/g-plane/malva-v0.15.2.wasm", | 20 | "https://plugins.dprint.dev/g-plane/malva-v0.15.2.wasm", |
| 22 | "https://plugins.dprint.dev/g-plane/markup_fmt-v0.27.0.wasm", | 21 | "https://plugins.dprint.dev/g-plane/markup_fmt-v0.27.0.wasm", |
| 22 | "https://plugins.dprint.dev/exec-0.6.2.json@df98f54ffd3092b8a841aedd6d098a2651f16d0a796a40535774f1a8b4b9d463", | ||
| 23 | ], | 23 | ], |
| 24 | } | 24 | } |
framework/lib/add-script.marko+1-1| ... | @@ -1 +1 @@ | ... | @@ -1 +1 @@ |
| 1 | <const/_=$global.cloverScripts.add(input.value) /> | 1 | <const/_=$global.cloverScripts.add(input.value)> |
framework/sitegen-wrap.marko+2-2| ... | @@ -1,2 +1,2 @@ | ... | @@ -1,2 +1,2 @@ |
| 1 | <const/_=$global.cloverScripts=input.cloverScripts /> | 1 | <const/_=$global.cloverScripts = input.cloverScripts> |
| 2 | <${input.page} ...input.attrs /> | 2 | <${input.page} ...input.attrs/> |
package-lock.json+26| ... | @@ -19,6 +19,8 @@ | ... | @@ -19,6 +19,8 @@ |
| 19 | "marko": "5.38.33", | 19 | "marko": "5.38.33", |
| 20 | "msgpackr": "1.11.5", | 20 | "msgpackr": "1.11.5", |
| 21 | "playwright": "1.58.2", | 21 | "playwright": "1.58.2", |
| 22 | "prettier": "^3.8.1", | ||
| 23 | "prettier-plugin-marko": "^4.0.9", | ||
| 22 | "source-map-support": "0.5.21", | 24 | "source-map-support": "0.5.21", |
| 23 | "typescript": "6.0.2", | 25 | "typescript": "6.0.2", |
| 24 | "unique-names-generator": "4.7.1", | 26 | "unique-names-generator": "4.7.1", |
| ... | @@ -2178,6 +2180,30 @@ | ... | @@ -2178,6 +2180,30 @@ |
| 2178 | "node": "^10 || ^12 || >=14" | 2180 | "node": "^10 || ^12 || >=14" |
| 2179 | } | 2181 | } |
| 2180 | }, | 2182 | }, |
| 2183 | "node_modules/prettier": { | ||
| 2184 | "version": "3.8.1", | ||
| 2185 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", | ||
| 2186 | "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", | ||
| 2187 | "license": "MIT", | ||
| 2188 | "bin": { | ||
| 2189 | "prettier": "bin/prettier.cjs" | ||
| 2190 | }, | ||
| 2191 | "engines": { | ||
| 2192 | "node": ">=14" | ||
| 2193 | }, | ||
| 2194 | "funding": { | ||
| 2195 | "url": "https://github.com/prettier/prettier?sponsor=1" | ||
| 2196 | } | ||
| 2197 | }, | ||
| 2198 | "node_modules/prettier-plugin-marko": { | ||
| 2199 | "version": "4.0.9", | ||
| 2200 | "resolved": "https://registry.npmjs.org/prettier-plugin-marko/-/prettier-plugin-marko-4.0.9.tgz", | ||
| 2201 | "integrity": "sha512-njqNk0oF6jolUMXNUOSlvRe6Dd50DN4NvQk3EY/6hUrU4tJw/HSwvhDF1uTYVs5KjOY5lH+x5C13C1Av9jajWA==", | ||
| 2202 | "license": "MIT", | ||
| 2203 | "dependencies": { | ||
| 2204 | "htmljs-parser": "^5.10.2" | ||
| 2205 | } | ||
| 2206 | }, | ||
| 2181 | "node_modules/raptor-async": { | 2207 | "node_modules/raptor-async": { |
| 2182 | "version": "1.1.3", | 2208 | "version": "1.1.3", |
| 2183 | "resolved": "https://registry.npmjs.org/raptor-async/-/raptor-async-1.1.3.tgz", | 2209 | "resolved": "https://registry.npmjs.org/raptor-async/-/raptor-async-1.1.3.tgz", |
package.json+2| ... | @@ -15,6 +15,8 @@ | ... | @@ -15,6 +15,8 @@ |
| 15 | "marko": "5.38.33", | 15 | "marko": "5.38.33", |
| 16 | "msgpackr": "1.11.5", | 16 | "msgpackr": "1.11.5", |
| 17 | "playwright": "1.58.2", | 17 | "playwright": "1.58.2", |
| 18 | "prettier": "^3.8.1", | ||
| 19 | "prettier-plugin-marko": "^4.0.9", | ||
| 18 | "source-map-support": "0.5.21", | 20 | "source-map-support": "0.5.21", |
| 19 | "typescript": "6.0.2", | 21 | "typescript": "6.0.2", |
| 20 | "unique-names-generator": "4.7.1", | 22 | "unique-names-generator": "4.7.1", |
src/blog/tags/blog-layout.marko+52-53| ... | @@ -6,7 +6,7 @@ export const theme = { | ... | @@ -6,7 +6,7 @@ export const theme = { |
| 6 | }; | 6 | }; |
| 7 | export const components = { | 7 | export const components = { |
| 8 | heading: Heading, | 8 | heading: Heading, |
| 9 | codeBlock: CodeBlock | 9 | codeBlock: CodeBlock, |
| 10 | }; | 10 | }; |
| 11 | 11 | ||
| 12 | export interface Module { | 12 | export interface Module { |
| ... | @@ -35,68 +35,67 @@ export interface Context { | ... | @@ -35,68 +35,67 @@ export interface Context { |
| 35 | 35 | ||
| 36 | export const markoOnly = true; | 36 | export const markoOnly = true; |
| 37 | 37 | ||
| 38 | <const/module: Module = input.module /> | 38 | <const/module: Module=input.module> |
| 39 | <const/{ date, translation } = module /> | 39 | <const/{ date, translation }=module> |
| 40 | <const/{ title, description, keywords, canonical } = module.meta /> | 40 | <const/{ title, description, keywords, canonical }=module.meta> |
| 41 | <const/slug = canonical.replace(/^\/blog\/|\.[a-zA-Z_-]+$/g, '') /> | 41 | <const/slug=canonical.replace(/^\/blog\/|\.[a-zA-Z_-]+$/g, "")> |
| 42 | 42 | ||
| 43 | <const/translations= | 43 | <const/translations=( |
| 44 | Path.resolve(import.meta.dirname, "../pages/" + slug).readDirSync() | 44 | Path.resolve(import.meta.dirname, "../pages/" + slug) |
| 45 | .readDirSync() | ||
| 45 | .filter((x) => x.ext === ".mdo" && x.baseWithoutExt !== "en") | 46 | .filter((x) => x.ext === ".mdo" && x.baseWithoutExt !== "en") |
| 46 | .map((x) => x.baseWithoutExt) /> | 47 | .map((x) => x.baseWithoutExt) |
| 47 | <const/t = UNWRAP(localization.languages[translation?.lang ?? "en"]) /> | 48 | )> |
| 49 | <const/t=UNWRAP(localization.languages[translation?.lang ?? "en"])> | ||
| 48 | 50 | ||
| 49 | <const/_=(() => { | 51 | <const/_=(() => { |
| 50 | $global.cloverOutline = input.outline; | 52 | $global.cloverOutline = input.outline; |
| 51 | })() /> | 53 | })()> |
| 52 | 54 | ||
| 53 | <main> | 55 | <main> |
| 54 | <header> | 56 | <header> |
| 55 | <a href="/">${t.return}</a> | 57 | <a href="/">${t.return}</a> |
| 58 | <h1 style={ "max-width": "30ch" }>${title}</h1> | ||
| 59 | <p class="description"><em>${description}</em></p> | ||
| 56 | 60 | ||
| 57 | <h1 style={ 'max-width': "30ch" }>${title}</> | 61 | <div.meta> |
| 58 | <p class="description"><em>${description}</em></p> | 62 | <a class="meta-author" href="/">${t.writtenByClover}</a><span>•</span> |
| 63 | <if=translation> | ||
| 64 | <a.meta-translate href=translation.href> | ||
| 65 | ${t.translatedBy(translation)} | ||
| 66 | </a> | ||
| 67 | <span>•</span> | ||
| 68 | </if> | ||
| 69 | <span.meta-date>${date}</span> | ||
| 70 | </div> | ||
| 71 | <div.tag-list> | ||
| 72 | <if=translations.length> | ||
| 73 | <a | ||
| 74 | class=["custom tag lang lang-original", { active: !translation }] | ||
| 75 | href=`/blog/${slug}` | ||
| 76 | > | ||
| 77 | ${localization.languages.en.title} | ||
| 78 | </a> | ||
| 79 | <for|code| of=translations> | ||
| 80 | <a | ||
| 81 | class=["custom tag lang", { active: code === translation?.lang }] | ||
| 82 | href=`/blog/${slug}.${code}` | ||
| 83 | > | ||
| 84 | ${UNWRAP(localization.languages[code]).title} | ||
| 85 | </a> | ||
| 86 | </for> | ||
| 87 | <a.custom.tag.square href="/blog/community-translations">${"+"}</a> | ||
| 88 | <span.bar/> | ||
| 89 | </if> | ||
| 90 | <for|tag| of=keywords> | ||
| 91 | <div.tag>${tag}</div> | ||
| 92 | </for> | ||
| 93 | </div> | ||
| 59 | 94 | ||
| 60 | <div.meta> | 95 | <hr> |
| 61 | <a class="meta-author" href="/">${t.writtenByClover}</a> | 96 | </header> |
| 62 | <span>•</span> | ||
| 63 | <if=translation> | ||
| 64 | <a.meta-translate href=translation.href> | ||
| 65 | ${t.translatedBy(translation)} | ||
| 66 | </a> | ||
| 67 | <span>•</span> | ||
| 68 | </> | ||
| 69 | <span.meta-date>${date}</span> | ||
| 70 | </div> | ||
| 71 | <div.tag-list> | ||
| 72 | <if=translations.length> | ||
| 73 | <a | ||
| 74 | class=["custom tag lang lang-original", { active: !translation }] | ||
| 75 | href=`/blog/${slug}` | ||
| 76 | > | ||
| 77 | ${localization.languages.en.title} | ||
| 78 | </a> | ||
| 79 | <for|code| of=translations> | ||
| 80 | <a | ||
| 81 | class=["custom tag lang", { active: code === translation?.lang }] | ||
| 82 | href=`/blog/${slug}.${code}` | ||
| 83 | > | ||
| 84 | ${UNWRAP(localization.languages[code]).title} | ||
| 85 | </a> | ||
| 86 | </> | ||
| 87 | <a.custom.tag.square href="/blog/community-translations">${"+"}</a> | ||
| 88 | <span.bar /> | ||
| 89 | </> | ||
| 90 | <for|tag| of=keywords> | ||
| 91 | <div.tag>${tag}</div> | ||
| 92 | </> | ||
| 93 | </div> | ||
| 94 | |||
| 95 | <hr /> | ||
| 96 | </header> | ||
| 97 | |||
| 98 | <${input.content} /> | ||
| 99 | 97 | ||
| 98 | <${input.content}/> | ||
| 100 | </main> | 99 | </main> |
| 101 | 100 | ||
| 102 | import * as localization from "../localization.ts"; | 101 | import * as localization from "../localization.ts"; |
src/blog/tags/code-block.marko+14-9| ... | @@ -1,15 +1,20 @@ | ... | @@ -1,15 +1,20 @@ |
| 1 | <const/{ filename, language, tint, whitespace, content } = input /> | 1 | <const/{ filename, language, tint, whitespace, content }=input> |
| 2 | 2 | ||
| 3 | <const/highlightedPromise = ["tsx", "jsx", "json", "html", "ts", "js"].includes(language) | 3 | <const/highlightedPromise=( |
| 4 | ? highlight.highlightCode(content.trim(), language) | 4 | ["tsx", "jsx", "json", "html", "ts", "js"].includes(language) |
| 5 | : content.trim() | 5 | ? highlight.highlightCode(content.trim(), language) |
| 6 | /> | 6 | : content.trim() |
| 7 | )> | ||
| 7 | 8 | ||
| 8 | <figure class=["code", tint]> | 9 | <figure class=["code", tint]> |
| 9 | <if=filename><figcaption>${filename}</figcaption></> | 10 | <if=filename> |
| 10 | <await|highlighted|=highlightedPromise> | 11 | <figcaption>${filename}</figcaption> |
| 11 | <pre style=whitespace ? { "white-space": whitespace } : {}><code>$!{highlighted}</code></pre> | 12 | </if> |
| 12 | </> | 13 | <await|highlighted|=highlightedPromise> |
| 14 | <pre style=whitespace ? { "white-space": whitespace } : {}><code>$!{ | ||
| 15 | highlighted | ||
| 16 | }</code></pre> | ||
| 17 | </await> | ||
| 13 | </figure> | 18 | </figure> |
| 14 | 19 | ||
| 15 | import * as highlight from "#src/file-viewer/highlight.ts"; | 20 | import * as highlight from "#src/file-viewer/highlight.ts"; |
src/blog/tags/heading.marko+3-3| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | <const/{ level, content, ...attrs } = input /> | 1 | <const/{ level, content, ...attrs }=input> |
| 2 | <${'h' + level} ...attrs> | 2 | <${"h" + level} ...attrs> |
| 3 | <a.return href="#toc">§</a> | 3 | <a.return href="#toc">§</a> |
| 4 | <${content} /> | 4 | <${content}/> |
| 5 | </> | 5 | </> |
src/blog/tags/table-of-contents.marko+18-10| ... | @@ -5,25 +5,33 @@ export interface Node { | ... | @@ -5,25 +5,33 @@ export interface Node { |
| 5 | children: Node[]; | 5 | children: Node[]; |
| 6 | } | 6 | } |
| 7 | 7 | ||
| 8 | <const/outline = $global.cloverOutline /> | 8 | <const/outline=$global.cloverOutline> |
| 9 | <const/groups = markodown.outlineToTree(outline) /> | 9 | <const/groups=markodown.outlineToTree(outline)> |
| 10 | 10 | ||
| 11 | <define/Recurse|input: markodown.HeadingTree|> | 11 | <define/Recurse|input: markodown.HeadingTree|> |
| 12 | <a href=`#${input.id}`><${input.content} /></a> | 12 | <a href=`#${input.id}`> |
| 13 | <${input.content}/> | ||
| 14 | </a> | ||
| 13 | <if=input.children.length> | 15 | <if=input.children.length> |
| 14 | <ul> | 16 | <ul> |
| 15 | <for|item| of=input.children> | 17 | <for|item| of=input.children> |
| 16 | <li><Recurse ...item /></li> | 18 | <li> |
| 17 | </> | 19 | <Recurse ...item/> |
| 20 | </li> | ||
| 21 | </for> | ||
| 18 | </ul> | 22 | </ul> |
| 19 | </> | 23 | </if> |
| 20 | </> | 24 | </define> |
| 21 | 25 | ||
| 22 | <div#toc> | 26 | <div#toc> |
| 23 | <h2>Contents:</h2> | 27 | <h2>Contents:</h2> |
| 24 | <ul><for|item| of=groups> | 28 | <ul> |
| 25 | <li><Recurse ...item /></li> | 29 | <for|item| of=groups> |
| 26 | </></ul> | 30 | <li> |
| 31 | <Recurse ...item/> | ||
| 32 | </li> | ||
| 33 | </for> | ||
| 34 | </ul> | ||
| 27 | </div> | 35 | </div> |
| 28 | 36 | ||
| 29 | import * as markodown from "@clo/markodown"; | 37 | import * as markodown from "@clo/markodown"; |
src/file-viewer/bin/file-scan.ts+42-54| ... | @@ -15,8 +15,7 @@ const sotToken = process.env.CLOVER_SOT_KEY; | ... | @@ -15,8 +15,7 @@ const sotToken = process.env.CLOVER_SOT_KEY; |
| 15 | export async function main() { | 15 | export async function main() { |
| 16 | const start = performance.now(); | 16 | const start = performance.now(); |
| 17 | using _ = log.startWidget({ | 17 | using _ = log.startWidget({ |
| 18 | format: ({ now }) => | 18 | format: ({ now }) => `paper clover's file scanner [${((now - start) / 1000).toFixed(1)}s]`, |
| 19 | `paper clover's file scanner [${((now - start) / 1000).toFixed(1)}s]`, | ||
| 20 | }); | 19 | }); |
| 21 | 20 | ||
| 22 | const promises = new async.PromiseAggregator(); | 21 | const promises = new async.PromiseAggregator(); |
| ... | @@ -63,10 +62,10 @@ export async function main() { | ... | @@ -63,10 +62,10 @@ export async function main() { |
| 63 | } | 62 | } |
| 64 | 63 | ||
| 65 | if ( | 64 | if ( |
| 66 | !mediaFile || // All processes must be performed if there is no file. | 65 | !mediaFile // All processes must be performed if there is no file. |
| 67 | // Rerun all processors if it changed | 66 | // Rerun all processors if it changed |
| 68 | stat.size !== mediaFile.size || | 67 | || stat.size !== mediaFile.size |
| 69 | stat.mtime.getTime() !== mediaFile.date.getTime() | 68 | || stat.mtime.getTime() !== mediaFile.date.getTime() |
| 70 | ) { | 69 | ) { |
| 71 | promises.push(updateMetadata({ path, publicPath, stat, mediaFile })); | 70 | promises.push(updateMetadata({ path, publicPath, stat, mediaFile })); |
| 72 | } else { | 71 | } else { |
| ... | @@ -101,9 +100,9 @@ export async function main() { | ... | @@ -101,9 +100,9 @@ export async function main() { |
| 101 | }); | 100 | }); |
| 102 | let date = stat.mtime; | 101 | let date = stat.mtime; |
| 103 | if ( | 102 | if ( |
| 104 | mediaFile && | 103 | mediaFile |
| 105 | mediaFile.date.getTime() < stat.mtime.getTime() && | 104 | && mediaFile.date.getTime() < stat.mtime.getTime() |
| 106 | Date.now() - stat.mtime.getTime() < monthMilliseconds | 105 | && Date.now() - stat.mtime.getTime() < monthMilliseconds |
| 107 | ) { | 106 | ) { |
| 108 | date = mediaFile.date; | 107 | date = mediaFile.date; |
| 109 | console.warn( | 108 | console.warn( |
| ... | @@ -137,7 +136,7 @@ export async function main() { | ... | @@ -137,7 +136,7 @@ export async function main() { |
| 137 | }, | 136 | }, |
| 138 | () => ({ priority: -1 }), | 137 | () => ({ priority: -1 }), |
| 139 | ); | 138 | ); |
| 140 | const queueFileProcessor = queue.wrap(async function ({ | 139 | const queueFileProcessor = queue.wrap(async function({ |
| 141 | path, | 140 | path, |
| 142 | stat, | 141 | stat, |
| 143 | mediaFile, | 142 | mediaFile, |
| ... | @@ -190,9 +189,7 @@ export async function main() { | ... | @@ -190,9 +189,7 @@ export async function main() { |
| 190 | node.passive = true; | 189 | node.passive = true; |
| 191 | 190 | ||
| 192 | const ext = mediaFile.extensionNonEmpty.toLowerCase(); | 191 | const ext = mediaFile.extensionNonEmpty.toLowerCase(); |
| 193 | let possible = processors.filter((p) => | 192 | let possible = processors.filter((p) => p.include ? p.include.has(ext) : !p.exclude?.has(ext)); |
| 194 | p.include ? p.include.has(ext) : !p.exclude?.has(ext) | ||
| 195 | ); | ||
| 196 | if (possible.length === 0) return; | 193 | if (possible.length === 0) return; |
| 197 | ASSERT(possible.length < 16, "too many bits"); | 194 | ASSERT(possible.length < 16, "too many bits"); |
| 198 | 195 | ||
| ... | @@ -227,9 +224,7 @@ export async function main() { | ... | @@ -227,9 +224,7 @@ export async function main() { |
| 227 | .join(";"), | 224 | .join(";"), |
| 228 | ); | 225 | ); |
| 229 | } else { | 226 | } else { |
| 230 | possible = order.map(({ id }) => | 227 | possible = order.map(({ id }) => UNWRAP(possible.find((p) => p.id === id))); |
| 231 | UNWRAP(possible.find((p) => p.id === id)) | ||
| 232 | ); | ||
| 233 | } | 228 | } |
| 234 | 229 | ||
| 235 | // Queue needed processors. | 230 | // Queue needed processors. |
| ... | @@ -294,14 +289,11 @@ export async function main() { | ... | @@ -294,14 +289,11 @@ export async function main() { |
| 294 | const children = dir.getChildren(); | 289 | const children = dir.getChildren(); |
| 295 | 290 | ||
| 296 | // readme.txt | 291 | // readme.txt |
| 297 | const readmeContent = children.find((x) => | 292 | const readmeContent = children.find((x) => x.basename === "readme.txt")?.contents ?? ""; |
| 298 | x.basename === "readme.txt" | ||
| 299 | )?.contents ?? ""; | ||
| 300 | 293 | ||
| 301 | // dirsort | 294 | // dirsort |
| 302 | let dirsort: string[] | null = null; | 295 | let dirsort: string[] | null = null; |
| 303 | const dirSortRaw = | 296 | const dirSortRaw = children.find((x) => x.basename === ".dirsort")?.contents ?? ""; |
| 304 | children.find((x) => x.basename === ".dirsort")?.contents ?? ""; | ||
| 305 | if (dirSortRaw) { | 297 | if (dirSortRaw) { |
| 306 | dirsort = dirSortRaw | 298 | dirsort = dirSortRaw |
| 307 | .split("\n") | 299 | .split("\n") |
| ... | @@ -417,9 +409,9 @@ export async function main() { | ... | @@ -417,9 +409,9 @@ export async function main() { |
| 417 | } else console.warn("Missing SOT token"); | 409 | } else console.warn("Missing SOT token"); |
| 418 | 410 | ||
| 419 | console.info( | 411 | console.info( |
| 420 | "Updated file viewer index in \x1b[1m" + | 412 | "Updated file viewer index in \x1b[1m" |
| 421 | ((performance.now() - start) / 1000).toFixed(1) + | 413 | + ((performance.now() - start) / 1000).toFixed(1) |
| 422 | "s\x1b[0m", | 414 | + "s\x1b[0m", |
| 423 | ); | 415 | ); |
| 424 | 416 | ||
| 425 | const { duration, count } = MediaFile.db | 417 | const { duration, count } = MediaFile.db |
| ... | @@ -442,12 +434,12 @@ export async function main() { | ... | @@ -442,12 +434,12 @@ export async function main() { |
| 442 | 434 | ||
| 443 | console.info(); | 435 | console.info(); |
| 444 | console.info( | 436 | console.info( |
| 445 | "Global Stats:\n" + | 437 | "Global Stats:\n" |
| 446 | `- File Count: \x1b[1m${count}\x1b[0m\n` + | 438 | + `- File Count: \x1b[1m${count}\x1b[0m\n` |
| 447 | `- Derived Count: \x1b[1m${derivedCount}\x1b[0m\n` + | 439 | + `- Derived Count: \x1b[1m${derivedCount}\x1b[0m\n` |
| 448 | `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n` + | 440 | + `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n` |
| 449 | `- Canonical Size: \x1b[1m${formatSize(canonicalSize)}\x1b[0m\n` + | 441 | + `- Canonical Size: \x1b[1m${formatSize(canonicalSize)}\x1b[0m\n` |
| 450 | `- Derived Size: \x1b[1m${formatSize(derivedSize)}\x1b[0m\n`, | 442 | + `- Derived Size: \x1b[1m${formatSize(derivedSize)}\x1b[0m\n`, |
| 451 | ); | 443 | ); |
| 452 | } | 444 | } |
| 453 | 445 | ||
| ... | @@ -837,8 +829,8 @@ const processors = [ | ... | @@ -837,8 +829,8 @@ const processors = [ |
| 837 | crypto | 829 | crypto |
| 838 | .createHash("sha1") | 830 | .createHash("sha1") |
| 839 | .update( | 831 | .update( |
| 840 | process.run.toString() + | 832 | process.run.toString() |
| 841 | (process.version ? String(process.version) : ""), | 833 | + (process.version ? String(process.version) : ""), |
| 842 | ) | 834 | ) |
| 843 | .digest().buffer, | 835 | .digest().buffer, |
| 844 | ).reduce((a, b) => a ^ b), | 836 | ).reduce((a, b) => a ^ b), |
| ... | @@ -888,12 +880,12 @@ function skipBasename(basename: string): boolean { | ... | @@ -888,12 +880,12 @@ function skipBasename(basename: string): boolean { |
| 888 | if (basename === ".date") return false; | 880 | if (basename === ".date") return false; |
| 889 | 881 | ||
| 890 | return ( | 882 | return ( |
| 891 | basename.startsWith(".") || | 883 | basename.startsWith(".") |
| 892 | // basename.startsWith("._") || | 884 | // basename.startsWith("._") || |
| 893 | // basename.startsWith(".tmp") || | 885 | // basename.startsWith(".tmp") || |
| 894 | // basename === ".DS_Store" || | 886 | // basename === ".DS_Store" || |
| 895 | basename.toLowerCase() === "thumbs.db" || | 887 | || basename.toLowerCase() === "thumbs.db" |
| 896 | basename.toLowerCase() === "desktop.ini" | 888 | || basename.toLowerCase() === "desktop.ini" |
| 897 | ); | 889 | ); |
| 898 | } | 890 | } |
| 899 | 891 | ||
| ... | @@ -946,8 +938,8 @@ async function scrubLocationMetadata( | ... | @@ -946,8 +938,8 @@ async function scrubLocationMetadata( |
| 946 | "-s", | 938 | "-s", |
| 947 | path.toString(), | 939 | path.toString(), |
| 948 | ]); | 940 | ]); |
| 949 | hasLocation = videoCheck.includes("GPS") || | 941 | hasLocation = videoCheck.includes("GPS") |
| 950 | videoCheck.includes("Location"); | 942 | || videoCheck.includes("Location"); |
| 951 | args = [ | 943 | args = [ |
| 952 | "-gps:all=", | 944 | "-gps:all=", |
| 953 | "-xmp:all=", | 945 | "-xmp:all=", |
| ... | @@ -963,10 +955,10 @@ async function scrubLocationMetadata( | ... | @@ -963,10 +955,10 @@ async function scrubLocationMetadata( |
| 963 | "-s", | 955 | "-s", |
| 964 | path.toString(), | 956 | path.toString(), |
| 965 | ]); | 957 | ]); |
| 966 | hasLocation = m4aCheck.includes("GPS") || | 958 | hasLocation = m4aCheck.includes("GPS") |
| 967 | m4aCheck.includes("Location") || | 959 | || m4aCheck.includes("Location") |
| 968 | m4aCheck.includes("Filename") || | 960 | || m4aCheck.includes("Filename") |
| 969 | m4aCheck.includes("Title"); | 961 | || m4aCheck.includes("Title"); |
| 970 | 962 | ||
| 971 | if (hasLocation) { | 963 | if (hasLocation) { |
| 972 | args = [ | 964 | args = [ |
| ... | @@ -1028,13 +1020,13 @@ async function scrubLocationMetadata( | ... | @@ -1028,13 +1020,13 @@ async function scrubLocationMetadata( |
| 1028 | 1020 | ||
| 1029 | const monthMilliseconds = 30 * 24 * 60 * 60 * 1000; | 1021 | const monthMilliseconds = 30 * 24 * 60 * 60 * 1000; |
| 1030 | 1022 | ||
| 1031 | import * as async from "@clo/lib/async"; | ||
| 1032 | import * as fs from "#sitegen/fs"; | 1023 | import * as fs from "#sitegen/fs"; |
| 1033 | import * as subprocess from "@clo/lib/subprocess"; | ||
| 1034 | import { Path } from "#sitegen/path"; | 1024 | import { Path } from "#sitegen/path"; |
| 1035 | import * as queue from "@clo/lib/queue"; | 1025 | import * as async from "@clo/lib/async"; |
| 1036 | import * as log from "@clo/lib/log"; | 1026 | import * as log from "@clo/lib/log"; |
| 1037 | import * as progress from "@clo/lib/progress"; | 1027 | import * as progress from "@clo/lib/progress"; |
| 1028 | import * as queue from "@clo/lib/queue"; | ||
| 1029 | import * as subprocess from "@clo/lib/subprocess"; | ||
| 1038 | import * as ts from "@clo/lib/ts"; | 1030 | import * as ts from "@clo/lib/ts"; |
| 1039 | 1031 | ||
| 1040 | import * as child_process from "node:child_process"; | 1032 | import * as child_process from "node:child_process"; |
| ... | @@ -1044,19 +1036,15 @@ import * as path from "node:path"; | ... | @@ -1044,19 +1036,15 @@ import * as path from "node:path"; |
| 1044 | import * as stream from "node:stream"; | 1036 | import * as stream from "node:stream"; |
| 1045 | import * as zlib from "node:zlib"; | 1037 | import * as zlib from "node:zlib"; |
| 1046 | 1038 | ||
| 1047 | import { MediaFile, MediaFileKind } from "#src/file-viewer/models/MediaFile.ts"; | 1039 | import { formatDate, formatDurationLong, formatSize } from "#src/file-viewer/format.ts"; |
| 1048 | import { FilePermissions } from "#src/file-viewer/models/FilePermissions.ts"; | ||
| 1049 | import { | ||
| 1050 | formatDate, | ||
| 1051 | formatDurationLong, | ||
| 1052 | formatSize, | ||
| 1053 | } from "#src/file-viewer/format.ts"; | ||
| 1054 | import * as rules from "#src/file-viewer/rules.ts"; | ||
| 1055 | import * as highlight from "#src/file-viewer/highlight.ts"; | 1040 | import * as highlight from "#src/file-viewer/highlight.ts"; |
| 1056 | import * as ffmpeg from "@clo/lib/subprocess/ffmpeg"; | ||
| 1057 | import * as rsync from "#src/file-viewer/rsync.ts"; | ||
| 1058 | import * as derived from "#src/file-viewer/models/derived.ts"; | 1041 | import * as derived from "#src/file-viewer/models/derived.ts"; |
| 1042 | import { FilePermissions } from "#src/file-viewer/models/FilePermissions.ts"; | ||
| 1043 | import { MediaFile, MediaFileKind } from "#src/file-viewer/models/MediaFile.ts"; | ||
| 1044 | import * as rsync from "#src/file-viewer/rsync.ts"; | ||
| 1045 | import * as rules from "#src/file-viewer/rules.ts"; | ||
| 1059 | import * as transcodeRules from "#src/file-viewer/transcode-rules.ts"; | 1046 | import * as transcodeRules from "#src/file-viewer/transcode-rules.ts"; |
| 1047 | import * as ffmpeg from "@clo/lib/subprocess/ffmpeg"; | ||
| 1060 | 1048 | ||
| 1061 | import { rawFileRoot as root } from "../paths.ts"; | ||
| 1062 | import { ASSERT, UNWRAP } from "@clo/lib/assert"; | 1049 | import { ASSERT, UNWRAP } from "@clo/lib/assert"; |
| 1050 | import { rawFileRoot as root } from "../paths.ts"; |
src/file-viewer/bin/file-trim.ts+1-2| ... | @@ -1,8 +1,7 @@ | ... | @@ -1,8 +1,7 @@ |
| 1 | export async function main() { | 1 | export async function main() { |
| 2 | const start = performance.now(); | 2 | const start = performance.now(); |
| 3 | using _ = log.startWidget({ | 3 | using _ = log.startWidget({ |
| 4 | format: (now) => | 4 | format: (now) => `paper clover's file scanner [${((now - start) / 1000).toFixed(1)}s]`, |
| 5 | `paper clover's file scanner [${((now - start) / 1000).toFixed(1)}s]`, | ||
| 6 | }); | 5 | }); |
| 7 | 6 | ||
| 8 | const orphaned = derived.findOrphanedRoots(); | 7 | const orphaned = derived.findOrphanedRoots(); |
src/file-viewer/transcode-rules.test.ts+8-19| ... | @@ -226,15 +226,10 @@ test("dense sweep never produces mismatched ladder aspect ratios", () => { | ... | @@ -226,15 +226,10 @@ test("dense sweep never produces mismatched ladder aspect ratios", () => { |
| 226 | } | 226 | } |
| 227 | }); | 227 | }); |
| 228 | 228 | ||
| 229 | import { test } from "vitest"; | ||
| 230 | import assert from "node:assert"; | ||
| 231 | import { Path } from "#sitegen/path"; | 229 | import { Path } from "#sitegen/path"; |
| 232 | import { | 230 | import assert from "node:assert"; |
| 233 | getAv1VideoArgs, | 231 | import { test } from "vitest"; |
| 234 | getConstantAspectScaleFilter, | 232 | import { getAv1VideoArgs, getConstantAspectScaleFilter, getMpegDashArgs, videoFormats } from "./transcode-rules.ts"; |
| 235 | getMpegDashArgs, | ||
| 236 | videoFormats, | ||
| 237 | } from "./transcode-rules.ts"; | ||
| 238 | 233 | ||
| 239 | function assertString(value: string | undefined | null): string { | 234 | function assertString(value: string | undefined | null): string { |
| 240 | assert.equal(typeof value, "string"); | 235 | assert.equal(typeof value, "string"); |
| ... | @@ -275,9 +270,7 @@ const EXACT_ERROR_THRESHOLD = 0.0005; | ... | @@ -275,9 +270,7 @@ const EXACT_ERROR_THRESHOLD = 0.0005; |
| 275 | function getFeasibleCandidates(source: { width: number; height: number }) { | 270 | function getFeasibleCandidates(source: { width: number; height: number }) { |
| 276 | const caps = [ | 271 | const caps = [ |
| 277 | source.height, | 272 | source.height, |
| 278 | ...videoFormats.flatMap((preset) => | 273 | ...videoFormats.flatMap((preset) => preset.maxHeight != null ? [Math.min(preset.maxHeight, source.height)] : []), |
| 279 | preset.maxHeight != null ? [Math.min(preset.maxHeight, source.height)] : [] | ||
| 280 | ), | ||
| 281 | ]; | 274 | ]; |
| 282 | const sourceRatio = source.width / source.height; | 275 | const sourceRatio = source.width / source.height; |
| 283 | const maxDenominator = Math.max(1, Math.floor(Math.min(...caps) / 2)); | 276 | const maxDenominator = Math.max(1, Math.floor(Math.min(...caps) / 2)); |
| ... | @@ -300,16 +293,14 @@ function getFeasibleCandidates(source: { width: number; height: number }) { | ... | @@ -300,16 +293,14 @@ function getFeasibleCandidates(source: { width: number; height: number }) { |
| 300 | if (seen.has(key)) continue; | 293 | if (seen.has(key)) continue; |
| 301 | seen.add(key); | 294 | seen.add(key); |
| 302 | 295 | ||
| 303 | const dims = videoFormats.map((preset) => | 296 | const dims = videoFormats.map((preset) => getExpectedDimensionsForPreset(source, candidate, preset.maxHeight)); |
| 304 | getExpectedDimensionsForPreset(source, candidate, preset.maxHeight) | ||
| 305 | ); | ||
| 306 | if (dims.some((value) => value.width <= 0 || value.height <= 0)) continue; | 297 | if (dims.some((value) => value.width <= 0 || value.height <= 0)) continue; |
| 307 | 298 | ||
| 308 | const retainedArea = dims | 299 | const retainedArea = dims |
| 309 | .map((value) => value.width * value.height) | 300 | .map((value) => value.width * value.height) |
| 310 | .reduce((sum, value) => sum + value, 0); | 301 | .reduce((sum, value) => sum + value, 0); |
| 311 | const error = Math.abs((candidate.width / candidate.height) - sourceRatio) / | 302 | const error = Math.abs((candidate.width / candidate.height) - sourceRatio) |
| 312 | sourceRatio; | 303 | / sourceRatio; |
| 313 | candidates.push({ ...candidate, error, retainedArea }); | 304 | candidates.push({ ...candidate, error, retainedArea }); |
| 314 | } | 305 | } |
| 315 | 306 | ||
| ... | @@ -337,9 +328,7 @@ function getCandidateScore( | ... | @@ -337,9 +328,7 @@ function getCandidateScore( |
| 337 | source: { width: number; height: number }, | 328 | source: { width: number; height: number }, |
| 338 | ratio: { width: number; height: number }, | 329 | ratio: { width: number; height: number }, |
| 339 | ) { | 330 | ) { |
| 340 | const dims = videoFormats.map((preset) => | 331 | const dims = videoFormats.map((preset) => getExpectedDimensionsForPreset(source, ratio, preset.maxHeight)); |
| 341 | getExpectedDimensionsForPreset(source, ratio, preset.maxHeight) | ||
| 342 | ); | ||
| 343 | if (dims.some((value) => value.width <= 0 || value.height <= 0)) return null; | 332 | if (dims.some((value) => value.width <= 0 || value.height <= 0)) return null; |
| 344 | const sourceRatio = source.width / source.height; | 333 | const sourceRatio = source.width / source.height; |
| 345 | return { | 334 | return { |
src/file-viewer/transcode-rules.ts+42-31| ... | @@ -202,9 +202,7 @@ function getCanonicalSquarePixelAspectRatio(source: Dimensions): Dimensions { | ... | @@ -202,9 +202,7 @@ function getCanonicalSquarePixelAspectRatio(source: Dimensions): Dimensions { |
| 202 | if (seen.has(key)) continue; | 202 | if (seen.has(key)) continue; |
| 203 | seen.add(key); | 203 | seen.add(key); |
| 204 | 204 | ||
| 205 | const dimensions = caps.map((cap) => | 205 | const dimensions = caps.map((cap) => getExactSquarePixelDimensions(source, aspect, cap)); |
| 206 | getExactSquarePixelDimensions(source, aspect, cap) | ||
| 207 | ); | ||
| 208 | if (dimensions.some((value) => value == null)) continue; | 206 | if (dimensions.some((value) => value == null)) continue; |
| 209 | 207 | ||
| 210 | const retainedArea = dimensions | 208 | const retainedArea = dimensions |
| ... | @@ -215,11 +213,11 @@ function getCanonicalSquarePixelAspectRatio(source: Dimensions): Dimensions { | ... | @@ -215,11 +213,11 @@ function getCanonicalSquarePixelAspectRatio(source: Dimensions): Dimensions { |
| 215 | 213 | ||
| 216 | if (error <= exactErrorThreshold) { | 214 | if (error <= exactErrorThreshold) { |
| 217 | if ( | 215 | if ( |
| 218 | !bestWithinThreshold || | 216 | !bestWithinThreshold |
| 219 | retainedArea > bestWithinThreshold.retainedArea || | 217 | || retainedArea > bestWithinThreshold.retainedArea |
| 220 | ( | 218 | || ( |
| 221 | retainedArea === bestWithinThreshold.retainedArea && | 219 | retainedArea === bestWithinThreshold.retainedArea |
| 222 | error < bestWithinThreshold.error | 220 | && error < bestWithinThreshold.error |
| 223 | ) | 221 | ) |
| 224 | ) { | 222 | ) { |
| 225 | bestWithinThreshold = candidate; | 223 | bestWithinThreshold = candidate; |
| ... | @@ -228,9 +226,9 @@ function getCanonicalSquarePixelAspectRatio(source: Dimensions): Dimensions { | ... | @@ -228,9 +226,9 @@ function getCanonicalSquarePixelAspectRatio(source: Dimensions): Dimensions { |
| 228 | } | 226 | } |
| 229 | 227 | ||
| 230 | if ( | 228 | if ( |
| 231 | !bestFallback || | 229 | !bestFallback |
| 232 | error < bestFallback.error || | 230 | || error < bestFallback.error |
| 233 | (error === bestFallback.error && retainedArea > bestFallback.retainedArea) | 231 | || (error === bestFallback.error && retainedArea > bestFallback.retainedArea) |
| 234 | ) { | 232 | ) { |
| 235 | bestFallback = candidate; | 233 | bestFallback = candidate; |
| 236 | } | 234 | } |
| ... | @@ -247,8 +245,8 @@ export function getConstantAspectScaleFilter( | ... | @@ -247,8 +245,8 @@ export function getConstantAspectScaleFilter( |
| 247 | const dimensions = getExactSquarePixelDimensions(source, aspect, maxHeight); | 245 | const dimensions = getExactSquarePixelDimensions(source, aspect, maxHeight); |
| 248 | if (!dimensions) return null; | 246 | if (!dimensions) return null; |
| 249 | if ( | 247 | if ( |
| 250 | dimensions.width === source.width && | 248 | dimensions.width === source.width |
| 251 | dimensions.height === source.height | 249 | && dimensions.height === source.height |
| 252 | ) return null; | 250 | ) return null; |
| 253 | return `scale=${dimensions.width}:${dimensions.height}`; | 251 | return `scale=${dimensions.width}:${dimensions.height}`; |
| 254 | } | 252 | } |
| ... | @@ -336,9 +334,7 @@ export function getMpegDashArgs( | ... | @@ -336,9 +334,7 @@ export function getMpegDashArgs( |
| 336 | if (audios.length > 0) { | 334 | if (audios.length > 0) { |
| 337 | const audioId = videos.length > 0 ? 1 : 0; | 335 | const audioId = videos.length > 0 ? 1 : 0; |
| 338 | const audioIndices = audios.map((_, i) => videos.length + i).join(","); | 336 | const audioIndices = audios.map((_, i) => videos.length + i).join(","); |
| 339 | adaptationSets += `${ | 337 | adaptationSets += `${adaptationSets ? " " : ""}id=${audioId},streams=${audioIndices}`; |
| 340 | adaptationSets ? " " : "" | ||
| 341 | }id=${audioId},streams=${audioIndices}`; | ||
| 342 | } | 338 | } |
| 343 | 339 | ||
| 344 | cmd.push("-adaptation_sets", adaptationSets); | 340 | cmd.push("-adaptation_sets", adaptationSets); |
| ... | @@ -378,11 +374,16 @@ export function getH264HlsArgs( | ... | @@ -378,11 +374,16 @@ export function getH264HlsArgs( |
| 378 | const aIdx = i * 2 + 1; | 374 | const aIdx = i * 2 + 1; |
| 379 | // deno-fmt-ignore | 375 | // deno-fmt-ignore |
| 380 | cmd.push( | 376 | cmd.push( |
| 381 | `-c:${vIdx}`, "libx264", | 377 | `-c:${vIdx}`, |
| 382 | `-b:v:${vIdx}`, preset.videoBitrate, | 378 | "libx264", |
| 383 | `-filter:${vIdx}`, `scale=-2:min(${preset.maxHeight}\\,ih)`, | 379 | `-b:v:${vIdx}`, |
| 384 | `-c:${aIdx}`, "aac", | 380 | preset.videoBitrate, |
| 385 | `-b:a:${aIdx}`, preset.audioBitrate, | 381 | `-filter:${vIdx}`, |
| 382 | `scale=-2:min(${preset.maxHeight}\\,ih)`, | ||
| 383 | `-c:${aIdx}`, | ||
| 384 | "aac", | ||
| 385 | `-b:a:${aIdx}`, | ||
| 386 | preset.audioBitrate, | ||
| 386 | ); | 387 | ); |
| 387 | }); | 388 | }); |
| 388 | 389 | ||
| ... | @@ -399,9 +400,12 @@ export function getH264HlsArgs( | ... | @@ -399,9 +400,12 @@ export function getH264HlsArgs( |
| 399 | hlsPresets.forEach((preset, i) => { | 400 | hlsPresets.forEach((preset, i) => { |
| 400 | // deno-fmt-ignore | 401 | // deno-fmt-ignore |
| 401 | cmd.push( | 402 | cmd.push( |
| 402 | `-c:v:${i}`, "libx264", | 403 | `-c:v:${i}`, |
| 403 | `-b:v:${i}`, preset.videoBitrate, | 404 | "libx264", |
| 404 | `-filter:v:${i}`, `scale=-2:min(${preset.maxHeight}\\,ih)`, | 405 | `-b:v:${i}`, |
| 406 | preset.videoBitrate, | ||
| 407 | `-filter:v:${i}`, | ||
| 408 | `scale=-2:min(${preset.maxHeight}\\,ih)`, | ||
| 405 | ); | 409 | ); |
| 406 | }); | 410 | }); |
| 407 | 411 | ||
| ... | @@ -413,13 +417,20 @@ export function getH264HlsArgs( | ... | @@ -413,13 +417,20 @@ export function getH264HlsArgs( |
| 413 | 417 | ||
| 414 | // deno-fmt-ignore | 418 | // deno-fmt-ignore |
| 415 | cmd.push( | 419 | cmd.push( |
| 416 | "-f", "hls", | 420 | "-f", |
| 417 | "-hls_time", "2", | 421 | "hls", |
| 418 | "-hls_list_size", "0", | 422 | "-hls_time", |
| 419 | "-hls_segment_type", "fmp4", | 423 | "2", |
| 420 | "-hls_allow_cache", "1", | 424 | "-hls_list_size", |
| 421 | "-master_pl_name", "master.m3u8", | 425 | "0", |
| 422 | "-hls_segment_filename", "hls.%v.%03d.ts", | 426 | "-hls_segment_type", |
| 427 | "fmp4", | ||
| 428 | "-hls_allow_cache", | ||
| 429 | "1", | ||
| 430 | "-master_pl_name", | ||
| 431 | "master.m3u8", | ||
| 432 | "-hls_segment_filename", | ||
| 433 | "hls.%v.%03d.ts", | ||
| 423 | "-y", | 434 | "-y", |
| 424 | outDir.join("hls.%v.m3u8").toString(), | 435 | outDir.join("hls.%v.m3u8").toString(), |
| 425 | ); | 436 | ); |
src/file-viewer/views/canvas.marko+6-5| ... | @@ -3,15 +3,16 @@ export interface Input { | ... | @@ -3,15 +3,16 @@ export interface Input { |
| 3 | } | 3 | } |
| 4 | export const meta = { title: "canvas" }; | 4 | export const meta = { title: "canvas" }; |
| 5 | 5 | ||
| 6 | <const/{ script } = input /> | 6 | <const/{ script }=input> |
| 7 | 7 | ||
| 8 | <canvas id="canvas" | 8 | <canvas |
| 9 | id="canvas" | ||
| 9 | data-canvas=script | 10 | data-canvas=script |
| 10 | data-standalone="true" | 11 | data-standalone="true" |
| 11 | style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;" | 12 | style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;" |
| 12 | ></canvas> | 13 | /> |
| 13 | <AddScript="file-viewer/scripts/canvas_" + script + ".client.ts" /> | 14 | <AddScript="file-viewer/scripts/canvas_" + script + ".client.ts"/> |
| 14 | 15 | ||
| 15 | client import "./canvas.client.ts"; | 16 | client import "./canvas.client.ts"; |
| 16 | 17 | ||
| 17 | import { addScript as AddScript } from '#sitegen'; | 18 | import { addScript as AddScript } from "#sitegen"; |
src/file-viewer/views/lofi.marko+73-52| ... | @@ -5,66 +5,87 @@ export interface Input { | ... | @@ -5,66 +5,87 @@ export interface Input { |
| 5 | } | 5 | } |
| 6 | export { meta, theme } from "./clofi.tsx"; | 6 | export { meta, theme } from "./clofi.tsx"; |
| 7 | 7 | ||
| 8 | <const/{ file: dir, hasCotyledonCookie } = input /> | 8 | <const/{ file: dir, hasCotyledonCookie }=input> |
| 9 | <const/{ path: fullPath, dirname, basename, kind } = dir /> | 9 | <const/{ path: fullPath, dirname, basename, kind }=dir> |
| 10 | <const/isRoot = fullPath == '/'> | 10 | <const/isRoot=fullPath == "/"> |
| 11 | 11 | ||
| 12 | <div#lofi> | 12 | <div#lofi> |
| 13 | <define/ListItem|{ value: file }|> | ||
| 14 | <const/dir=file.kind === MediaFileKind.directory> | ||
| 15 | <const/meta=( | ||
| 16 | dir | ||
| 17 | ? formatSize(file.size) | ||
| 18 | : (file.duration ?? 0) > 0 | ||
| 19 | ? formatDuration(file.duration!) | ||
| 20 | : null | ||
| 21 | )> | ||
| 22 | <li class={ dir }> | ||
| 23 | <a href=`/file${escapeUri(file.path)}`> | ||
| 24 | <code>${formatDate(file.date)}</code>${" "} | ||
| 25 | <span>${file.basenameWithoutExt}</span> | ||
| 26 | <if=file.extension><span class="ext">${file.extension}</span></if> | ||
| 27 | <if=dir>/</if> | ||
| 28 | <if=meta><span class="meta">&nbsp;&nbsp;&nbsp;(${meta})</span></if> | ||
| 29 | </a> | ||
| 30 | </li> | ||
| 31 | </define> | ||
| 13 | 32 | ||
| 14 | <define/ListItem|{ value: file }|> | 33 | <h1> |
| 15 | <const/dir=file.kind === MediaFileKind.directory/> | 34 | <if=isRoot>clo's files</if> |
| 16 | <const/meta=( | 35 | <else>${fullPath}</else> |
| 17 | dir | 36 | </h1> |
| 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)}</>${" "}<span>${file.basenameWithoutExt}</span><if=file.extension><span class="ext">${file.extension}</></><if=dir>/</> | ||
| 26 | <if=meta><span class='meta'>&nbsp;&nbsp;&nbsp;(${meta})</></> | ||
| 27 | </a> | ||
| 28 | </li> | ||
| 29 | </define> | ||
| 30 | 37 | ||
| 31 | <h1> | 38 | <if=isRoot> |
| 32 | <if=isRoot>clo's files</> | 39 | <const/{ readme, sections }=sort.splitRootDirFiles(dir, hasCotyledonCookie)> |
| 33 | <else>${fullPath}</> | 40 | <if=readme> |
| 34 | </h1> | 41 | <ul> |
| 35 | 42 | <ListItem=readme/> | |
| 36 | <if=isRoot> | 43 | </ul> |
| 37 | <const/{ readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie) /> | 44 | </if> |
| 38 | <if=readme><ul><ListItem=readme/></ul></> | 45 | <for|{ key, files, titleColor }| of=sections> |
| 39 | <for|{key, files, titleColor }| of=sections> | 46 | <h2 style={ color: titleColor }>${key}</h2> |
| 40 | <h2 style={color: titleColor }>${key}</> | 47 | <ul> |
| 48 | <for|item| of=files> | ||
| 49 | <ListItem=item/> | ||
| 50 | </for> | ||
| 51 | </ul> | ||
| 52 | </for> | ||
| 53 | // <if=!hasCotyledonCookie> | ||
| 54 | // <br><br><br><br><br><br> | ||
| 55 | // <p style={ opacity: 0.3 }> | ||
| 56 | // would you like to | ||
| 57 | // <a | ||
| 58 | // href="/file/cotyledon" | ||
| 59 | // style={ | ||
| 60 | // color: 'white', | ||
| 61 | // 'text-decoration': 'underline', | ||
| 62 | // } | ||
| 63 | // >dive deeper?</a> | ||
| 64 | // </p> | ||
| 65 | // </> | ||
| 66 | </if> | ||
| 67 | <else> | ||
| 41 | <ul> | 68 | <ul> |
| 42 | <for|item| of=files><ListItem=item /></> | 69 | <li> |
| 70 | <a href=`/file${escapeUri(path.posix.dirname(fullPath))}`> | ||
| 71 | [up one...] | ||
| 72 | </a> | ||
| 73 | </li> | ||
| 74 | <for|item| of=dir.getChildren()> | ||
| 75 | ${" "} | ||
| 76 | <ListItem=item/> | ||
| 77 | ${" "} | ||
| 78 | </for> | ||
| 43 | </ul> | 79 | </ul> |
| 44 | </> | 80 | </else> |
| 45 | // <if=!hasCotyledonCookie> | ||
| 46 | // <br><br><br><br><br><br> | ||
| 47 | // <p style={ opacity: 0.3 }> | ||
| 48 | // would you like to | ||
| 49 | // <a | ||
| 50 | // href="/file/cotyledon" | ||
| 51 | // style={ | ||
| 52 | // color: 'white', | ||
| 53 | // 'text-decoration': 'underline', | ||
| 54 | // } | ||
| 55 | // >dive deeper?</a> | ||
| 56 | // </p> | ||
| 57 | // </> | ||
| 58 | </><else> | ||
| 59 | <ul> | ||
| 60 | <li><a href=`/file${escapeUri(path.posix.dirname(fullPath))}`>[up one...]</a></li> | ||
| 61 | <for|item| of=dir.getChildren()> <ListItem=item /> </> | ||
| 62 | </ul> | ||
| 63 | </> | ||
| 64 | |||
| 65 | </div> | 81 | </div> |
| 66 | 82 | ||
| 67 | import * as path from "node:path"; | 83 | import * as path from "node:path"; |
| 68 | import { escapeUri, formatDuration, formatSize, formatDate } from "#src/file-viewer/format.ts"; | 84 | import { |
| 85 | escapeUri, | ||
| 86 | formatDuration, | ||
| 87 | formatSize, | ||
| 88 | formatDate, | ||
| 89 | } from "#src/file-viewer/format.ts"; | ||
| 69 | import { MediaFileKind } from "#src/file-viewer/models/MediaFile.ts"; | 90 | import { MediaFileKind } from "#src/file-viewer/models/MediaFile.ts"; |
| 70 | import * as sort from "#src/file-viewer/sort.ts"; | 91 | import * as sort from "#src/file-viewer/sort.ts"; |
src/file-viewer/views/lofideon.marko+19-15| ... | @@ -1,22 +1,26 @@ | ... | @@ -1,22 +1,26 @@ |
| 1 | export interface Input { | 1 | export interface Input { |
| 2 | stage: number | 2 | stage: number; |
| 3 | } | 3 | } |
| 4 | export const meta = { title: 'C O T Y L E D O N' }; | 4 | export const meta = { title: "C O T Y L E D O N" }; |
| 5 | export const theme = { | 5 | export const theme = { |
| 6 | bg: '#ff00ff', | 6 | bg: "#ff00ff", |
| 7 | fg: '#000000', | 7 | fg: "#000000", |
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | <h1>co<br>ty<br>le<br>don</h1> | 10 | <h1>co<br>ty<br>le<br>don</h1> |
| 11 | 11 | ||
| 12 | <if=input.stage==0> | 12 | <if=input.stage == 0> |
| 13 | 13 | <p> | |
| 14 | <p> | 14 | this place is sacred, but dangerous. i have to keep visitors to an absolute |
| 15 | this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts. | 15 | minimum; you'll get dust on all the artifacts. |
| 16 | </p><p> | 16 | </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 | 17 | <p> |
| 18 | </p><p> | 18 | by entering our museum, you agree not to use your camera. flash off isn't |
| 19 | <sub>(in english: please do not store downloads after you're done viewing them)</sub> | 19 | enough; the bits and bytes are alergic even to a camera's sensor |
| 20 | </p> | 20 | </p> |
| 21 | 21 | <p> | |
| 22 | </> | 22 | <sub> |
| 23 | (in english: please do not store downloads after you're done viewing them) | ||
| 24 | </sub> | ||
| 25 | </p> | ||
| 26 | </if> |
src/pages/admin/login.fail.marko+1-1| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | export const meta = { title: 'paper clover admin' }; | 1 | export const meta = { title: "paper clover admin" }; |
| 2 | 2 | ||
| 3 | <main> | 3 | <main> |
| 4 | <h1>login</h1> | 4 | <h1>login</h1> |
src/pages/admin/login.marko+1-1| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | export const meta = { title: 'paper clover admin' }; | 1 | export const meta = { title: "paper clover admin" }; |
| 2 | 2 | ||
| 3 | <main> | 3 | <main> |
| 4 | <h1>login</h1> | 4 | <h1>login</h1> |
src/pages/friends/auth.fail.marko+1-1| ... | @@ -5,7 +5,7 @@ export const meta = { title: "password required" }; | ... | @@ -5,7 +5,7 @@ export const meta = { title: "password required" }; |
| 5 | <p>please contact clover</p> | 5 | <p>please contact clover</p> |
| 6 | <form method="post"> | 6 | <form method="post"> |
| 7 | <h1>try again</h1> | 7 | <h1>try again</h1> |
| 8 | <input type="password" name="password" placeholder="password" /> | 8 | <input type="password" name="password" placeholder="password"> |
| 9 | <button type="submit">enter</button> | 9 | <button type="submit">enter</button> |
| 10 | </form> | 10 | </form> |
| 11 | </main> | 11 | </main> |
src/pages/friends/auth.marko+1-1| ... | @@ -3,7 +3,7 @@ export const meta = { title: "password required" }; | ... | @@ -3,7 +3,7 @@ export const meta = { title: "password required" }; |
| 3 | <main> | 3 | <main> |
| 4 | <form method="post"> | 4 | <form method="post"> |
| 5 | <h1>what's the password</h1> | 5 | <h1>what's the password</h1> |
| 6 | <input type="password" name="password" placeholder="password"/> | 6 | <input type="password" name="password" placeholder="password"> |
| 7 | <button type="submit">enter</button> | 7 | <button type="submit">enter</button> |
| 8 | <p>(clover will give the password to you if you're a friend)</p> | 8 | <p>(clover will give the password to you if you're a friend)</p> |
| 9 | </form> | 9 | </form> |
src/pages/friends/misconfigure.marko+1-1| ... | @@ -3,7 +3,7 @@ export const meta = { title: "password required" }; | ... | @@ -3,7 +3,7 @@ export const meta = { title: "password required" }; |
| 3 | <main> | 3 | <main> |
| 4 | <form method="post" action="/friends"> | 4 | <form method="post" action="/friends"> |
| 5 | <h1>what's the password</h1> | 5 | <h1>what's the password</h1> |
| 6 | <input type="password" name="password" placeholder="password" disabled /> | 6 | <input type="password" name="password" placeholder="password" disabled> |
| 7 | <button type="submit" disabled>enter</button> | 7 | <button type="submit" disabled>enter</button> |
| 8 | <p style="color:red">this page's password is not configured</p> | 8 | <p style="color:red">this page's password is not configured</p> |
| 9 | </form> | 9 | </form> |
src/pages/history-of-japan-reanimated.marko+142-53| ... | @@ -4,97 +4,186 @@ client import "./history-of-japan-reanimated.client.ts"; | ... | @@ -4,97 +4,186 @@ client import "./history-of-japan-reanimated.client.ts"; |
| 4 | 4 | ||
| 5 | export const meta = { | 5 | export const meta = { |
| 6 | title: "history of japan reanimated", | 6 | title: "history of japan reanimated", |
| 7 | description: "17 artists and musicians come together to remake bill wurtz's legendary \"history of japan\" to celebrate its 10 year anniversary.", | 7 | description: |
| 8 | '17 artists and musicians come together to remake bill wurtz\'s legendary "history of japan" to celebrate its 10 year anniversary.', | ||
| 8 | embed: { | 9 | embed: { |
| 9 | thumbnail: "https://paperclover.net/file/2026/history%20of%20japan%20reanimated/thumbnail.jpeg" | 10 | thumbnail: |
| 11 | "https://paperclover.net/file/2026/history%20of%20japan%20reanimated/thumbnail.jpeg", | ||
| 10 | }, | 12 | }, |
| 11 | canonical: "/history-of-japan-reanimated", | 13 | canonical: "/history-of-japan-reanimated", |
| 12 | twitter: { | 14 | twitter: { |
| 13 | image: "https://paperclover.net/file/2026/history%20of%20japan%20reanimated/thumbnail.jpeg" | 15 | image: |
| 16 | "https://paperclover.net/file/2026/history%20of%20japan%20reanimated/thumbnail.jpeg", | ||
| 14 | }, | 17 | }, |
| 15 | }; | 18 | }; |
| 16 | export const theme = { | 19 | export const theme = { |
| 17 | fg: "#fff", | 20 | fg: "#fff", |
| 18 | bg: "#442233", | 21 | bg: "#442233", |
| 19 | primary: "#ff8b47" | 22 | primary: "#ff8b47", |
| 20 | }; | 23 | }; |
| 21 | 24 | ||
| 22 | <main> | 25 | <main> |
| 23 | <nav><a href="/">back to home</a></nav> | 26 | <nav><a href="/">back to home</a></nav> |
| 24 | <h1>history of japan reanimated</h1> | 27 | <h1>history of japan reanimated</h1> |
| 25 | <p> | 28 | <p> |
| 26 | me and 16 other artists (music, narration, and animation) have been | 29 | me and 16 other artists (music, narration, and animation) have been spending |
| 27 | spending the past couple months on a recreation of bill wurtz's legendary | 30 | the past couple months on a recreation of bill wurtz's legendary "<a href="https://billwurtz.com/history-of-japan.html"> |
| 28 | "<a href="https://billwurtz.com/history-of-japan.html">history of japan</a>" to celebrate the 10 year anniversary of his release. i | 31 | history of japan |
| 29 | contributed two animation sections. the canonical release is on | 32 | </a>" to celebrate the 10 year anniversary of his release. i contributed two |
| 30 | <a href="https://youtu.be/nqsoti-OuRM">LunaPogi's YouTube page</a>, but i | 33 | animation sections. the canonical release is on |
| 31 | am providing this ad-free video player. | 34 | <a href="https://youtu.be/nqsoti-OuRM">LunaPogi's YouTube page</a>, but i am providing |
| 35 | this ad-free video player. | ||
| 32 | </p> | 36 | </p> |
| 33 | 37 | ||
| 34 | <clover-video | 38 | <clover-video |
| 35 | id="main-player" | 39 | id="main-player" |
| 36 | file="/2026/history of japan reanimated/history of japan reanimated.mp4" | 40 | file="/2026/history of japan reanimated/history of japan reanimated.mp4" |
| 37 | poster="/2026/history of japan reanimated/thumbnail.jpeg" | 41 | poster="/2026/history of japan reanimated/thumbnail.jpeg" |
| 38 | > | 42 | > |
| 39 | <@header>history of japan reanimated</> | 43 | <@header>history of japan reanimated</@header> |
| 40 | </> | 44 | </clover-video> |
| 41 | |||
| 42 | <div id="live-credits"> | ||
| 43 | |||
| 44 | 45 | ||
| 45 | </div> | 46 | <div id="live-credits"/> |
| 46 | 47 | ||
| 47 | <p> | 48 | <p>the wonderful souls who worked on the project, in order of appearance.</p> |
| 48 | the wonderful souls who worked on the project, in order of appearance. | ||
| 49 | </p> | ||
| 50 | <ul> | 49 | <ul> |
| 51 | <li><a href="https://www.youtube.com/channel/UClywJ-scBa8hcCywQ_7gWbg">Kappacap</a> - thumbnail artist</li> | 50 | <li> |
| 52 | <li><a href="https://www.tumblr.com/atamai-m8">atamai</a> - animation <span style="color:#B4A7AD">(0:03-1:02)</></li> | 51 | <a href="https://www.youtube.com/channel/UClywJ-scBa8hcCywQ_7gWbg"> |
| 53 | <li><a href="https://www.youtube.com/channel/UCNJ6EbYQcnqpB6cQLGYXQow">moxley</a> - music and narration <span style="color:#B4A7AD">(0:03-3:10)</>, animation <span style="color:#B4A7AD">(3:51-4:03)</></li> | 52 | Kappacap |
| 54 | <li><a href="https://www.youtube.com/channel/UCdy0IJXbdKyTWyKajyEvodw">TheJustinator</a> - animation <span style="color:#B4A7AD">(1:03-1:33, 2:02-2:33)</>, music <span style="color:#B4A7AD">(3:11-6:04)</></li> | 53 | </a> |
| 55 | <li><a href="https://chaosyumi.net">chaosyumi</a> - animation <span style="color:#B4A7AD">(1:34-2:01, 5:34-6:05)</></li> | 54 | - thumbnail artist |
| 56 | <li><a href="https://www.youtube.com/channel/UC7NHQWu3awWcC0rm8nB5CfQ">LunaPogi</a> - animation <span style="color:#B4A7AD">(3:36-3:51, 8:01-9:10)</>, music <span style="color:#B4A7AD">(6:05-9:10)</>, narration <span style="color:#B4A7AD">(8:01-9:10)</></li> | 55 | </li> |
| 57 | <li><a href="https://www.youtube.com/channel/UChec240BfC1UKX9DvBBIyLQ">zeccet (bgongo)</a> - animation <span style="color:#B4A7AD">(2:23-3:10)</></li> | 56 | <li> |
| 58 | <li><a href="https://www.youtube.com/channel/UCLfv-6_J0WLsA2jAsJxiLOg">TheRealTinyWorld</a> - animation <span style="color:#B4A7AD">(3:11-3:35)</></li> | 57 | <a href="https://www.tumblr.com/atamai-m8">atamai</a> - animation |
| 59 | <li><a href="https://www.castingcall.club/fredfrenchiii">Fred French VA</a> - narration <span style="color:#B4A7AD">(3:11-4:35)</></li> | 58 | <span style="color:#B4A7AD">(0:03-1:02)</span> |
| 60 | <li><a href="https://www.youtube.com/channel/UCqeAGMquQNtrSfd-quZX3KQ">rqfirqfo</a> - animation <span style="color:#B4A7AD">(4:04-4:35)</></li> | 59 | </li> |
| 61 | <li><a href="https://www.youtube.com/@krispykarim">krispykarim</a> - animation <span style="color:#B4A7AD">(4:36 - 5:04)</>, narration <span style="color:#B4A7AD">(4:36-8:01)</></li> | 60 | <li> |
| 62 | <li><a href="https://www.youtube.com/channel/UCkhbgaqiIkliOKyEORnkhDg">SimplyDoodled Studios</a> - animation <span style="color:#B4A7AD">(5:05-5:33)</></li> | 61 | <a href="https://www.youtube.com/channel/UCNJ6EbYQcnqpB6cQLGYXQow"> |
| 63 | <li><span style="color:#ff7de9">paper clover (me)</span> - animation <span style="color:#B4A7AD">(6:05-6:38)</>, credits animation <span style="color:#B4A7AD">(9:11-9:35)</></li> | 62 | moxley |
| 64 | <li><a href="https://www.youtube.com/channel/UCftnPCmG6dfQ50YOMRMtkjg">Coffee</a> - transcription <span style="color:#B4A7AD">(6:05-9:10)</></li> | 63 | </a> |
| 65 | <li><a href="https://www.youtube.com/channel/UCPQv3ueB-E771gnwcgALzxw">harjjw</a> - animation <span style="color:#B4A7AD">(6:38-7:03)</></li> | 64 | - music and narration <span style="color:#B4A7AD">(0:03-3:10)</span>, animation |
| 66 | <li><a href="https://www.youtube.com/channel/UCcR5G761gkEKvWskjEqWFkQ">yellowmarkers</a> - animation <span style="color:#B4A7AD">(7:04-7:36)</></li> | 65 | <span style="color:#B4A7AD">(3:51-4:03)</span> |
| 67 | <li><a href="https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg">TheNothingGuy07</a> - animation <span style="color:#B4A7AD">(7:37-7:59)</></li> | 66 | </li> |
| 67 | <li> | ||
| 68 | <a href="https://www.youtube.com/channel/UCdy0IJXbdKyTWyKajyEvodw"> | ||
| 69 | TheJustinator | ||
| 70 | </a> | ||
| 71 | - animation <span style="color:#B4A7AD">(1:03-1:33, 2:02-2:33)</span>, music | ||
| 72 | <span style="color:#B4A7AD">(3:11-6:04)</span> | ||
| 73 | </li> | ||
| 74 | <li> | ||
| 75 | <a href="https://chaosyumi.net">chaosyumi</a> - animation | ||
| 76 | <span style="color:#B4A7AD">(1:34-2:01, 5:34-6:05)</span> | ||
| 77 | </li> | ||
| 78 | <li> | ||
| 79 | <a href="https://www.youtube.com/channel/UC7NHQWu3awWcC0rm8nB5CfQ"> | ||
| 80 | LunaPogi | ||
| 81 | </a> | ||
| 82 | - animation <span style="color:#B4A7AD">(3:36-3:51, 8:01-9:10)</span>, music | ||
| 83 | <span style="color:#B4A7AD">(6:05-9:10)</span>, narration | ||
| 84 | <span style="color:#B4A7AD">(8:01-9:10)</span> | ||
| 85 | </li> | ||
| 86 | <li> | ||
| 87 | <a href="https://www.youtube.com/channel/UChec240BfC1UKX9DvBBIyLQ"> | ||
| 88 | zeccet (bgongo) | ||
| 89 | </a> | ||
| 90 | - animation <span style="color:#B4A7AD">(2:23-3:10)</span> | ||
| 91 | </li> | ||
| 92 | <li> | ||
| 93 | <a href="https://www.youtube.com/channel/UCLfv-6_J0WLsA2jAsJxiLOg"> | ||
| 94 | TheRealTinyWorld | ||
| 95 | </a> | ||
| 96 | - animation <span style="color:#B4A7AD">(3:11-3:35)</span> | ||
| 97 | </li> | ||
| 98 | <li> | ||
| 99 | <a href="https://www.castingcall.club/fredfrenchiii">Fred French VA</a> - | ||
| 100 | narration <span style="color:#B4A7AD">(3:11-4:35)</span> | ||
| 101 | </li> | ||
| 102 | <li> | ||
| 103 | <a href="https://www.youtube.com/channel/UCqeAGMquQNtrSfd-quZX3KQ"> | ||
| 104 | rqfirqfo | ||
| 105 | </a> | ||
| 106 | - animation <span style="color:#B4A7AD">(4:04-4:35)</span> | ||
| 107 | </li> | ||
| 108 | <li> | ||
| 109 | <a href="https://www.youtube.com/@krispykarim">krispykarim</a> - animation | ||
| 110 | <span style="color:#B4A7AD">(4:36 - 5:04)</span>, narration <span style="color:#B4A7AD">(4:36-8:01)</span> | ||
| 111 | </li> | ||
| 112 | <li> | ||
| 113 | <a href="https://www.youtube.com/channel/UCkhbgaqiIkliOKyEORnkhDg"> | ||
| 114 | SimplyDoodled Studios | ||
| 115 | </a> | ||
| 116 | - animation <span style="color:#B4A7AD">(5:05-5:33)</span> | ||
| 117 | </li> | ||
| 118 | <li> | ||
| 119 | <span style="color:#ff7de9">paper clover (me)</span> - animation | ||
| 120 | <span style="color:#B4A7AD">(6:05-6:38)</span>, credits animation <span style="color:#B4A7AD">(9:11-9:35)</span> | ||
| 121 | </li> | ||
| 122 | <li> | ||
| 123 | <a href="https://www.youtube.com/channel/UCftnPCmG6dfQ50YOMRMtkjg"> | ||
| 124 | Coffee | ||
| 125 | </a> | ||
| 126 | - transcription <span style="color:#B4A7AD">(6:05-9:10)</span> | ||
| 127 | </li> | ||
| 128 | <li> | ||
| 129 | <a href="https://www.youtube.com/channel/UCPQv3ueB-E771gnwcgALzxw"> | ||
| 130 | harjjw | ||
| 131 | </a> | ||
| 132 | - animation <span style="color:#B4A7AD">(6:38-7:03)</span> | ||
| 133 | </li> | ||
| 134 | <li> | ||
| 135 | <a href="https://www.youtube.com/channel/UCcR5G761gkEKvWskjEqWFkQ"> | ||
| 136 | yellowmarkers | ||
| 137 | </a> | ||
| 138 | - animation <span style="color:#B4A7AD">(7:04-7:36)</span> | ||
| 139 | </li> | ||
| 140 | <li> | ||
| 141 | <a href="https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg"> | ||
| 142 | TheNothingGuy07 | ||
| 143 | </a> | ||
| 144 | - animation <span style="color:#B4A7AD">(7:37-7:59)</span> | ||
| 145 | </li> | ||
| 68 | </ul> | 146 | </ul> |
| 69 | 147 | ||
| 70 | <p> | 148 | <p> |
| 71 | the music during the credits is an instrumental by bill wurtz: <a href="https://billwurtz.com/hey-mom-i-beat-the-scale.mp3">hey mom i beat the scale!</a> | 149 | the music during the credits is an instrumental by bill wurtz: |
| 150 | <a href="https://billwurtz.com/hey-mom-i-beat-the-scale.mp3"> | ||
| 151 | hey mom i beat the scale! | ||
| 152 | </a> | ||
| 72 | </p> | 153 | </p> |
| 73 | 154 | ||
| 74 | <h2>what's next</h2> | 155 | <h2>what's next</h2> |
| 75 | <p> | 156 | <p> |
| 76 | from the success of this project, a much more ambitous project is being | 157 | from the success of this project, a much more ambitous project is being |
| 77 | planned: the reanimation for 'history of the entire world, i guess'. i am joining the project as one of the co-hosts. we are currently doing initial planning, | 158 | planned: the reanimation for 'history of the entire world, i guess'. i am |
| 78 | all artists (music, visual, narration) interested should fill out the <a href="https://paperclover.net/hotewig-reanimated">interest form</a>. by submitting the form, i'll be able to reach out when it's actually time to start cooking. you can also join the project's <a href="https://discord.gg/37axTSuGXd">public discord</a>. | 159 | joining the project as one of the co-hosts. we are currently doing initial |
| 160 | planning, all artists (music, visual, narration) interested should fill out | ||
| 161 | the <a href="https://paperclover.net/hotewig-reanimated">interest form</a>. by | ||
| 162 | submitting the form, i'll be able to reach out when it's actually time to start | ||
| 163 | cooking. you can also join the project's <a href="https://discord.gg/37axTSuGXd"> | ||
| 164 | public discord | ||
| 165 | </a>. | ||
| 79 | </p> | 166 | </p> |
| 80 | 167 | ||
| 81 | <h2>behind the scenes</h2> | 168 | <h2>behind the scenes</h2> |
| 82 | <p> | 169 | <p> |
| 83 | here is a 55 minute dive into my process, as well as nearly every little trick i used on this project. additionally, for one of the first times from me, a free download for <a href="/file/2026/history%20of%20japan%20reanimated/project%20files/history-of-japan-reanimated.zip?dl">all of the project files</a>. | 170 | here is a 55 minute dive into my process, as well as nearly every little |
| 171 | trick i used on this project. additionally, for one of the first times from | ||
| 172 | me, a free download for | ||
| 173 | <a href="/file/2026/history%20of%20japan%20reanimated/project%20files/history-of-japan-reanimated.zip?dl"> | ||
| 174 | all of the project files | ||
| 175 | </a>. | ||
| 84 | </p> | 176 | </p> |
| 85 | <clover-video | 177 | <clover-video file="/2026/history of japan reanimated/behind-the-scenes.mp4"> |
| 86 | file="/2026/history of japan reanimated/behind-the-scenes.mp4" | 178 | <@header>behind the scenes session with clover</@header> |
| 87 | > | 179 | </clover-video> |
| 88 | <@header>behind the scenes session with clover</> | ||
| 89 | </> | ||
| 90 | |||
| 91 | 180 | ||
| 92 | <h2>more links</h2> | 181 | <h2>more links</h2> |
| 93 | <p> | 182 | <p> |
| 94 | <a href="/q+a">paper clover q&a page</a><br/> | 183 | <a href="/q+a">paper clover q&a page</a><br> |
| 95 | <a href="/subscribe">paper clover's mailing list</a> | 184 | <a href="/subscribe">paper clover's mailing list</a> |
| 96 | </p> | 185 | </p> |
| 97 | 186 | ||
| 98 | <h2>mentions on the q&a</h2> | 187 | <h2>mentions on the q&a</h2> |
| 99 | <questions-embed="history-of-japan-reanimated" /> | 188 | <questions-embed="history-of-japan-reanimated"/> |
| 100 | </main> | 189 | </main> |
src/pages/in-the-summer.marko+44-32| ... | @@ -1,43 +1,55 @@ | ... | @@ -1,43 +1,55 @@ |
| 1 | export const meta = { title: 'in the summer' }; | 1 | export const meta = { title: "in the summer" }; |
| 2 | export const theme = { | 2 | export const theme = { |
| 3 | bg: '#7DCC8C', | 3 | bg: "#7DCC8C", |
| 4 | fg: '#000', | 4 | fg: "#000", |
| 5 | primary: '#9b3706', | 5 | primary: "#9b3706", |
| 6 | } | 6 | }; |
| 7 | 7 | ||
| 8 | import "./in-the-summer.css"; | 8 | import "./in-the-summer.css"; |
| 9 | 9 | ||
| 10 | <main style={ 'max-width': '110ch' }> | 10 | <main style={ "max-width": "110ch" }> |
| 11 | <nav><a href="/">back to home</a></nav> | 11 | <nav><a href="/">back to home</a></nav> |
| 12 | <div.contain> | 12 | <div.contain> |
| 13 | <header> | 13 | <header> |
| 14 | <h1>in the summer</h1> | 14 | <h1>in the summer</h1> |
| 15 | <time style="margin-left:.5rem;opacity:.5">(2025-08-01)</time> | 15 | <time style="margin-left:.5rem;opacity:.5">(2025-08-01)</time> |
| 16 | </header> | 16 | </header> |
| 17 | 17 | ||
| 18 | <p> | 18 | <p> |
| 19 | this is a song about the summer, and related feelings. written in 2022, | 19 | this is a song about the summer, and related feelings. written in 2022, |
| 20 | produced in 2025. i had a lot of fun playing with a much larger instrument | 20 | produced in 2025. i had a lot of fun playing with a much larger instrument |
| 21 | selection, and new 3d workflows. enjoy! | 21 | selection, and new 3d workflows. enjoy! |
| 22 | </p> | 22 | </p> |
| 23 | </div> | 23 | </div> |
| 24 | 24 | ||
| 25 | <clover-video | 25 | <clover-video |
| 26 | file="/2025/in the summer/in the summer.mp4" | 26 | file="/2025/in the summer/in the summer.mp4" |
| 27 | poster="/2025/in the summer/thumbnail.jpeg" | 27 | poster="/2025/in the summer/thumbnail.jpeg" |
| 28 | > | 28 | > |
| 29 | <@header><strong>music video</>: in the summer</> | 29 | <@header><strong>music video</strong>: in the summer</@header> |
| 30 | </> | 30 | </clover-video> |
| 31 | 31 | ||
| 32 | <div.contain> | 32 | <div.contain> |
| 33 | <h2>downloads</h2> | 33 | <h2>downloads</h2> |
| 34 | <ul> | 34 | <ul> |
| 35 | <li><a href="/file/2025/in%20the%20summer/in%20the%20summer.mp4?view=dl">in the summer.mp4</a> (AV1, requires modern player)</li> | 35 | <li> |
| 36 | <li><a href="/file/2025/in%20the%20summer/in%20the%20summer.mp3?view=dl">in the summer.mp3</a> (Music)</li> | 36 | <a href="/file/2025/in%20the%20summer/in%20the%20summer.mp4?view=dl"> |
| 37 | <li><a href="/file/2025/in%20the%20summer/fragments">fragments/</a> (behind the scenes)</li> | 37 | in the summer.mp4 |
| 38 | </ul> | 38 | </a> |
| 39 | <h2>mentions on the q&a</h2> | 39 | (AV1, requires modern player) |
| 40 | <questions-embed="in-the-summer" /> | 40 | </li> |
| 41 | </div> | 41 | <li> |
| 42 | <a href="/file/2025/in%20the%20summer/in%20the%20summer.mp3?view=dl"> | ||
| 43 | in the summer.mp3 | ||
| 44 | </a> | ||
| 45 | (Music) | ||
| 46 | </li> | ||
| 47 | <li> | ||
| 48 | <a href="/file/2025/in%20the%20summer/fragments">fragments/</a> (behind | ||
| 49 | the scenes) | ||
| 50 | </li> | ||
| 51 | </ul> | ||
| 52 | <h2>mentions on the q&a</h2> | ||
| 53 | <questions-embed="in-the-summer"/> | ||
| 54 | </div> | ||
| 42 | </main> | 55 | </main> |
| 43 |
src/pages/index.marko+19-8| ... | @@ -4,8 +4,11 @@ import "./index.css"; | ... | @@ -4,8 +4,11 @@ import "./index.css"; |
| 4 | export const theme = { bg: "#fff", fg: "#666" }; | 4 | export const theme = { bg: "#fff", fg: "#666" }; |
| 5 | export const meta = { | 5 | export const meta = { |
| 6 | title: "paper clover", | 6 | title: "paper clover", |
| 7 | description: "and then we knew, just like paper airplanes: that we could fly...", | 7 | description: |
| 8 | embed: { /* Default embed */ }, | 8 | "and then we knew, just like paper airplanes: that we could fly...", |
| 9 | embed: { | ||
| 10 | /* Default embed */ | ||
| 11 | }, | ||
| 9 | alternates: { | 12 | alternates: { |
| 10 | "application/rss+xml": "rss.xml", | 13 | "application/rss+xml": "rss.xml", |
| 11 | }, | 14 | }, |
| ... | @@ -15,15 +18,23 @@ export const meta = { | ... | @@ -15,15 +18,23 @@ export const meta = { |
| 15 | <h2>about</h2> | 18 | <h2>about</h2> |
| 16 | <p> | 19 | <p> |
| 17 | i am a girl that is adorable and | 20 | i am a girl that is adorable and |
| 18 | <a class='custom cite' href="https://en.wikipedia.org/wiki/Life">alive</a>. | 21 | <a class="custom cite" href="https://en.wikipedia.org/wiki/Life">alive</a>. |
| 19 | </p> | ||
| 20 | <p> | ||
| 21 | this site contains my art. | ||
| 22 | </p> | 22 | </p> |
| 23 | <p>this site contains my art.</p> | ||
| 23 | <h2>posts</h2> | 24 | <h2>posts</h2> |
| 24 | <ul class="section-list"> | 25 | <ul class="section-list"> |
| 25 | <li>video collab: <a href="/history-of-japan-reanimated">history of japan reanimated</a> (2026-02-02)</li> | 26 | <li> |
| 26 | <li>blog: <a href="/blog/webdev/one-year-next-app-router">One Year with Next.js App Router — why we're moving on</a> (2025-10-21)</li> | 27 | video collab: |
| 28 | <a href="/history-of-japan-reanimated">history of japan reanimated</a> | ||
| 29 | (2026-02-02) | ||
| 30 | </li> | ||
| 31 | <li> | ||
| 32 | blog: | ||
| 33 | <a href="/blog/webdev/one-year-next-app-router"> | ||
| 34 | One Year with Next.js App Router — why we're moving on | ||
| 35 | </a> | ||
| 36 | (2025-10-21) | ||
| 37 | </li> | ||
| 27 | <li>song: <a href="/in-the-summer">in the summer</a> (2025-08-08)</li> | 38 | <li>song: <a href="/in-the-summer">in the summer</a> (2025-08-08)</li> |
| 28 | <li>song: <a href="/waterfalls">waterfalls</a> (2025-01-01)</li> | 39 | <li>song: <a href="/waterfalls">waterfalls</a> (2025-01-01)</li> |
| 29 | </ul> | 40 | </ul> |
src/pages/resume.marko+39-40| ... | @@ -3,48 +3,47 @@ import "./resume.css"; | ... | @@ -3,48 +3,47 @@ import "./resume.css"; |
| 3 | export const meta = { title: "clover's resume" }; | 3 | export const meta = { title: "clover's resume" }; |
| 4 | 4 | ||
| 5 | <main> | 5 | <main> |
| 6 | <h1>clover's resume</h1> | 6 | <h1>clover's resume</h1> |
| 7 | <div>last updated: 2025</> | 7 | <div>last updated: 2025</div> |
| 8 | 8 | ||
| 9 | <article.job> | 9 | <article.job> |
| 10 | <header> | 10 | <header> |
| 11 | <h2>web/backend engineer</h2> | 11 | <h2>web/backend engineer</h2> |
| 12 | <em>2025-now</em> | 12 | <em>2025-now</em> |
| 13 | </> | 13 | </header> |
| 14 | <ul> | 14 | <ul><i>(more details added as time goes on...)</i></ul> |
| 15 | <i>(more details added as time goes on...)</i> | 15 | </article> |
| 16 | </ul> | ||
| 17 | </> | ||
| 18 | 16 | ||
| 19 | <article.job> | 17 | <article.job> |
| 20 | <header> | 18 | <header> |
| 21 | <h2>runtime/systems engineer</h2> | 19 | <h2>runtime/systems engineer</h2> |
| 22 | <em>2023-2025</em> | 20 | <em>2023-2025</em> |
| 23 | <p>developer tools company</p> | 21 | <p>developer tools company</p> |
| 24 | </> | 22 | </header> |
| 25 | <ul> | 23 | <ul> |
| 26 | <li>hardcore engineering, elegant solutions</> | 24 | <li>hardcore engineering, elegant solutions</li> |
| 27 | <li>platform compatibility & stability</> | 25 | <li>platform compatibility & stability</li> |
| 28 | <li>debugging and profiling across platforms</> | 26 | <li>debugging and profiling across platforms</li> |
| 29 | </ul> | 27 | </ul> |
| 30 | </> | 28 | </article> |
| 31 | 29 | ||
| 32 | <article.job> | 30 | <article.job> |
| 33 | <header> | 31 | <header> |
| 34 | <h2>technician</h2> | 32 | <h2>technician</h2> |
| 35 | <em>2023; part time</em> | 33 | <em>2023; part time</em> |
| 36 | <p>automotive maintainance company</p> | 34 | <p>automotive maintainance company</p> |
| 37 | </> | 35 | </header> |
| 38 | <ul> | 36 | <ul> |
| 39 | <li>pressed buttons on a computer</> | 37 | <li>pressed buttons on a computer</li> |
| 40 | </ul> | 38 | </ul> |
| 41 | </> | 39 | </article> |
| 42 | 40 | ||
| 43 | <footer> | 41 | <footer> |
| 44 | <h2>eduation</h2> <em>2004-now</em> | 42 | <h2>eduation</h2> |
| 45 | <p> | 43 | ${" "}<em>2004-now</em> |
| 46 | my life on earth has taught me more than i expected. i <br/> | 44 | <p> |
| 47 | continue to learn new things daily, as if it was magic. | 45 | my life on earth has taught me more than i expected. i <br> continue to |
| 48 | </p> | 46 | learn new things daily, as if it was magic. |
| 49 | </footer> | 47 | </p> |
| 48 | </footer> | ||
| 50 | </main> | 49 | </main> |
src/pages/subscribe.marko+22-24| ... | @@ -1,31 +1,29 @@ | ... | @@ -1,31 +1,29 @@ |
| 1 | import './subscribe.css'; | 1 | import "./subscribe.css"; |
| 2 | export const meta = { | 2 | export const meta = { |
| 3 | title: "paper clover mailing list", | 3 | title: "paper clover mailing list", |
| 4 | description: "subscribe to the mailing list", | 4 | description: "subscribe to the mailing list", |
| 5 | }; | 5 | }; |
| 6 | 6 | ||
| 7 | <main> | 7 | <main> |
| 8 | <h1>mailing list</h1> | 8 | <h1>mailing list</h1> |
| 9 | <canvas style="display: none;"></canvas> | 9 | <canvas style="display: none;"/> |
| 10 | <p> | 10 | <p> |
| 11 | the mailing list is used for big project updates. this is about once every | 11 | the mailing list is used for big project updates. this is about once every |
| 12 | couple of months. to get added, please email anything with the word | 12 | couple of months. to get added, please email anything with the word |
| 13 | "subscribe" in the subject or body to the following address: | 13 | "subscribe" in the subject or body to the following address: |
| 14 | </p> | 14 | </p> |
| 15 | <code id="subscribe" class="flex"> | 15 | <code id="subscribe" class="flex"> |
| 16 | <a href="mailto:subscribe@paperclover.net?subject=paper%20clover%20mailing%20list&body=I%20would%20like%20to%20be%20subscribed%20to%20the%20following%20mailing%20lists%3A%0A%0A-%20Technical%20Blog%20Posts%20-%20YES%0A-%20Art%20(Original%20Music%2FVideo)%20-%20YES%0A%0A(feel%20free%20to%20write%20whatever%20else%20you%20want)">subscribe@paperclover.net</a> | 16 | <a href="mailto:subscribe@paperclover.net?subject=paper%20clover%20mailing%20list&body=I%20would%20like%20to%20be%20subscribed%20to%20the%20following%20mailing%20lists%3A%0A%0A-%20Technical%20Blog%20Posts%20-%20YES%0A-%20Art%20(Original%20Music%2FVideo)%20-%20YES%0A%0A(feel%20free%20to%20write%20whatever%20else%20you%20want)"> |
| 17 | </code> | 17 | subscribe@paperclover.net |
| 18 | <p> | 18 | </a> |
| 19 | i read every message, so you can say whatever else you want and i will see | 19 | </code> |
| 20 | it. there won't be a confirmation email for subscribing. for those who are | 20 | <p> |
| 21 | close friends with me, add your name and i'll add you to the 'friends' list, | 21 | i read every message, so you can say whatever else you want and i will see |
| 22 | where i send life updates in addition to new content. | 22 | it. there won't be a confirmation email for subscribing. for those who are |
| 23 | </p> | 23 | close friends with me, add your name and i'll add you to the 'friends' list, |
| 24 | <p>to unsubscribe, send an email asking to unsubscribe.</p> | 24 | where i send life updates in addition to new content. |
| 25 | <br> | 25 | </p> |
| 26 | <br> | 26 | <p>to unsubscribe, send an email asking to unsubscribe.</p> |
| 27 | <br> | 27 | <br><br><br> |
| 28 | <p> | 28 | <p><a href="/">back to home</a></p> |
| 29 | <a href="/">back to home</a> | ||
| 30 | </p> | ||
| 31 | </main> | 29 | </main> |
src/pages/waterfalls.marko+10-18| ... | @@ -7,7 +7,7 @@ export const theme = { | ... | @@ -7,7 +7,7 @@ export const theme = { |
| 7 | }; | 7 | }; |
| 8 | 8 | ||
| 9 | <main class="waterfalls"> | 9 | <main class="waterfalls"> |
| 10 | <div class="waterfalls_bg1" /> | 10 | <div class="waterfalls_bg1"/> |
| 11 | <main class="waterfalls"> | 11 | <main class="waterfalls"> |
| 12 | <div class="contain"> | 12 | <div class="contain"> |
| 13 | <nav><a href="/">back to home</a></nav> | 13 | <nav><a href="/">back to home</a></nav> |
| ... | @@ -22,30 +22,22 @@ export const theme = { | ... | @@ -22,30 +22,22 @@ export const theme = { |
| 22 | file="/2025/waterfalls/waterfalls.mp4" | 22 | file="/2025/waterfalls/waterfalls.mp4" |
| 23 | poster="/2025/waterfalls/thumbnail.jpeg" | 23 | poster="/2025/waterfalls/thumbnail.jpeg" |
| 24 | > | 24 | > |
| 25 | <@header><strong>music video</strong>: waterfalls</> | 25 | <@header><strong>music video</strong>: waterfalls</@header> |
| 26 | </> | 26 | </clover-video> |
| 27 | <div class="contain"> | 27 | <div class="contain"> |
| 28 | <p> | 28 | <p>it's nice to be <em>free</em>.</p> |
| 29 | it's nice to be <em>free</em>. | ||
| 30 | </p> | ||
| 31 | <p>like paper airplanes in the wind.</p> | 29 | <p>like paper airplanes in the wind.</p> |
| 32 | <p>the stars shine in the sky. it's beautiful.</p> | 30 | <p>the stars shine in the sky. it's beautiful.</p> |
| 31 | <p>love <br> –clover caruso</p> | ||
| 32 | <br> | ||
| 33 | <p> | 33 | <p> |
| 34 | love | 34 | <strong>more</strong>: <br><a href="/file/2025/waterfalls/fragments"> |
| 35 | <br /> | 35 | fragments |
| 36 | –clover caruso | 36 | </a><br><a href="/q+a">q&a page</a> |
| 37 | </p> | ||
| 38 | <br /> | ||
| 39 | |||
| 40 | <p> | ||
| 41 | <strong>more</strong>: <br /> | ||
| 42 | <a href="/file/2025/waterfalls/fragments">fragments</a> | ||
| 43 | <br /> | ||
| 44 | <a href="/q+a">q&a page</a> | ||
| 45 | </p> | 37 | </p> |
| 46 | 38 | ||
| 47 | <h2>mentions on the q&a</h2> | 39 | <h2>mentions on the q&a</h2> |
| 48 | <questions-embed="waterfalls" /> | 40 | <questions-embed="waterfalls"/> |
| 49 | </div> | 41 | </div> |
| 50 | </main> | 42 | </main> |
| 51 | </main> | 43 | </main> |
src/q+a/pages/admin.q+a.marko+5-6| ... | @@ -7,20 +7,19 @@ export const meta: Metadata = { | ... | @@ -7,20 +7,19 @@ export const meta: Metadata = { |
| 7 | description: "ask clover a question", | 7 | description: "ask clover a question", |
| 8 | }; | 8 | }; |
| 9 | export const regenerate = { | 9 | export const regenerate = { |
| 10 | tags: ["q+a", "q+a inbox"] | 10 | tags: ["q+a", "q+a inbox"], |
| 11 | }; | 11 | }; |
| 12 | 12 | ||
| 13 | <const/inboxSize = PendingQuestion.getAll().length /> | 13 | <const/inboxSize=PendingQuestion.getAll().length> |
| 14 | <if=(inboxSize > 0)> | 14 | <if=(inboxSize > 0)> |
| 15 | <p> | 15 | <p> |
| 16 | <strong style={ color: "#ff4b4b" }> | 16 | <strong style={ color: "#ff4b4b" }> |
| 17 | notice: ${inboxSize} items in | 17 | notice: ${inboxSize} items in <a href="/admin/q+a/inbox">inbox</a>! |
| 18 | <a href="/admin/q+a/inbox">inbox</a>! | ||
| 19 | </strong> | 18 | </strong> |
| 20 | </p> | 19 | </p> |
| 21 | </> | 20 | </if> |
| 22 | 21 | ||
| 23 | <MainPage admin /> | 22 | <MainPage admin/> |
| 24 | 23 | ||
| 25 | import MainPage from "./q+a.marko"; | 24 | import MainPage from "./q+a.marko"; |
| 26 | import { PendingQuestion } from "#src/q+a/models/PendingQuestion.ts"; | 25 | import { PendingQuestion } from "#src/q+a/models/PendingQuestion.ts"; |
src/q+a/pages/q+a.404.marko+3-5| ... | @@ -4,9 +4,7 @@ export const meta: Metadata = { | ... | @@ -4,9 +4,7 @@ export const meta: Metadata = { |
| 4 | }; | 4 | }; |
| 5 | 5 | ||
| 6 | <question-layout> | 6 | <question-layout> |
| 7 | <h2>sound the alarms</h2> | 7 | <h2>sound the alarms</h2> |
| 8 | <p>this page doesn't exist</p> | 8 | <p>this page doesn't exist</p> |
| 9 | <p> | 9 | <p><a href="/q+a">return to the questions list</a></p> |
| 10 | <a href="/q+a">return to the questions list</a> | ||
| 11 | </p> | ||
| 12 | </question-layout> | 10 | </question-layout> |
src/q+a/pages/q+a.marko+9-11| ... | @@ -9,19 +9,17 @@ export const meta: Metadata = { | ... | @@ -9,19 +9,17 @@ export const meta: Metadata = { |
| 9 | description: "ask clover a question", | 9 | description: "ask clover a question", |
| 10 | }; | 10 | }; |
| 11 | 11 | ||
| 12 | <const/{ admin = false } = input /> | 12 | <const/{ admin = false }=input> |
| 13 | <const/questions = [...Question.getAll()] /> | 13 | <const/questions=[...Question.getAll()]> |
| 14 | 14 | ||
| 15 | <question-layout> | 15 | <question-layout> |
| 16 | <if=!admin> | 16 | <if=!admin> |
| 17 | <question-form /> | 17 | <question-form/> |
| 18 | </> | 18 | </if> |
| 19 | <for|question| of=questions> | 19 | <for|question| of=questions> |
| 20 | <question ...{ question, admin } /> | 20 | <question ...{ question, admin }/> |
| 21 | </> | 21 | </for> |
| 22 | <footer> | 22 | <footer>fun fact: clover has answered ${questions.length} questions</footer> |
| 23 | fun fact: clover has answered ${questions.length} questions | ||
| 24 | </footer> | ||
| 25 | </question-layout> | 23 | </question-layout> |
| 26 | 24 | ||
| 27 | import { Question } from "#src/q+a/models/Question.ts"; | 25 | import { Question } from "#src/q+a/models/Question.ts"; |
src/q+a/tags/question-form.marko-1| ... | @@ -10,4 +10,3 @@ | ... | @@ -10,4 +10,3 @@ |
| 10 | <button type="submit">send</button> | 10 | <button type="submit">send</button> |
| 11 | <div class="disabled-button">send</div> | 11 | <div class="disabled-button">send</div> |
| 12 | </form> | 12 | </form> |
| 13 |
src/q+a/tags/question-layout.marko+2-2| ... | @@ -8,8 +8,8 @@ client import "./header.client.ts"; | ... | @@ -8,8 +8,8 @@ client import "./header.client.ts"; |
| 8 | <span class="n" style="--s:#999;margin:0 5px">&</span> | 8 | <span class="n" style="--s:#999;margin:0 5px">&</span> |
| 9 | <span class="q" style="color:var(--b);--s:#994253">questions</span> | 9 | <span class="q" style="color:var(--b);--s:#994253">questions</span> |
| 10 | </h1> | 10 | </h1> |
| 11 | </> | 11 | </if> |
| 12 | <${input.content} /> | 12 | <${input.content}/> |
| 13 | </main> | 13 | </main> |
| 14 | 14 | ||
| 15 | export const theme = { | 15 | export const theme = { |
src/q+a/tags/question.marko+15-18| ... | @@ -10,34 +10,31 @@ export interface Input { | ... | @@ -10,34 +10,31 @@ export interface Input { |
| 10 | // 2024-12-31 05:00:00 EST | 10 | // 2024-12-31 05:00:00 EST |
| 11 | export const transitionDate = 1735639200000; | 11 | export const transitionDate = 1735639200000; |
| 12 | 12 | ||
| 13 | <const/{ question, admin = false, editor = false } = input /> | 13 | <const/{ question, admin = false, editor = false }=input> |
| 14 | <const/{ id, date } = question/> | 14 | <const/{ id, date }=question> |
| 15 | 15 | ||
| 16 | <${"e-"} | 16 | <${"e-"} |
| 17 | f=(date > transitionDate ? true : null) | 17 | f=(date > transitionDate ? true : null) |
| 18 | id=admin ? `q${id}` : undefined | 18 | id=admin ? `q${id}` : undefined |
| 19 | > | 19 | > |
| 20 | <if=admin> | 20 | <if=admin> |
| 21 | <a | 21 | <a style="margin-right: 0.5rem" href=`/admin/q+a/${id}`>[EDIT]</a> |
| 22 | style="margin-right: 0.5rem" | 22 | </if> |
| 23 | href=`/admin/q+a/${id}` | ||
| 24 | >[EDIT]</a> | ||
| 25 | </> | ||
| 26 | <a> | 23 | <a> |
| 27 | <time | 24 | <time datetime=formatQuestionISOTimestamp(date)> |
| 28 | datetime=formatQuestionISOTimestamp(date) | 25 | ${formatQuestionTimestamp(date)} |
| 29 | >${formatQuestionTimestamp(date)}</time><br /> | 26 | </time> |
| 30 | </a> | 27 | <br> |
| 31 | 28 | </a>$!{markdown.renderMarkdown(question.text)} | |
| 32 | $!{markdown.renderMarkdown(question.text)} | ||
| 33 | </> | 29 | </> |
| 34 | 30 | ||
| 35 | // this singleton script will make all the '<time>' tags clickable. | 31 | // this singleton script will make all the '<time>' tags clickable. |
| 36 | <if=!editor> | 32 | <if=!editor>client import "./clickable-links.client.ts";</if> |
| 37 | client import "./clickable-links.client.ts"; | ||
| 38 | </> | ||
| 39 | 33 | ||
| 40 | import type { Question } from "#src/q+a/models/Question.ts"; | 34 | import type { Question } from "#src/q+a/models/Question.ts"; |
| 41 | import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "#src/q+a/format.ts"; | 35 | import { |
| 36 | formatQuestionTimestamp, | ||
| 37 | formatQuestionISOTimestamp, | ||
| 38 | } from "#src/q+a/format.ts"; | ||
| 42 | import * as markdown from "#src/q+a/markdown.ts"; | 39 | import * as markdown from "#src/q+a/markdown.ts"; |
| 43 | import { addScript as Script } from "#sitegen" | 40 | import { addScript as Script } from "#sitegen"; |
src/q+a/views/backend-inbox.marko+25-29| ... | @@ -4,39 +4,35 @@ import { | ... | @@ -4,39 +4,35 @@ import { |
| 4 | formatQuestionISOTimestamp, | 4 | formatQuestionISOTimestamp, |
| 5 | formatQuestionTimestamp, | 5 | formatQuestionTimestamp, |
| 6 | } from "#src/q+a/format.ts"; | 6 | } from "#src/q+a/format.ts"; |
| 7 | export const meta = { title: 'question answer inbox' }; | 7 | export const meta = { title: "question answer inbox" }; |
| 8 | 8 | ||
| 9 | <const/questions = PendingQuestion.getAll() /> | 9 | <const/questions=PendingQuestion.getAll()> |
| 10 | 10 | ||
| 11 | <question-layout minimal> | 11 | <question-layout minimal> |
| 12 | <h1>inbox</h1> | 12 | <h1>inbox</h1> |
| 13 | <for|q| of=questions> | 13 | <for|q| of=questions> |
| 14 | <div | 14 | <div |
| 15 | data-q=q.id | 15 | data-q=q.id |
| 16 | style="border-bottom: 2px solid #fff7; margin-bottom: 1rem" | 16 | style="border-bottom: 2px solid #fff7; margin-bottom: 1rem" |
| 17 | > | 17 | > |
| 18 | <time datetime=formatQuestionISOTimestamp(q.date)> | 18 | <time datetime=formatQuestionISOTimestamp(q.date)> |
| 19 | ${formatQuestionTimestamp(q.date)} ${q.id} | 19 | ${formatQuestionTimestamp(q.date)} ${q.id} |
| 20 | </time> | 20 | </time> |
| 21 | <div style="color: dodgerblue; margin-bottom: 0.25rem"> | 21 | <div style="color: dodgerblue; margin-bottom: 0.25rem"> |
| 22 | ${q.sourceName} | 22 | ${q.sourceName}${ |
| 23 | ${q.sourceLocation !== "unknown" ? `from ${q.sourceLocation}` : ''} | 23 | q.sourceLocation !== "unknown" ? `from ${q.sourceLocation}` : "" |
| 24 | ${q.sourceVPN ? `(VPN: ${q.sourceVPN})` : ''} | 24 | }${q.sourceVPN ? `(VPN: ${q.sourceVPN})` : ""} |
| 25 | </div> | ||
| 26 | <p style="white-space: pre-wrap">${q.prompt}</p> | ||
| 27 | <p> | ||
| 28 | <button onclick=`onReply("${q.id}")` style="color: lime">reply</button> | ||
| 29 | <button onclick=`onDelete("${q.id}")` style="color: red">reject</button> | ||
| 30 | <button onclick=`onDeleteFull("${q.id}")` style="color: red"> | ||
| 31 | DELETE | ||
| 32 | </button> | ||
| 33 | </p> | ||
| 25 | </div> | 34 | </div> |
| 26 | <p style="white-space: pre-wrap">${q.prompt}</p> | 35 | </for> |
| 27 | <p> | ||
| 28 | <button onclick=`onReply("${q.id}")` style="color: lime"> | ||
| 29 | reply | ||
| 30 | </button> | ||
| 31 | <button onclick=`onDelete("${q.id}")` style="color: red"> | ||
| 32 | reject | ||
| 33 | </button> | ||
| 34 | <button onclick=`onDeleteFull("${q.id}")` style="color: red"> | ||
| 35 | DELETE | ||
| 36 | </button> | ||
| 37 | </p> | ||
| 38 | </div> | ||
| 39 | </> | ||
| 40 | </question-layout> | 36 | </question-layout> |
| 41 | 37 | ||
| 42 | client import "./backend-inbox.client.ts"; | 38 | client import "./backend-inbox.client.ts"; |
src/q+a/views/editor.marko+17-20| ... | @@ -16,22 +16,19 @@ export interface ScriptPayload { | ... | @@ -16,22 +16,19 @@ export interface ScriptPayload { |
| 16 | date: string; | 16 | date: string; |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | <const/{ question } = input /> | 19 | <const/{ question }=input> |
| 20 | 20 | ||
| 21 | <const/payload = { | 21 | <const/payload={ |
| 22 | id: question.id, | 22 | id: question.id, |
| 23 | qmid: question.qmid, | 23 | qmid: question.qmid, |
| 24 | text: question.text, | 24 | text: question.text, |
| 25 | type: question.type, | 25 | type: question.type, |
| 26 | date: question.date.toISOString(), | 26 | date: question.date.toISOString(), |
| 27 | } satisfies ScriptPayload/> | 27 | } satisfies ScriptPayload> |
| 28 | 28 | ||
| 29 | <div#edit-grid.qa> | 29 | <div#edit-grid.qa> |
| 30 | <div#topleft> | 30 | <div#topleft> |
| 31 | <button | 31 | <button onclick="location.href='/admin/q+a'" style={ color: "magenta" }> |
| 32 | onclick="location.href='/admin/q+a'" | ||
| 33 | style={ color: "magenta" } | ||
| 34 | > | ||
| 35 | all | 32 | all |
| 36 | </button> | 33 | </button> |
| 37 | <button | 34 | <button |
| ... | @@ -42,25 +39,25 @@ export interface ScriptPayload { | ... | @@ -42,25 +39,25 @@ export interface ScriptPayload { |
| 42 | </button> | 39 | </button> |
| 43 | </div> | 40 | </div> |
| 44 | <div#topright> | 41 | <div#topright> |
| 45 | <button onclick="onCommitQuestion()" style="color:lime"> | 42 | <button onclick="onCommitQuestion()" style="color:lime">update</button> |
| 46 | update | 43 | <button onclick="onDelete()" style={ color: "red" }>reject</button> |
| 47 | </button> | ||
| 48 | <button onclick="onDelete()" style={ color: "red" }> | ||
| 49 | reject | ||
| 50 | </button> | ||
| 51 | <select id="type" onchange="onTypeChange()" value=question.type> | 44 | <select id="type" onchange="onTypeChange()" value=question.type> |
| 52 | <option value=QuestionType.normal>normal</> | 45 | <option value=QuestionType.normal>normal</option> |
| 53 | <option value=QuestionType.annotation>annotation</> | 46 | <option value=QuestionType.annotation>annotation</option> |
| 54 | <option value=QuestionType.reject>reject</> | 47 | <option value=QuestionType.reject>reject</option> |
| 55 | </select> | 48 | </select> |
| 56 | </div> | 49 | </div> |
| 57 | <textarea#editor /> | 50 | <textarea#editor/> |
| 58 | <main#preview> | 51 | <main#preview> |
| 59 | <question editor ...{ question } /> | 52 | <question editor ...{ question }/> |
| 60 | </main> | 53 | </main> |
| 61 | </div> | 54 | </div> |
| 62 | <html-script>self.payload=${JSON.stringify(payload).replaceAll('</script>', '<\\/script>')}</html-script> | 55 | <html-script> |
| 63 | <html-script src="/js/q+a/scripts/editor.js" type="module" /> | 56 | self.payload = ${ |
| 57 | JSON.stringify(payload).replaceAll("</script>", "<\\/script>") | ||
| 58 | }; | ||
| 59 | </html-script> | ||
| 60 | <html-script src="/js/q+a/scripts/editor.js" type="module"/> | ||
| 64 | 61 | ||
| 65 | import { type PendingQuestion } from "#src/q+a/models/PendingQuestion.ts"; | 62 | import { type PendingQuestion } from "#src/q+a/models/PendingQuestion.ts"; |
| 66 | import { Question, QuestionType } from "#src/q+a/models/Question.ts"; | 63 | import { Question, QuestionType } from "#src/q+a/models/Question.ts"; |
src/q+a/views/fail.marko+11-19| ... | @@ -3,26 +3,18 @@ export const theme = { | ... | @@ -3,26 +3,18 @@ export const theme = { |
| 3 | ...layout.theme, | 3 | ...layout.theme, |
| 4 | primary: "#58ffee", | 4 | primary: "#58ffee", |
| 5 | }; | 5 | }; |
| 6 | export const meta = { title: 'oh no' }; | 6 | export const meta = { title: "oh no" }; |
| 7 | 7 | ||
| 8 | <const/{ error, content }=input/> | 8 | <const/{ error, content }=input> |
| 9 | 9 | ||
| 10 | <question-layout> | 10 | <question-layout> |
| 11 | <h2 style="color: red">:(</h2> | 11 | <h2 style="color: red">:(</h2> |
| 12 | <p style="color: red"> | 12 | <p style="color: red">${error}</p> |
| 13 | ${error} | 13 | <if=content> |
| 14 | </p> | 14 | <br><br> |
| 15 | <if=content> | 15 | <p>here is a copy of what you wrote, if you want to try again:</p> |
| 16 | <br /> | 16 | <pre style="white-space: pre-wrap"><code>${content}</code></pre> |
| 17 | <br /> | 17 | </if> |
| 18 | <p> | 18 | <br><br> |
| 19 | here is a copy of what you wrote, if you want to try again: | 19 | <p><a href="/q+a">return to the questions list</a></p> |
| 20 | </p> | ||
| 21 | <pre style="white-space: pre-wrap"><code>${content}</code></pre> | ||
| 22 | </> | ||
| 23 | <br /> | ||
| 24 | <br /> | ||
| 25 | <p> | ||
| 26 | <a href="/q+a">return to the questions list</a> | ||
| 27 | </p> | ||
| 28 | </question-layout> | 20 | </question-layout> |
src/q+a/views/image-embed.marko+14-6| ... | @@ -4,13 +4,21 @@ export interface Input { | ... | @@ -4,13 +4,21 @@ export interface Input { |
| 4 | } | 4 | } |
| 5 | 5 | ||
| 6 | <html-style> | 6 | <html-style> |
| 7 | main { padding-top: 11px; } | 7 | main { |
| 8 | e- { margin: 0!important } | 8 | padding-top: 11px; |
| 9 | e- > :first-child { margin-top: 0!important } | 9 | } |
| 10 | e- > :last-child { margin-bottom: 0!important } | 10 | e- { |
| 11 | margin: 0 !important; | ||
| 12 | } | ||
| 13 | e- > :first-child { | ||
| 14 | margin-top: 0 !important; | ||
| 15 | } | ||
| 16 | e- > :last-child { | ||
| 17 | margin-bottom: 0 !important; | ||
| 18 | } | ||
| 11 | </html-style> | 19 | </html-style> |
| 12 | <main.qa> | 20 | <main.qa> |
| 13 | <question question=input.question /> | 21 | <question question=input.question/> |
| 14 | </main> | 22 | </main> |
| 15 | 23 | ||
| 16 | import { Question } from '#src/q+a/models/Question.ts'; | 24 | import { Question } from "#src/q+a/models/Question.ts"; |
src/q+a/views/permalink.marko+37-30| ... | @@ -4,54 +4,61 @@ export interface Input { | ... | @@ -4,54 +4,61 @@ export interface Input { |
| 4 | } | 4 | } |
| 5 | 5 | ||
| 6 | server export function meta({ context, question }) { | 6 | server export function meta({ context, question }) { |
| 7 | const isDiscord = context.req.header("user-agent") | 7 | const isDiscord = context.req |
| 8 | .header("user-agent") | ||
| 8 | ?.toLowerCase() | 9 | ?.toLowerCase() |
| 9 | .includes("discord"); | 10 | .includes("discord"); |
| 10 | if (question.type === QuestionType.normal) { | 11 | if (question.type === QuestionType.normal) { |
| 11 | return { | 12 | return { |
| 12 | title: "question permalink", | 13 | title: "question permalink", |
| 13 | openGraph: { | 14 | openGraph: { |
| 14 | title: isDiscord ? undefined : `question at ${formatQuestionTimestamp(question.date)}`, | 15 | title: isDiscord |
| 15 | image: `https://paperclover.net/q+a/${question.id}.png` | 16 | ? undefined |
| 17 | : `question at ${formatQuestionTimestamp(question.date)}`, | ||
| 18 | image: `https://paperclover.net/q+a/${question.id}.png`, | ||
| 16 | }, | 19 | }, |
| 17 | twitter: { card: "summary_large_image" }, | 20 | twitter: { card: "summary_large_image" }, |
| 18 | themeColor: isDiscord | 21 | themeColor: isDiscord |
| 19 | ? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71" | 22 | ? question.date.getTime() > transitionDate |
| 23 | ? "#8c78ff" | ||
| 24 | : "#58ff71" | ||
| 20 | : undefined, | 25 | : undefined, |
| 21 | }; | 26 | }; |
| 22 | } | 27 | } |
| 23 | return { title: 'question permalink' }; | 28 | return { title: "question permalink" }; |
| 24 | } | 29 | } |
| 25 | 30 | ||
| 26 | <const/{ question }=input/> | 31 | <const/{ question }=input> |
| 27 | <const/{ type }=question/> | 32 | <const/{ type }=question> |
| 28 | 33 | ||
| 29 | <question-layout> | 34 | <question-layout> |
| 30 | <if=type==QuestionType.normal> | 35 | <if=type == QuestionType.normal> |
| 31 | <p>this page is a permalink to the following question:</p> | 36 | <p>this page is a permalink to the following question:</p> |
| 32 | <question ...{question} /> | 37 | <question ...{ question }/> |
| 33 | </><else if=type==QuestionType.pending> | 38 | </if> |
| 34 | <p> | 39 | <else if=type == QuestionType.pending> |
| 35 | this page is a permalink to a question that | 40 | <p> |
| 36 | has not yet been answered. | 41 | this page is a permalink to a question that has not yet been answered. |
| 37 | </p> | 42 | </p> |
| 38 | <p><a href="/q+a">read questions with existing responses</a>.</p> | 43 | <p><a href="/q+a">read questions with existing responses</a>.</p> |
| 39 | </><else if=type==QuestionType.reject> | 44 | </else> |
| 40 | <p> | 45 | <else if=type == QuestionType.reject> |
| 41 | this page is a permalink to a question, but the question | 46 | <p> |
| 42 | was deleted instead of answered. maybe it was sent multiple | 47 | this page is a permalink to a question, but the question was deleted |
| 43 | times, or maybe the question was not a question. who knows. | 48 | instead of answered. maybe it was sent multiple times, or maybe the |
| 44 | </p> | 49 | question was not a question. who knows. |
| 45 | <p>sorry, sister</p> | 50 | </p> |
| 46 | <p><a href="/q+a">all questions</a></p> | 51 | <p>sorry, sister</p> |
| 47 | </><else> | 52 | <p><a href="/q+a">all questions</a></p> |
| 48 | <p>oh dear, this question is in an invalid state</p> | 53 | </else> |
| 49 | <pre>${JSON.stringify(question, null, 2)}</pre> | 54 | <else> |
| 50 | </> | 55 | <p>oh dear, this question is in an invalid state</p> |
| 56 | <pre>${JSON.stringify(question, null, 2)}</pre> | ||
| 57 | </else> | ||
| 51 | 58 | ||
| 52 | <a href="/q+a">view more questions</a> | 59 | <a href="/q+a">view more questions</a> |
| 53 | </question-layout> | 60 | </question-layout> |
| 54 | 61 | ||
| 55 | import { Question, QuestionType } from '#src/q+a/models/Question.ts'; | 62 | import { Question, QuestionType } from "#src/q+a/models/Question.ts"; |
| 56 | import { transitionDate } from "#src/q+a/tags/question.marko"; | 63 | import { transitionDate } from "#src/q+a/tags/question.marko"; |
| 57 | import { formatQuestionTimestamp } from "#src/q+a/format.ts"; | 64 | import { formatQuestionTimestamp } from "#src/q+a/format.ts"; |
src/q+a/views/success.marko+13-20| ... | @@ -1,29 +1,22 @@ | ... | @@ -1,29 +1,22 @@ |
| 1 | export interface Input { | 1 | export interface Input { |
| 2 | permalink: string; | 2 | permalink: string; |
| 3 | } | 3 | } |
| 4 | export const meta = { title: 'question submitted!!!' }; | 4 | export const meta = { title: "question submitted!!!" }; |
| 5 | 5 | ||
| 6 | <const/{ permalink }=input/> | 6 | <const/{ permalink }=input> |
| 7 | 7 | ||
| 8 | <question-layout> | 8 | <question-layout> |
| 9 | <h2 style="color: #8c78ff; margin-bottom: 0">thank you</h2> | 9 | <h2 style="color: #8c78ff; margin-bottom: 0">thank you</h2> |
| 10 | <p style="color: #8c78ff"> | 10 | <p style="color: #8c78ff">your question has been submitted.</p> |
| 11 | your question has been submitted. | 11 | <p> |
| 12 | </p> | 12 | please keep in mind it may take multiple days for clover to answer your |
| 13 | <p> | 13 | question, even if other questions are answered before yours. sometimes more |
| 14 | please keep in mind it may take multiple days for clover to answer your | 14 | in-depth questions need her to dig through archives. |
| 15 | question, even if other questions are answered before yours. sometimes more | 15 | </p> |
| 16 | in-depth questions need her to dig through archives. | 16 | <br> |
| 17 | </p> | 17 | <p><strong>permalink</strong>: <a href=permalink>${permalink}</a></p> |
| 18 | <br /> | 18 | <br><br> |
| 19 | <p> | 19 | <p><a href="/q+a">return to the questions list</a></p> |
| 20 | <strong>permalink</strong>: <a href=permalink>${permalink}</a> | ||
| 21 | </p> | ||
| 22 | <br /> | ||
| 23 | <br /> | ||
| 24 | <p> | ||
| 25 | <a href="/q+a">return to the questions list</a> | ||
| 26 | </p> | ||
| 27 | </question-layout> | 20 | </question-layout> |
| 28 | 21 | ||
| 29 | import * as layout from "../tags/question-layout.marko"; | 22 | import * as layout from "../tags/question-layout.marko"; |
src/q+a/views/things-random.marko+2-3| ... | @@ -1,10 +1,9 @@ | ... | @@ -1,10 +1,9 @@ |
| 1 | export const meta = { title: 'random number' }; | 1 | export const meta = { title: "random number" }; |
| 2 | client import "./things-random.client.ts"; | 2 | client import "./things-random.client.ts"; |
| 3 | 3 | ||
| 4 | <const/number = Math.floor(Math.random() * 999999) + 1 /> | 4 | <const/number=Math.floor(Math.random() * 999999) + 1> |
| 5 | 5 | ||
| 6 | <main> | 6 | <main> |
| 7 | <div>${number}</div> | 7 | <div>${number}</div> |
| 8 | <button>another</button> | 8 | <button>another</button> |
| 9 | </main> | 9 | </main> |
| 10 |
src/tags/clover-media.marko+26-19| ... | @@ -5,41 +5,48 @@ export interface Input { | ... | @@ -5,41 +5,48 @@ export interface Input { |
| 5 | align?: string; | 5 | align?: string; |
| 6 | } | 6 | } |
| 7 | 7 | ||
| 8 | <const/{ src, alt = "", displayWidthEstimate, align, ...rest } = input /> | 8 | <const/{ src, alt = "", displayWidthEstimate, align, ...rest }=input> |
| 9 | <const/file = typeof src === 'object' ? src : MediaFile.getByPath(src) /> | 9 | <const/file=typeof src === "object" ? src : MediaFile.getByPath(src)> |
| 10 | 10 | ||
| 11 | <if=!file> | 11 | <if=!file> |
| 12 | <div style={ color: 'red' }>ERROR: No file in database: ${src}</div> | 12 | <div style={ color: "red" }>ERROR: No file in database: ${src}</div> |
| 13 | </><else if=extsVideo.has(file.extension.toLowerCase())> | 13 | </if> |
| 14 | <clover-video noFigure ...{ file } /> | 14 | <else if=extsVideo.has(file.extension.toLowerCase())> |
| 15 | </><else if=file.extension === '.gif'> | 15 | <clover-video noFigure ...{ file }/> |
| 16 | <img src=`/file${escapeUri(file.path)}` ...rest /> | 16 | </else> |
| 17 | </><else> | 17 | <else if=file.extension === ".gif"> |
| 18 | <const/base = `/file${escapeUri(file.path)}` /> | 18 | <img src=`/file${escapeUri(file.path)}` ...rest> |
| 19 | <const/{ width } = UNWRAP(file.parseDimensions()) /> | 19 | </else> |
| 20 | <const/targetSizes = transcodeRules.imageSizes.filter((w) => w < width) /> | 20 | <else> |
| 21 | <const/sizes = | 21 | <const/base=`/file${escapeUri(file.path)}`> |
| 22 | <const/{ width }=UNWRAP(file.parseDimensions())> | ||
| 23 | <const/targetSizes=transcodeRules.imageSizes.filter((w) => w < width)> | ||
| 24 | <const/sizes=( | ||
| 22 | displayWidthEstimate | 25 | displayWidthEstimate |
| 23 | ? typeof displayWidthEstimate === "number" | 26 | ? typeof displayWidthEstimate === "number" |
| 24 | ? `${displayWidthEstimate}px` | 27 | ? `${displayWidthEstimate}px` |
| 25 | : displayWidthEstimate | 28 | : displayWidthEstimate |
| 26 | : `100vw` | 29 | : `100vw` |
| 27 | /> | 30 | )> |
| 28 | <picture> | 31 | <picture> |
| 29 | <for|ext| of=[ "jxl", "avif", "webp" ]> | 32 | <for|ext| of=["jxl", "avif", "webp"]> |
| 30 | <source | 33 | <source |
| 31 | type=`image/${ext}` | 34 | type=`image/${ext}` |
| 32 | srcset=targetSizes.map((s) => `${base}:/${s}.${ext} ${s}w`) | 35 | srcset=targetSizes.map((s) => `${base}:/${s}.${ext} ${s}w`) |
| 33 | ...{ sizes } | 36 | ...{ sizes } |
| 34 | /> | 37 | > |
| 35 | </> | 38 | </for> |
| 36 | <img | 39 | <img |
| 37 | src=base | 40 | src=base |
| 38 | style=align ? { 'object-position': align, width: '100%', height: '100%' } : undefined | 41 | style=( |
| 42 | align | ||
| 43 | ? { "object-position": align, width: "100%", height: "100%" } | ||
| 44 | : undefined | ||
| 45 | ) | ||
| 39 | ...rest | 46 | ...rest |
| 40 | /> | 47 | > |
| 41 | </picture> | 48 | </picture> |
| 42 | </> | 49 | </else> |
| 43 | 50 | ||
| 44 | import * as transcodeRules from "#src/file-viewer/transcode-rules.ts"; | 51 | import * as transcodeRules from "#src/file-viewer/transcode-rules.ts"; |
| 45 | import { extsVideo } from "#src/file-viewer/rules.ts"; | 52 | import { extsVideo } from "#src/file-viewer/rules.ts"; |
src/tags/clover-video.marko+43-30| ... | @@ -6,25 +6,35 @@ export interface Input { | ... | @@ -6,25 +6,35 @@ export interface Input { |
| 6 | poster?: MediaFile | string; | 6 | poster?: MediaFile | string; |
| 7 | header?: Marko.AttrTag<{ content: Marko.Body }>; | 7 | header?: Marko.AttrTag<{ content: Marko.Body }>; |
| 8 | minimal?: boolean; | 8 | minimal?: boolean; |
| 9 | noFigure?: boolean | 9 | noFigure?: boolean; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | <const/{ id, header, minimal, noFigure } = input /> | 12 | <const/{ id, header, minimal, noFigure }=input> |
| 13 | <const/file = typeof UNWRAP(input.file) === 'object' | 13 | <const/file=( |
| 14 | ? input.file | 14 | typeof UNWRAP(input.file) === "object" |
| 15 | : UNWRAP(MediaFile.getByPath(input.file), `No file ${JSON.stringify(input.file)}`) /> | 15 | ? input.file |
| 16 | <const/poster = input.poster | 16 | : UNWRAP( |
| 17 | ? typeof input.poster === 'object' | 17 | MediaFile.getByPath(input.file), |
| 18 | ? input.poster | 18 | `No file ${JSON.stringify(input.file)}`, |
| 19 | : UNWRAP(MediaFile.getByPath(input.poster), `No file ${JSON.stringify(input.poster)}`) | 19 | ) |
| 20 | : null /> | 20 | )> |
| 21 | <const/poster=( | ||
| 22 | input.poster | ||
| 23 | ? typeof input.poster === "object" | ||
| 24 | ? input.poster | ||
| 25 | : UNWRAP( | ||
| 26 | MediaFile.getByPath(input.poster), | ||
| 27 | `No file ${JSON.stringify(input.poster)}`, | ||
| 28 | ) | ||
| 29 | : null | ||
| 30 | )> | ||
| 21 | 31 | ||
| 22 | <const/{ width, height } = file.parseDimensions() /> | 32 | <const/{ width, height }=file.parseDimensions()> |
| 23 | 33 | ||
| 24 | <define/Inner> | 34 | <define/Inner> |
| 25 | <video | 35 | <video |
| 26 | ...{ id } | 36 | ...{ id } |
| 27 | class={ 'clover-video': noFigure } | 37 | class={ "clover-video": noFigure } |
| 28 | data-src=file.path | 38 | data-src=file.path |
| 29 | poster=poster ? `/file${poster.path}` : null | 39 | poster=poster ? `/file${poster.path}` : null |
| 30 | width=width | 40 | width=width |
| ... | @@ -36,36 +46,39 @@ export interface Input { | ... | @@ -36,36 +46,39 @@ export interface Input { |
| 36 | <figure.clover-video class={ minimal }> | 46 | <figure.clover-video class={ minimal }> |
| 37 | <if=!minimal> | 47 | <if=!minimal> |
| 38 | <figcaption> | 48 | <figcaption> |
| 39 | <if=header><${header.content}/></><else> | 49 | <if=header> |
| 40 | ${file.basename} | 50 | <${header.content}/> |
| 41 | </else> | 51 | </if> |
| 52 | <else>${file.basename}</else> | ||
| 42 | </figcaption> | 53 | </figcaption> |
| 43 | </> | 54 | </if> |
| 44 | <div.stack style=`aspect-ratio:${width}/${height}`> | 55 | <div.stack style=`aspect-ratio:${width}/${height}`> |
| 45 | <Inner /> | 56 | <Inner/> |
| 46 | <noscript.fallback> | 57 | <noscript.fallback> |
| 47 | <h2>scripting disabled!</h2> | 58 | <h2>scripting disabled!</h2> |
| 48 | <p> | 59 | <p> |
| 49 | to play this video, JavaScript must be enabled in your | 60 | to play this video, JavaScript must be enabled in your browser. the |
| 50 | browser. the default video playback behavior is not enabled here | 61 | default video playback behavior is not enabled here as it results in a |
| 51 | as it results in a worse experience on slower internet | 62 | worse experience on slower internet connections, and |
| 52 | connections, and <a href="http://caniuse.com/mpeg-dash">MPEG-DASH | 63 | <a href="http://caniuse.com/mpeg-dash"> |
| 53 | streaming is not natively supported in any browser</a> without JavaScript. | 64 | MPEG-DASH streaming is not natively supported in any browser |
| 65 | </a> | ||
| 66 | without JavaScript. | ||
| 54 | </p> | 67 | </p> |
| 55 | <p> | 68 | <p> |
| 56 | since disabling JavaScript is not the default in any browser, | 69 | since disabling JavaScript is not the default in any browser, you |
| 57 | you likely wanted to download the raw video file anyways. | 70 | likely wanted to download the raw video file anyways. those links are |
| 58 | those links are available below, but note that you will need | 71 | available below, but note that you will need an AV1-compatible video |
| 59 | an AV1-compatible video player, such as VLC Media Player or | 72 | player, such as VLC Media Player or QuickTime Player (M3 chip or |
| 60 | QuickTime Player (M3 chip or newer) | 73 | newer) |
| 61 | </p> | 74 | </p> |
| 62 | </noscript> | 75 | </noscript> |
| 63 | </div> | 76 | </div> |
| 64 | </figure> | 77 | </figure> |
| 65 | </> | 78 | </if> |
| 66 | <else> | 79 | <else> |
| 67 | <Inner /> | 80 | <Inner/> |
| 68 | </> | 81 | </else> |
| 69 | 82 | ||
| 70 | client import "./clover-video.client.ts"; | 83 | client import "./clover-video.client.ts"; |
| 71 | 84 |
src/tags/photo-grid.marko+25-23| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import './photo-grid.css'; | 1 | import "./photo-grid.css"; |
| 2 | 2 | ||
| 3 | static const gap = 11; | 3 | static const gap = 11; |
| 4 | 4 | ||
| ... | @@ -39,12 +39,12 @@ static const alignments = { | ... | @@ -39,12 +39,12 @@ static const alignments = { |
| 39 | "right-end": "100% 50%", | 39 | "right-end": "100% 50%", |
| 40 | }; | 40 | }; |
| 41 | 41 | ||
| 42 | <const/{ base, cols, style } = input /> | 42 | <const/{ base, cols, style }=input> |
| 43 | <const/rows = input.rows ?? [325] /> | 43 | <const/rows=input.rows ?? [325]> |
| 44 | <const/colCount = input.cols.length /> | 44 | <const/colCount=input.cols.length> |
| 45 | <const/rowSum = rows.reduce((a, b) => a + b, 0) + gap * (rows.length - 1) /> | 45 | <const/rowSum=rows.reduce((a, b) => a + b, 0) + gap * (rows.length - 1)> |
| 46 | 46 | ||
| 47 | <const/items = (() => { | 47 | <const/items=(() => { |
| 48 | let occupied: boolean[][] = []; | 48 | let occupied: boolean[][] = []; |
| 49 | const row = (y: number) => (occupied[y] ??= new Array(colCount).fill(false)); | 49 | const row = (y: number) => (occupied[y] ??= new Array(colCount).fill(false)); |
| 50 | const anyCollide = (x: number, y: number, w: number, h: number) => { | 50 | const anyCollide = (x: number, y: number, w: number, h: number) => { |
| ... | @@ -55,18 +55,20 @@ static const alignments = { | ... | @@ -55,18 +55,20 @@ static const alignments = { |
| 55 | return false; | 55 | return false; |
| 56 | }; | 56 | }; |
| 57 | 57 | ||
| 58 | let x = 0, y = 0; | 58 | let x = 0, |
| 59 | y = 0; | ||
| 59 | let result: Positioned[] = []; | 60 | let result: Positioned[] = []; |
| 60 | for (const img of input.img) { | 61 | for (const img of input.img) { |
| 61 | const w = img.w ?? 1; | 62 | const w = img.w ?? 1; |
| 62 | const h = img.h ?? 1; | 63 | const h = img.h ?? 1; |
| 63 | const s = img.s ?? 0; | 64 | const s = img.s ?? 0; |
| 64 | if (w > colCount) throw new Error(`image ${img.src} is wider than the grid`); | 65 | if (w > colCount) |
| 66 | throw new Error(`image ${img.src} is wider than the grid`); | ||
| 65 | 67 | ||
| 66 | x += s; | 68 | x += s; |
| 67 | while (x > colCount - w || anyCollide(x, y, w, h)) { | 69 | while (x > colCount - w || anyCollide(x, y, w, h)) { |
| 68 | x += 1; | 70 | x += 1; |
| 69 | if (x > colCount - w) x = 0, y += 1; | 71 | if (x > colCount - w) ((x = 0), (y += 1)); |
| 70 | } | 72 | } |
| 71 | 73 | ||
| 72 | result.push({ | 74 | result.push({ |
| ... | @@ -74,7 +76,7 @@ static const alignments = { | ... | @@ -74,7 +76,7 @@ static const alignments = { |
| 74 | y, | 76 | y, |
| 75 | w, | 77 | w, |
| 76 | h, | 78 | h, |
| 77 | src: base ? base + '/' + img.src : img.src, | 79 | src: base ? base + "/" + img.src : img.src, |
| 78 | align: alignments[img.align] ?? img.align, | 80 | align: alignments[img.align] ?? img.align, |
| 79 | }); | 81 | }); |
| 80 | 82 | ||
| ... | @@ -84,24 +86,24 @@ static const alignments = { | ... | @@ -84,24 +86,24 @@ static const alignments = { |
| 84 | } | 86 | } |
| 85 | 87 | ||
| 86 | x += w; | 88 | x += w; |
| 87 | if (x >= colCount) x = 0, y += 1; | 89 | if (x >= colCount) ((x = 0), (y += 1)); |
| 88 | } | 90 | } |
| 89 | return result; | 91 | return result; |
| 90 | })() /> | 92 | })()> |
| 91 | 93 | ||
| 92 | <${'photo-grid'} | 94 | <${"photo-grid"} style={ |
| 93 | style={ | 95 | "grid-template-columns": input.cols.map((x) => x + "fr").join(" "), |
| 94 | 'grid-template-columns': input.cols.map(x => x + 'fr').join(' '), | 96 | "grid-template-rows": rows |
| 95 | 'grid-template-rows': rows.map(x => (100 * x / rowSum).toFixed(1) + '%').join(' '), | 97 | .map((x) => ((100 * x) / rowSum).toFixed(1) + "%") |
| 96 | 'aspect-ratio': `768/${rowSum}`, | 98 | .join(" "), |
| 97 | ...style, | 99 | "aspect-ratio": `768/${rowSum}`, |
| 98 | } | 100 | ...style, |
| 99 | > | 101 | }> |
| 100 | <for|{ x, y, w, h, src, align }| of=items> | 102 | <for|{ x, y, w, h, src, align }| of=items> |
| 101 | <${'photo-grid-item'} style={ | 103 | <${"photo-grid-item"} style={ |
| 102 | 'grid-area': `${y + 1} / ${x + 1} / ${1 + y + h} / ${1 + x + w}`, | 104 | "grid-area": `${y + 1} / ${x + 1} / ${1 + y + h} / ${1 + x + w}`, |
| 103 | }> | 105 | }> |
| 104 | <clover-media ...{ src, align } /> | 106 | <clover-media ...{ src, align }/> |
| 105 | </> | 107 | </> |
| 106 | </for> | 108 | </for> |
| 107 | </> | 109 | </> |
src/tags/questions-embed.marko+3-4| ... | @@ -5,11 +5,10 @@ export interface Input { | ... | @@ -5,11 +5,10 @@ export interface Input { |
| 5 | } | 5 | } |
| 6 | 6 | ||
| 7 | <div.questions-embed.qa> | 7 | <div.questions-embed.qa> |
| 8 | <a href="/q+a">ask your own question / view all questions</a> | 8 | <a href="/q+a">ask your own question / view all questions</a><br> |
| 9 | <br /> | ||
| 10 | <for|question| of=Question.search(input.value)> | 9 | <for|question| of=Question.search(input.value)> |
| 11 | <QuestionRender ...{ question } /> | 10 | <QuestionRender ...{ question }/> |
| 12 | </> | 11 | </for> |
| 13 | </div> | 12 | </div> |
| 14 | 13 | ||
| 15 | import QuestionRender from "#src/q+a/tags/question.marko"; | 14 | import QuestionRender from "#src/q+a/tags/question.marko"; |