Compare commits
16 Commits
979627ca2a
...
main
Author | SHA1 | Date | |
---|---|---|---|
0beb3b1071
|
|||
001c524d73
|
|||
9dc01a59be
|
|||
a414a80766
|
|||
46519ef1ea
|
|||
6f9717f530
|
|||
6883780d57
|
|||
acefbcbd14
|
|||
d450f4ed42 | |||
6a54bdeec6 | |||
4ff7ef7b5c | |||
23f33b7472 | |||
0f146ea699 | |||
d8f40500bb
|
|||
1838a25f9a
|
|||
00e7820462
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ data/
|
|||||||
data-dev/
|
data-dev/
|
||||||
_fresh/
|
_fresh/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
mise.toml
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
FROM denoland/deno:2.1.4 AS build
|
FROM denoland/deno:2.3.1 AS build
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl && \
|
curl ffmpeg && \
|
||||||
deno run -A npm:playwright install --with-deps firefox &&\
|
deno run -A npm:playwright install --with-deps firefox &&\
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -23,7 +23,11 @@ function generateJsonLd(resource: GenericResource): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resource.meta?.date) {
|
if (resource.meta?.date) {
|
||||||
baseSchema.datePublished = new Date(resource.meta.date).toISOString();
|
try {
|
||||||
|
baseSchema.datePublished = new Date(resource.meta.date).toISOString();
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore invalid date
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource.meta?.rating) {
|
if (resource.meta?.rating) {
|
||||||
|
@ -110,7 +110,7 @@ function Subline(
|
|||||||
const ctx = useContext(HeroContext);
|
const ctx = useContext(HeroContext);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`relative flex items-center z-50 flex gap-5 font-sm text-light mt-3`}
|
class={`relative flex items-center z-10 flex gap-5 font-sm text-light mt-3`}
|
||||||
style={{ color: ctx.image ? "#1F1F1F" : "white" }}
|
style={{ color: ctx.image ? "#1F1F1F" : "white" }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"lock": false,
|
"lock": false,
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
|
"unstable": [
|
||||||
|
"cron"
|
||||||
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
"start": "deno run --env-file -A --watch=static/,routes/ dev.ts",
|
"dev": "deno run --env-file -A --watch=static/,routes/ dev.ts",
|
||||||
|
"start": "deno run --env-file -A main.ts",
|
||||||
"db": "deno run --env-file -A npm:drizzle-kit",
|
"db": "deno run --env-file -A npm:drizzle-kit",
|
||||||
"build": "deno run -A dev.ts build",
|
"build": "deno run -A dev.ts build",
|
||||||
"preview": "deno run -A main.ts",
|
"preview": "deno run -A main.ts",
|
||||||
|
@ -6,6 +6,7 @@ import * as $_404 from "./routes/_404.tsx";
|
|||||||
import * as $_app from "./routes/_app.tsx";
|
import * as $_app from "./routes/_app.tsx";
|
||||||
import * as $_layout from "./routes/_layout.tsx";
|
import * as $_layout from "./routes/_layout.tsx";
|
||||||
import * as $_middleware from "./routes/_middleware.ts";
|
import * as $_middleware from "./routes/_middleware.ts";
|
||||||
|
import * as $admin_cache_index from "./routes/admin/cache/index.tsx";
|
||||||
import * as $admin_log_index from "./routes/admin/log/index.tsx";
|
import * as $admin_log_index from "./routes/admin/log/index.tsx";
|
||||||
import * as $admin_performance_index from "./routes/admin/performance/index.tsx";
|
import * as $admin_performance_index from "./routes/admin/performance/index.tsx";
|
||||||
import * as $api_articles_name_ from "./routes/api/articles/[name].ts";
|
import * as $api_articles_name_ from "./routes/api/articles/[name].ts";
|
||||||
@ -68,6 +69,7 @@ const manifest = {
|
|||||||
"./routes/_app.tsx": $_app,
|
"./routes/_app.tsx": $_app,
|
||||||
"./routes/_layout.tsx": $_layout,
|
"./routes/_layout.tsx": $_layout,
|
||||||
"./routes/_middleware.ts": $_middleware,
|
"./routes/_middleware.ts": $_middleware,
|
||||||
|
"./routes/admin/cache/index.tsx": $admin_cache_index,
|
||||||
"./routes/admin/log/index.tsx": $admin_log_index,
|
"./routes/admin/log/index.tsx": $admin_log_index,
|
||||||
"./routes/admin/performance/index.tsx": $admin_performance_index,
|
"./routes/admin/performance/index.tsx": $admin_performance_index,
|
||||||
"./routes/api/articles/[name].ts": $api_articles_name_,
|
"./routes/api/articles/[name].ts": $api_articles_name_,
|
||||||
|
@ -191,7 +191,7 @@ export const KMenu = (
|
|||||||
}}
|
}}
|
||||||
style={{ outline: "none !important" }}
|
style={{ outline: "none !important" }}
|
||||||
placeholder="Command"
|
placeholder="Command"
|
||||||
class="bg-transparent color pl-4 outline outline outline-2 outline-offset-2"
|
class="bg-transparent color pl-4 outline-none"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -27,9 +27,11 @@ export function Link(
|
|||||||
clearTimeout(globalThis.loadingTimeout);
|
clearTimeout(globalThis.loadingTimeout);
|
||||||
delete globalThis.loadingTimeout;
|
delete globalThis.loadingTimeout;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
globalThis.dispatchEvent(new CustomEvent("loading-finished"));
|
||||||
document.querySelector("main")?.classList.remove("loading");
|
document.querySelector("main")?.classList.remove("loading");
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
|
globalThis.dispatchEvent(new CustomEvent("loading-finished"));
|
||||||
document.querySelector("main")?.classList.remove("loading");
|
document.querySelector("main")?.classList.remove("loading");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
21
lib/cache.ts
21
lib/cache.ts
@ -8,7 +8,10 @@ interface SetCacheOptions {
|
|||||||
|
|
||||||
export const caches = new Map<
|
export const caches = new Map<
|
||||||
string,
|
string,
|
||||||
{ info: () => { count: number; sizeInKB: number } }
|
{
|
||||||
|
info: () => { name: string; count: number; sizeInKB: number };
|
||||||
|
clear: () => void;
|
||||||
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
export function createCache<T>(
|
export function createCache<T>(
|
||||||
@ -31,6 +34,10 @@ export function createCache<T>(
|
|||||||
return entry.value; // Return value if not expired
|
return entry.value; // Return value if not expired
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
cache.clear();
|
||||||
|
},
|
||||||
|
|
||||||
set(key: string, value: T | unknown, opts: SetCacheOptions = {}) {
|
set(key: string, value: T | unknown, opts: SetCacheOptions = {}) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const expiresIn = opts.expires ?? createOpts.expires;
|
const expiresIn = opts.expires ?? createOpts.expires;
|
||||||
@ -48,7 +55,7 @@ export function createCache<T>(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
info(): { count: number; sizeInKB: number } {
|
info() {
|
||||||
// Cleanup expired entries before calculating info
|
// Cleanup expired entries before calculating info
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|
||||||
@ -65,8 +72,8 @@ export function createCache<T>(
|
|||||||
totalBytes += keySize + valueSize;
|
totalBytes += keySize + valueSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeInKB = totalBytes / 1024; // Convert bytes to kilobytes
|
const sizeInKB = Math.floor(totalBytes / 1024); // Convert bytes to kilobytes
|
||||||
return { count, sizeInKB };
|
return { name: cacheName, count, sizeInKB };
|
||||||
},
|
},
|
||||||
|
|
||||||
has(key: string): boolean {
|
has(key: string): boolean {
|
||||||
@ -94,7 +101,13 @@ export function createCache<T>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
caches.set(cacheName, {
|
caches.set(cacheName, {
|
||||||
|
clear: api.clear.bind(api),
|
||||||
info: api.info.bind(api),
|
info: api.info.bind(api),
|
||||||
});
|
});
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCacheInfo() {
|
||||||
|
return [...caches.values().map((c) => c.info())];
|
||||||
|
}
|
||||||
|
12
lib/crud.ts
12
lib/crud.ts
@ -80,22 +80,17 @@ export function createCrud<T extends GenericResource>(
|
|||||||
async function read(id: string) {
|
async function read(id: string) {
|
||||||
const path = pathFromId(id);
|
const path = pathFromId(id);
|
||||||
|
|
||||||
if (cache.has(path)) {
|
|
||||||
return cache.get(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await getDocument(path);
|
const content = await getDocument(path);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parse(content, id);
|
let parsed = parse(content, id);
|
||||||
|
|
||||||
if (hasThumbnails) {
|
if (hasThumbnails) {
|
||||||
return addThumbnailToResource(parsed);
|
parsed = await addThumbnailToResource(parsed);
|
||||||
}
|
}
|
||||||
const doc = { ...parsed, content };
|
const doc = { ...parsed, content };
|
||||||
cache.set(path, doc, { expires: 10 * 1000 });
|
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
@ -110,7 +105,6 @@ export function createCrud<T extends GenericResource>(
|
|||||||
|
|
||||||
if (render) {
|
if (render) {
|
||||||
const rendered = render(content);
|
const rendered = render(content);
|
||||||
cache.set(path, content);
|
|
||||||
return createDocument(path, rendered);
|
return createDocument(path, rendered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +118,6 @@ export function createCrud<T extends GenericResource>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newDoc = transformDocument(content, updater);
|
const newDoc = transformDocument(content, updater);
|
||||||
cache.set("all", undefined);
|
|
||||||
await createDocument(path, newDoc);
|
await createDocument(path, newDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +137,6 @@ export function createCrud<T extends GenericResource>(
|
|||||||
}),
|
}),
|
||||||
)).sort(sortFunction<T>(sort)).filter((v) => !!v);
|
)).sort(sortFunction<T>(sort)).filter((v) => !!v);
|
||||||
|
|
||||||
cache.set("all", parsed);
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,8 +132,35 @@ export function parseDocument(doc: string) {
|
|||||||
.parse(doc);
|
.parse(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeFrontmatter(doc: string) {
|
||||||
|
if (doc.trim().startsWith("---")) {
|
||||||
|
return doc.trim().split("---").filter((s) => s.length).slice(1).join("---");
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeImage(doc: string, imageUrl?: string) {
|
||||||
|
if (!imageUrl) {
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
// Remove image from content
|
||||||
|
const first = doc.slice(0, 500);
|
||||||
|
const second = doc.slice(500);
|
||||||
|
|
||||||
|
// Regex pattern to match the image Markdown syntax with the specific URL
|
||||||
|
const pattern = new RegExp(
|
||||||
|
`!\\[.*?\\]\\(${imageUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\)`,
|
||||||
|
"g",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the matched image
|
||||||
|
const updatedMarkdown = first.replace(pattern, "");
|
||||||
|
return updatedMarkdown + second;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderMarkdown(doc: string) {
|
export function renderMarkdown(doc: string) {
|
||||||
return render(doc, {
|
return render(removeFrontmatter(doc), {
|
||||||
|
baseUrl: SILVERBULLET_SERVER,
|
||||||
allowMath: true,
|
allowMath: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER");
|
|||||||
export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY");
|
export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY");
|
||||||
export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
||||||
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY");
|
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY");
|
||||||
|
export const TELEGRAM_API_KEY = Deno.env.get("TELEGRAM_API_KEY")!;
|
||||||
|
|
||||||
export const GITEA_SERVER = Deno.env.get("GITEA_SERVER");
|
export const GITEA_SERVER = Deno.env.get("GITEA_SERVER");
|
||||||
export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID")!;
|
export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID")!;
|
||||||
|
@ -107,3 +107,24 @@ export function parseRating(rating: string | number) {
|
|||||||
}
|
}
|
||||||
return rating;
|
return rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function convertOggToMp3(
|
||||||
|
oggData: ArrayBuffer,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const ffmpeg = new Deno.Command("ffmpeg", {
|
||||||
|
args: ["-f", "ogg", "-i", "pipe:0", "-f", "mp3", "pipe:1"],
|
||||||
|
stdin: "piped",
|
||||||
|
stdout: "piped",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
const process = ffmpeg.spawn();
|
||||||
|
const writer = process.stdin.getWriter();
|
||||||
|
await writer.write(new Uint8Array(oggData));
|
||||||
|
await writer.close();
|
||||||
|
|
||||||
|
const output = await process.output();
|
||||||
|
const { code } = await process.status;
|
||||||
|
if (code !== 0) throw new Error(`FFmpeg exited with code ${code}`);
|
||||||
|
return output.stdout;
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { zodResponseFormat } from "https://deno.land/x/openai@v4.69.0/helpers/zo
|
|||||||
import { OPENAI_API_KEY } from "@lib/env.ts";
|
import { OPENAI_API_KEY } from "@lib/env.ts";
|
||||||
import { hashString } from "@lib/helpers.ts";
|
import { hashString } from "@lib/helpers.ts";
|
||||||
import { createCache } from "@lib/cache.ts";
|
import { createCache } from "@lib/cache.ts";
|
||||||
import recipeSchema, { recipeResponseSchema } from "@lib/recipeSchema.ts";
|
import { recipeResponseSchema } from "@lib/recipeSchema.ts";
|
||||||
|
|
||||||
const openAI = OPENAI_API_KEY && new OpenAI({ apiKey: OPENAI_API_KEY });
|
const openAI = OPENAI_API_KEY && new OpenAI({ apiKey: OPENAI_API_KEY });
|
||||||
|
|
||||||
@ -228,3 +228,21 @@ export async function extractRecipe(content: string) {
|
|||||||
|
|
||||||
return recipeResponseSchema.parse(completion.choices[0].message.parsed);
|
return recipeResponseSchema.parse(completion.choices[0].message.parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function transcribe(
|
||||||
|
mp3Data: Uint8Array,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!openAI) return;
|
||||||
|
|
||||||
|
const file = new File([mp3Data], "audio.mp3", {
|
||||||
|
type: "audio/mpeg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await openAI.audio.transcriptions.create({
|
||||||
|
file,
|
||||||
|
model: "whisper-1",
|
||||||
|
response_format: "text",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@ -50,7 +50,8 @@ export function isValidRecipe(
|
|||||||
| null
|
| null
|
||||||
| undefined,
|
| undefined,
|
||||||
) {
|
) {
|
||||||
return recipe?.ingredients?.length && recipe?.instructions?.length &&
|
return recipe?.ingredients?.length && recipe.ingredients.length > 1 &&
|
||||||
|
recipe?.instructions?.length &&
|
||||||
recipe.name?.length;
|
recipe.name?.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,15 @@ export function safeFileName(inputString: string): string {
|
|||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toUrlSafeString(input: string): string {
|
||||||
|
return input
|
||||||
|
.trim() // Remove leading and trailing whitespace
|
||||||
|
.toLowerCase() // Convert to lowercase
|
||||||
|
.replace(/[^a-z0-9\s-]/g, "") // Remove non-alphanumeric characters except spaces and hyphens
|
||||||
|
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
||||||
|
.replace(/-+/g, "-"); // Remove consecutive hyphens
|
||||||
|
}
|
||||||
|
|
||||||
export function extractHashTags(inputString: string) {
|
export function extractHashTags(inputString: string) {
|
||||||
const hashtags = [];
|
const hashtags = [];
|
||||||
|
|
||||||
|
110
lib/taskManager.ts
Normal file
110
lib/taskManager.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { transcribe } from "@lib/openai.ts";
|
||||||
|
import { createDocument } from "@lib/documents.ts";
|
||||||
|
import { createLogger } from "./log/index.ts";
|
||||||
|
import { convertOggToMp3 } from "./helpers.ts";
|
||||||
|
|
||||||
|
const log = createLogger("taskManager");
|
||||||
|
|
||||||
|
// In-memory task state
|
||||||
|
const activeTasks: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
noteName: string;
|
||||||
|
entries: Array<
|
||||||
|
{ type: string; content: string | ArrayBufferLike; fileName?: string }
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
export function startTask(chatId: string, noteName: string) {
|
||||||
|
activeTasks[chatId] = { noteName, entries: [] };
|
||||||
|
log.info(`Started note: ${noteName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function endTask(chatId: string): Promise<string | null> {
|
||||||
|
const task = activeTasks[chatId];
|
||||||
|
if (!task) return null;
|
||||||
|
|
||||||
|
log.info("Ending note", task.noteName);
|
||||||
|
|
||||||
|
let finalNote = `# ${task.noteName}\n\n`;
|
||||||
|
|
||||||
|
const photoTasks: { content: ArrayBuffer; path: string }[] = [];
|
||||||
|
|
||||||
|
let photoIndex = 0;
|
||||||
|
for (const entry of task.entries) {
|
||||||
|
if (entry.type === "text") {
|
||||||
|
finalNote += entry.content + "\n\n";
|
||||||
|
} else if (entry.type === "voice") {
|
||||||
|
try {
|
||||||
|
log.info("Converting OGG to MP3");
|
||||||
|
const mp3Data = await convertOggToMp3(entry.content as ArrayBuffer);
|
||||||
|
log.info("Finished converting OGG to MP3, transcribing...");
|
||||||
|
const transcript = await transcribe(mp3Data);
|
||||||
|
finalNote += `**Voice Transcript:**\n${transcript}\n\n`;
|
||||||
|
log.info("Finished transcribing");
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
finalNote += "**[Voice message could not be transcribed]**\n\n";
|
||||||
|
}
|
||||||
|
} else if (entry.type === "photo") {
|
||||||
|
const photoUrl = `${
|
||||||
|
task.noteName.replace(/\.md$/, "")
|
||||||
|
}/photo-${photoIndex++}.jpg`;
|
||||||
|
|
||||||
|
finalNote += `**Photo**:\n ${photoUrl}\n\n`;
|
||||||
|
photoTasks.push({
|
||||||
|
content: entry.content as ArrayBuffer,
|
||||||
|
path: photoUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const entry of photoTasks) {
|
||||||
|
await createDocument(entry.path, entry.content, "image/jpeg");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error("Error creating photo document:", err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await createDocument(task.noteName, finalNote, "text/markdown");
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Error creating document:", error);
|
||||||
|
return error instanceof Error
|
||||||
|
? error.toString()
|
||||||
|
: "Error creating document";
|
||||||
|
}
|
||||||
|
|
||||||
|
delete activeTasks[chatId];
|
||||||
|
log.debug({ finalNote });
|
||||||
|
return finalNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTextEntry(chatId: string, text: string) {
|
||||||
|
const task = activeTasks[chatId];
|
||||||
|
if (!task) return;
|
||||||
|
const entry = { type: "text", content: text };
|
||||||
|
log.debug("New Entry", entry);
|
||||||
|
task.entries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addVoiceEntry(chatId: string, buffer: ArrayBufferLike) {
|
||||||
|
const task = activeTasks[chatId];
|
||||||
|
if (!task) return;
|
||||||
|
const entry = { type: "voice", content: buffer };
|
||||||
|
log.debug("New Entry", entry);
|
||||||
|
task.entries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPhotoEntry(
|
||||||
|
chatId: string,
|
||||||
|
buffer: ArrayBufferLike,
|
||||||
|
fileName: string,
|
||||||
|
) {
|
||||||
|
const task = activeTasks[chatId];
|
||||||
|
if (!task) return;
|
||||||
|
const entry = { type: "photo", content: buffer, fileName };
|
||||||
|
log.debug("New Entry", entry);
|
||||||
|
task.entries.push(entry);
|
||||||
|
}
|
65
lib/telegram.ts
Normal file
65
lib/telegram.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Bot } from "https://deno.land/x/grammy@v1.36.1/mod.ts";
|
||||||
|
import { TELEGRAM_API_KEY } from "@lib/env.ts";
|
||||||
|
import { createLogger } from "./log/index.ts";
|
||||||
|
|
||||||
|
const bot = new Bot(TELEGRAM_API_KEY);
|
||||||
|
const log = createLogger("telegram");
|
||||||
|
|
||||||
|
import * as manager from "./taskManager.ts";
|
||||||
|
|
||||||
|
async function downloadFile(filePath: string): Promise<Uint8Array> {
|
||||||
|
log.info(`Downloading file from path: ${filePath}`);
|
||||||
|
const url = `https://api.telegram.org/file/bot${bot.token}/${filePath}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to download file: " + response.statusText);
|
||||||
|
}
|
||||||
|
log.info("File downloaded successfully");
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.command("start", async (ctx) => {
|
||||||
|
log.info("Received /start command");
|
||||||
|
const [_, noteName] = ctx.message?.text?.split(" ") || [];
|
||||||
|
if (!noteName) {
|
||||||
|
return ctx.reply("Please provide a note name. Usage: /start NoteName");
|
||||||
|
}
|
||||||
|
manager.startTask(ctx.chat.id.toString(), noteName);
|
||||||
|
await ctx.reply(`Started note: ${noteName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.command("end", async (ctx) => {
|
||||||
|
log.info("Received /end command");
|
||||||
|
const finalNote = await manager.endTask(ctx.chat.id.toString());
|
||||||
|
if (!finalNote) return ctx.reply("No active note found.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ctx.reply("Note complete. Here is your markdown:");
|
||||||
|
await ctx.reply(finalNote, { parse_mode: "MarkdownV2" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error sending final note:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on("message:text", (ctx) => {
|
||||||
|
log.info("Received text message");
|
||||||
|
manager.addTextEntry(ctx.chat.id.toString(), ctx.message.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on("message:voice", async (ctx) => {
|
||||||
|
log.info("Received photo message");
|
||||||
|
log.info("Received voice message");
|
||||||
|
const file = await ctx.getFile();
|
||||||
|
const buffer = await downloadFile(file.file_path!);
|
||||||
|
manager.addVoiceEntry(ctx.chat.id.toString(), buffer.buffer);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on("message:photo", async (ctx) => {
|
||||||
|
const file = await ctx.getFile();
|
||||||
|
const buffer = await downloadFile(file.file_path!);
|
||||||
|
const fileName = file.file_path!.split("/").pop()!;
|
||||||
|
manager.addPhotoEntry(ctx.chat.id.toString(), buffer.buffer, fileName);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.start();
|
1
main.ts
1
main.ts
@ -7,5 +7,6 @@
|
|||||||
import { start } from "$fresh/server.ts";
|
import { start } from "$fresh/server.ts";
|
||||||
import manifest from "./fresh.gen.ts";
|
import manifest from "./fresh.gen.ts";
|
||||||
import config from "./fresh.config.ts";
|
import config from "./fresh.config.ts";
|
||||||
|
import "@lib/telegram.ts";
|
||||||
|
|
||||||
await start(manifest, config);
|
await start(manifest, config);
|
||||||
|
@ -19,6 +19,7 @@ export default function App({ Component }: PageProps) {
|
|||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#141218" />
|
<meta name="theme-color" content="#141218" />
|
||||||
<style>{globalCss}</style>
|
<style>{globalCss}</style>
|
||||||
<title>Memorium</title>
|
<title>Memorium</title>
|
||||||
|
32
routes/admin/cache/index.tsx
vendored
Normal file
32
routes/admin/cache/index.tsx
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
import { getCacheInfo } from "@lib/cache.ts";
|
||||||
|
|
||||||
|
export const handler: Handlers<
|
||||||
|
{ cacheInfo: ReturnType<typeof getCacheInfo> }
|
||||||
|
> = {
|
||||||
|
GET(_, ctx) {
|
||||||
|
return ctx.render({ cacheInfo: getCacheInfo() });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Greet(
|
||||||
|
props: PageProps<
|
||||||
|
{ cacheInfo: ReturnType<typeof getCacheInfo> }
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const { cacheInfo } = props.data;
|
||||||
|
return (
|
||||||
|
<MainLayout
|
||||||
|
url={props.url}
|
||||||
|
title="Recipes"
|
||||||
|
context={{ type: "recipe" }}
|
||||||
|
>
|
||||||
|
<code>
|
||||||
|
<pre class="text-white">
|
||||||
|
{JSON.stringify(cacheInfo, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -8,7 +8,11 @@ import * as openai from "@lib/openai.ts";
|
|||||||
import tds from "https://cdn.skypack.dev/turndown@7.2.0";
|
import tds from "https://cdn.skypack.dev/turndown@7.2.0";
|
||||||
import { Article, createArticle } from "@lib/resource/articles.ts";
|
import { Article, createArticle } from "@lib/resource/articles.ts";
|
||||||
import { getYoutubeVideoDetails } from "@lib/youtube.ts";
|
import { getYoutubeVideoDetails } from "@lib/youtube.ts";
|
||||||
import { extractYoutubeId, isYoutubeLink } from "@lib/string.ts";
|
import {
|
||||||
|
extractYoutubeId,
|
||||||
|
isYoutubeLink,
|
||||||
|
toUrlSafeString,
|
||||||
|
} from "@lib/string.ts";
|
||||||
import { createLogger } from "@lib/log/index.ts";
|
import { createLogger } from "@lib/log/index.ts";
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
@ -69,6 +73,10 @@ async function processCreateArticle(
|
|||||||
const url = new URL(fetchUrl);
|
const url = new URL(fetchUrl);
|
||||||
|
|
||||||
function makeUrlAbsolute(src: string) {
|
function makeUrlAbsolute(src: string) {
|
||||||
|
if (src.startsWith("//")) {
|
||||||
|
return "https:" + src;
|
||||||
|
}
|
||||||
|
|
||||||
if (src.startsWith("/")) {
|
if (src.startsWith("/")) {
|
||||||
return `${url.origin}${src.replace(/$\//, "")}`;
|
return `${url.origin}${src.replace(/$\//, "")}`;
|
||||||
}
|
}
|
||||||
@ -98,20 +106,16 @@ async function processCreateArticle(
|
|||||||
|
|
||||||
if (href.startsWith("/")) {
|
if (href.startsWith("/")) {
|
||||||
return `[${content}](${url.origin}${href.replace(/$\//, "")})`;
|
return `[${content}](${url.origin}${href.replace(/$\//, "")})`;
|
||||||
}
|
} else if (href.startsWith("//")) {
|
||||||
|
return `[${content}](https:${href})`;
|
||||||
if (href.startsWith("#")) {
|
} else if (href.startsWith("#")) {
|
||||||
if (content.length < 2) return "";
|
if (content.length < 2) return "";
|
||||||
return `[${content}](${url.href}#${href})`.replace("##", "#");
|
return `[${content}](${url.href}#${href})`.replace("##", "#");
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (!href.startsWith("https://") && !href.startsWith("http://")) {
|
|
||||||
return `[${content}](${url.origin.replace(/\/$/, "")}/${
|
return `[${content}](${url.origin.replace(/\/$/, "")}/${
|
||||||
href.replace(/^\//, "")
|
href.replace(/^\//, "")
|
||||||
})`;
|
})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `[${content}](${href})`;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -125,7 +129,10 @@ async function processCreateArticle(
|
|||||||
metaAuthor || openai.extractAuthorName(markdown),
|
metaAuthor || openai.extractAuthorName(markdown),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const id = shortTitle || title || "";
|
console.log({ tags, shortTitle, author });
|
||||||
|
streamResponse.enqueue("postprocessing article");
|
||||||
|
|
||||||
|
const id = toUrlSafeString(shortTitle || title || "");
|
||||||
|
|
||||||
const meta: Article["meta"] = {
|
const meta: Article["meta"] = {
|
||||||
author: (author || "").replace("@", "twitter:"),
|
author: (author || "").replace("@", "twitter:"),
|
||||||
@ -161,7 +168,7 @@ async function processCreateArticle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
streamResponse.enqueue("finished processing");
|
streamResponse.enqueue("writing to disk");
|
||||||
|
|
||||||
await createArticle(newArticle.id, newArticle);
|
await createArticle(newArticle.id, newArticle);
|
||||||
|
|
||||||
|
@ -2,11 +2,14 @@ import { Handlers } from "$fresh/server.ts";
|
|||||||
import { documentTable } from "@lib/db/schema.ts";
|
import { documentTable } from "@lib/db/schema.ts";
|
||||||
import { db } from "@lib/db/sqlite.ts";
|
import { db } from "@lib/db/sqlite.ts";
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
|
import { caches } from "@lib/cache.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async DELETE() {
|
async DELETE() {
|
||||||
|
for (const cache of caches.values()) {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
await db.delete(documentTable).run();
|
await db.delete(documentTable).run();
|
||||||
return json({ status: "ok" });
|
return json({ status: "ok" });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { KMenu } from "@islands/KMenu.tsx";
|
|||||||
import { YoutubePlayer } from "@components/Youtube.tsx";
|
import { YoutubePlayer } from "@components/Youtube.tsx";
|
||||||
import { HashTags } from "@components/HashTags.tsx";
|
import { HashTags } from "@components/HashTags.tsx";
|
||||||
import { isYoutubeLink } from "@lib/string.ts";
|
import { isYoutubeLink } from "@lib/string.ts";
|
||||||
import { renderMarkdown } from "@lib/documents.ts";
|
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import PageHero from "@components/PageHero.tsx";
|
import PageHero from "@components/PageHero.tsx";
|
||||||
import { Star } from "@components/Stars.tsx";
|
import { Star } from "@components/Stars.tsx";
|
||||||
@ -14,6 +14,9 @@ import { MetaTags } from "@components/MetaTags.tsx";
|
|||||||
export const handler: Handlers<{ article: Article; session: unknown }> = {
|
export const handler: Handlers<{ article: Article; session: unknown }> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const article = await getArticle(ctx.params.name);
|
const article = await getArticle(ctx.params.name);
|
||||||
|
if (!article) {
|
||||||
|
return ctx.renderNotFound();
|
||||||
|
}
|
||||||
return ctx.render({ article, session: ctx.state.session });
|
return ctx.render({ article, session: ctx.state.session });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,7 +28,9 @@ export default function Greet(
|
|||||||
|
|
||||||
const { author = "", date = "" } = article.meta;
|
const { author = "", date = "" } = article.meta;
|
||||||
|
|
||||||
const content = renderMarkdown(article.content);
|
const content = renderMarkdown(
|
||||||
|
removeImage(article.content, article.meta.image),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout
|
<MainLayout
|
||||||
|
@ -4,6 +4,7 @@ import { PageProps } from "$fresh/server.ts";
|
|||||||
import { resources } from "@lib/resources.ts";
|
import { resources } from "@lib/resources.ts";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
|
import "@lib/telegram.ts";
|
||||||
|
|
||||||
export default function Home(props: PageProps) {
|
export default function Home(props: PageProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -2,7 +2,7 @@ import { PageProps, RouteContext } from "$fresh/server.ts";
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { getMovie, Movie } from "@lib/resource/movies.ts";
|
import { getMovie, Movie } from "@lib/resource/movies.ts";
|
||||||
import { HashTags } from "@components/HashTags.tsx";
|
import { HashTags } from "@components/HashTags.tsx";
|
||||||
import { renderMarkdown } from "@lib/documents.ts";
|
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { Recommendations } from "@islands/Recommendations.tsx";
|
import { Recommendations } from "@islands/Recommendations.tsx";
|
||||||
@ -17,9 +17,15 @@ export default async function Greet(
|
|||||||
const movie = await getMovie(ctx.params.name);
|
const movie = await getMovie(ctx.params.name);
|
||||||
const session = ctx.state.session;
|
const session = ctx.state.session;
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return ctx.renderNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
const { author = "", date = "" } = movie.meta;
|
const { author = "", date = "" } = movie.meta;
|
||||||
|
|
||||||
const content = renderMarkdown(movie.description || "");
|
const content = renderMarkdown(
|
||||||
|
removeImage(movie.description || "", movie.meta.image),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout url={props.url} title={`Movie > ${movie.name}`} context={movie}>
|
<MainLayout url={props.url} title={`Movie > ${movie.name}`} context={movie}>
|
||||||
|
@ -60,7 +60,7 @@ function ValidRecipe({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Greet(
|
export default function Page(
|
||||||
props: PageProps<{ recipe: Recipe; session: Record<string, string> }>,
|
props: PageProps<{ recipe: Recipe; session: Record<string, string> }>,
|
||||||
) {
|
) {
|
||||||
const { recipe, session } = props.data;
|
const { recipe, session } = props.data;
|
||||||
@ -114,6 +114,7 @@ export default function Greet(
|
|||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div
|
<div
|
||||||
|
class="whitespace-break-spaces markdown-body"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: renderMarkdown(recipe?.markdown || ""),
|
__html: renderMarkdown(recipe?.markdown || ""),
|
||||||
}}
|
}}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { HashTags } from "@components/HashTags.tsx";
|
import { HashTags } from "@components/HashTags.tsx";
|
||||||
import { renderMarkdown } from "@lib/documents.ts";
|
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
||||||
import { getSeries, Series } from "@lib/resource/series.ts";
|
import { getSeries, Series } from "@lib/resource/series.ts";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
@ -12,6 +12,10 @@ import { MetaTags } from "@components/MetaTags.tsx";
|
|||||||
export const handler: Handlers<{ serie: Series; session: unknown }> = {
|
export const handler: Handlers<{ serie: Series; session: unknown }> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const serie = await getSeries(ctx.params.name);
|
const serie = await getSeries(ctx.params.name);
|
||||||
|
|
||||||
|
if (!serie) {
|
||||||
|
return ctx.renderNotFound();
|
||||||
|
}
|
||||||
return ctx.render({ serie, session: ctx.state.session });
|
return ctx.render({ serie, session: ctx.state.session });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -23,7 +27,9 @@ export default function Greet(
|
|||||||
|
|
||||||
const { author = "", date = "" } = serie.meta;
|
const { author = "", date = "" } = serie.meta;
|
||||||
|
|
||||||
const content = renderMarkdown(serie.description || "");
|
const content = renderMarkdown(
|
||||||
|
removeImage(serie.description || "", serie.meta.image),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout url={props.url} title={`Serie > ${serie.name}`} context={serie}>
|
<MainLayout url={props.url} title={`Serie > ${serie.name}`} context={serie}>
|
||||||
|
@ -128,3 +128,34 @@ main {
|
|||||||
main.loading {
|
main.loading {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-body>h1 {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body>h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body>h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body>h1>.anchor,
|
||||||
|
.markdown-body>h2>.anchor,
|
||||||
|
.markdown-body>h3>.anchor {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-left: -26px;
|
||||||
|
margin-right: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body>h1:hover .anchor,
|
||||||
|
.markdown-body>h2:hover .anchor,
|
||||||
|
.markdown-body>h3:hover .anchor {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
@ -226,39 +226,44 @@ function rgbaToDataURL(w, h, rgba) {
|
|||||||
return "data:image/png;base64," + btoa(String.fromCharCode(...bytes));
|
return "data:image/png;base64," + btoa(String.fromCharCode(...bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll("[data-thumb]").forEach((entry) => {
|
function updateThumbnailImages() {
|
||||||
const hash = entry.getAttribute("data-thumb");
|
document.querySelectorAll("[data-thumb]").forEach((entry) => {
|
||||||
|
const hash = entry.getAttribute("data-thumb");
|
||||||
|
|
||||||
if (!hash) return;
|
if (!hash) return;
|
||||||
|
|
||||||
const decodedString = atob(hash);
|
const decodedString = atob(hash);
|
||||||
|
|
||||||
// Create Uint8Array from decoded string
|
// Create Uint8Array from decoded string
|
||||||
const buffer = new Uint8Array(decodedString.length);
|
const buffer = new Uint8Array(decodedString.length);
|
||||||
for (let i = 0; i < decodedString.length; i++) {
|
for (let i = 0; i < decodedString.length; i++) {
|
||||||
buffer[i] = decodedString.charCodeAt(i);
|
buffer[i] = decodedString.charCodeAt(i);
|
||||||
}
|
|
||||||
|
|
||||||
const image = thumbHashToRGBA(buffer);
|
|
||||||
const dataURL = rgbaToDataURL(image.w, image.h, image.rgba);
|
|
||||||
|
|
||||||
entry.style.background = `url(${dataURL})`;
|
|
||||||
entry.style.backgroundSize = "cover";
|
|
||||||
|
|
||||||
const child = entry.querySelector("img[data-thumb-img]");
|
|
||||||
setTimeout(() => {
|
|
||||||
const isLoaded = child && child.complete && child.naturalHeight !== 0;
|
|
||||||
if (child && !isLoaded) {
|
|
||||||
child.style.opacity = 0;
|
|
||||||
child.style.filter = "blur(5px)";
|
|
||||||
child.addEventListener("load", () => {
|
|
||||||
child.style.transition = "opacity 0.3s ease, filter 0.6s ease";
|
|
||||||
child.style.opacity = 1;
|
|
||||||
child.style.filter = "blur(0px)";
|
|
||||||
setTimeout(() => {
|
|
||||||
entry.style.background = "";
|
|
||||||
}, 400);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, 50);
|
|
||||||
});
|
const image = thumbHashToRGBA(buffer);
|
||||||
|
const dataURL = rgbaToDataURL(image.w, image.h, image.rgba);
|
||||||
|
|
||||||
|
entry.style.background = `url(${dataURL})`;
|
||||||
|
entry.style.backgroundSize = "cover";
|
||||||
|
|
||||||
|
const child = entry.querySelector("img[data-thumb-img]");
|
||||||
|
setTimeout(() => {
|
||||||
|
const isLoaded = child && child.complete && child.naturalHeight !== 0;
|
||||||
|
if (child && !isLoaded) {
|
||||||
|
child.style.opacity = 0;
|
||||||
|
child.style.filter = "blur(5px)";
|
||||||
|
child.addEventListener("load", () => {
|
||||||
|
child.style.transition = "opacity 0.3s ease, filter 0.6s ease";
|
||||||
|
child.style.opacity = 1;
|
||||||
|
child.style.filter = "blur(0px)";
|
||||||
|
setTimeout(() => {
|
||||||
|
entry.style.background = "";
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.addEventListener("load", updateThumbnailImages);
|
||||||
|
globalThis.addEventListener("loading-finished", updateThumbnailImages);
|
||||||
|
Reference in New Issue
Block a user