refactor: rework assets fetching

This commit is contained in:
Pihkaal
2024-07-25 10:02:15 +02:00
parent a0be097c34
commit f8e3e20d9d
15 changed files with 239 additions and 281 deletions

2
.gitignore vendored
View File

@@ -30,4 +30,4 @@ yarn-error.log*
.notes .notes
# Generated # Generated
/src/manifest.ts /src/assets.ts

View File

@@ -4,6 +4,24 @@ import { spawnSync } from "child_process";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { env } from "./env"; import { env } from "./env";
type Manifest = {
files: string[];
projects: string[];
links: {
name: string;
url: string;
icon: string;
};
};
type Project = {
name: string;
content: string;
language: string | null;
url: string;
private: boolean;
};
export const manifest = (): Plugin => ({ export const manifest = (): Plugin => ({
name: "generate-pages-plugin", name: "generate-pages-plugin",
buildStart: async () => { buildStart: async () => {
@@ -15,12 +33,12 @@ export const manifest = (): Plugin => ({
}); });
try { try {
const storedUpdatedAt = ( const storedUpdatedAt = (
await readFile("./node_modules/.cache/manifest") await readFile("./node_modules/.cache/assets")
).toString(); ).toString();
if (storedUpdatedAt === manifestRepo.updated_at) return; if (storedUpdatedAt === manifestRepo.updated_at) return;
} catch {} } catch {}
await writeFile("./node_modules/.cache/manifest", manifestRepo.updated_at); await writeFile("./node_modules/.cache/assets", manifestRepo.updated_at);
const getRepoFileContent = async (repo: string, path: string) => { const getRepoFileContent = async (repo: string, path: string) => {
const { data: file } = await octokit.repos.getContent({ const { data: file } = await octokit.repos.getContent({
@@ -36,18 +54,9 @@ export const manifest = (): Plugin => ({
const manifest = JSON.parse( const manifest = JSON.parse(
await getRepoFileContent(env.GITHUB_USERNAME, "manifest.json"), await getRepoFileContent(env.GITHUB_USERNAME, "manifest.json"),
) as { ) as Manifest;
files: string[];
projects: string[];
};
const projects: Array<{ const projects: Array<Project> = [];
name: string;
content: string;
language: string | null;
url: string;
private: boolean;
}> = [];
for (const project of manifest.projects) { for (const project of manifest.projects) {
const { data: repo } = await octokit.repos.get({ const { data: repo } = await octokit.repos.get({
owner: env.GITHUB_USERNAME, owner: env.GITHUB_USERNAME,
@@ -67,18 +76,16 @@ export const manifest = (): Plugin => ({
const code = ` const code = `
const projects = ${JSON.stringify(projects, null, 2)} as const; const projects = ${JSON.stringify(projects, null, 2)} as const;
export type Project = typeof projects[number]; const links = ${JSON.stringify(manifest.links, null, 2)} as const;
const projectsMap = Object.fromEntries(projects.map(project => [project.name, project])) as const; export const assets = {
export const manifest = {
projects, projects,
projectsMap links
}; };
`; `;
await writeFile("./src/manifest.ts", code); await writeFile("./src/assets.ts", code);
spawnSync("prettier", ["--write", "./src/manifest.ts"]); spawnSync("prettier", ["--write", "./src/assets.ts"]);
}, },
}); });

View File

@@ -116,7 +116,6 @@ const InnerCava = (props: InnerKittyProps<typeof Cava>) => {
const analyser = audioContext.createAnalyser(); const analyser = audioContext.createAnalyser();
analyser.fftSize = 256; analyser.fftSize = 256;
console.log("ok");
void audioElement.play().then(() => void audioContext.resume()); void audioElement.play().then(() => void audioContext.resume());
if (!sourceRef.current) { if (!sourceRef.current) {
@@ -132,7 +131,7 @@ const InnerCava = (props: InnerKittyProps<typeof Cava>) => {
return () => { return () => {
if (requestRef.current) cancelAnimationFrame(requestRef.current); if (requestRef.current) cancelAnimationFrame(requestRef.current);
}; };
}, [props.cols, props.audio]); }, [props.cols, props.audio, calculateBarHeights]);
return barHeights.map((value, i) => ( return barHeights.map((value, i) => (
<FrequencyBar key={i} value={value} max={255 / 2} height={props.rows} /> <FrequencyBar key={i} value={value} max={255 / 2} height={props.rows} />

View File

@@ -1,41 +1,9 @@
import axios from "axios"; import { useState } from "react";
import { useEffect, useState } from "react";
export const NvimEditor = (props: { source: string | undefined }) => {
const [cache, setCache] = useState(new Map<string, string>());
const [data, setData] = useState<string>();
const [loading, setLoading] = useState(false);
export const NvimEditor = (props: { content: string | undefined }) => {
const [selectedLine, setSelectedLine] = useState(0); const [selectedLine, setSelectedLine] = useState(0);
useEffect(() => { let rows = props.content?.split("\n") ?? [];
if (!props.source) return;
const cached = cache.get(props.source);
if (cached) {
console.log("cache hit");
setData(cached);
return;
}
setLoading(true);
axios
.get<string>(props.source)
.then(({ data }) => {
setData(data);
setLoading(false);
setCache((cache) => {
cache.set(props.source!, data);
return cache;
});
})
.catch(() => {
setLoading(false);
});
}, [props.source]);
let rows = data?.split("\n") ?? [];
// trim end empty lines // trim end empty lines
for (let i = rows.length - 1; i >= 0; i--) { for (let i = rows.length - 1; i >= 0; i--) {
if (rows[i].trim().length === 0) { if (rows[i].trim().length === 0) {
@@ -51,9 +19,7 @@ export const NvimEditor = (props: { source: string | undefined }) => {
} }
} }
return loading ? ( return (
<p>Loading...</p>
) : (
<table> <table>
<tbody> <tbody>
{rows.map((row, i) => ( {rows.map((row, i) => (

View File

@@ -1,10 +1,11 @@
import { ICONS, getIcon } from "~/utils/icons"; import { DEFAULT_ICON } from "~/utils/icons";
import { type Icon } from "~/utils/tree";
export const NvimStatusBar = (props: { export const NvimStatusBar = (props: {
label: string; label: string;
labelColor: string; labelColor: string;
fileIcon?: string; fileIcon?: Icon;
fileName: string; fileName?: string;
}) => ( }) => (
<div className="select-none bg-[#29293c]"> <div className="select-none bg-[#29293c]">
<span className="text-[#272332]" style={{ background: props.labelColor }}> <span className="text-[#272332]" style={{ background: props.labelColor }}>
@@ -15,14 +16,8 @@ export const NvimStatusBar = (props: {
</span> </span>
<span className="bg-[#474353] text-[#373040]">{"\ue0ba"}</span> <span className="bg-[#474353] text-[#373040]">{"\ue0ba"}</span>
<span className="bg-[#373040]">{` ${ <span className="bg-[#373040]">{` ${
getIcon({ props.fileIcon?.char ?? DEFAULT_ICON.char
type: "file", }${props.fileName ?? "Empty"} `}</span>
name: props.fileName,
icon: props.fileIcon,
fileName: props.fileName,
repo: "",
}).char
}${props.fileName} `}</span>
<span className="bg-[#373040] text-[#29293c]">{"\ue0ba"}</span> <span className="bg-[#373040] text-[#29293c]">{"\ue0ba"}</span>
</div> </div>
); );

View File

@@ -0,0 +1,34 @@
import { DEFAULT_ICON } from "~/utils/icons";
import { type Child } from "~/utils/tree";
export const NvimTreeChild = (props: {
child: Child;
y: number;
selected: boolean;
inDirectory: boolean | "last";
onSelect: (y: number) => void;
onOpen: (file: Child) => void;
}) => {
const icon = props.child.icon ?? DEFAULT_ICON;
return (
<li
style={{ background: props.selected ? "#504651" : "" }}
onMouseDown={() => props.onSelect(props.y)}
onDoubleClick={() => props.onOpen(props.child)}
>
{" "}
{props.inDirectory && (
<span className="text-[#5b515b]">
{props.inDirectory === "last" ? "└ " : "│ "}
</span>
)}
<span style={{ color: icon.color }}>{`${icon.char}`}</span>
{props.child.name === "README.md" ? (
<span className="font-bold text-[#d8c5a1]">README.md</span>
) : (
<span>{props.child.name}</span>
)}
</li>
);
};

View File

@@ -1,11 +1,11 @@
import { Directory } from "~/utils/types"; import { type Folder } from "~/utils/tree";
export const NvimTreeDirectory = (props: { export const NvimTreeDirectory = (props: {
directory: Directory; directory: Folder;
y: number; y: number;
selected: boolean; selected: boolean;
onSelect: (y: number) => void; onSelect: (y: number) => void;
onOpen: (directory: Directory) => void; onOpen: (directory: Folder) => void;
}) => ( }) => (
<li <li
className="text-[#a0b6ee]" className="text-[#a0b6ee]"

View File

@@ -1,32 +0,0 @@
import { getIcon } from "~/utils/icons";
import { File } from "~/utils/types";
export const NvimTreeFile = (props: {
file: File;
y: number;
selected: boolean;
inDirectory: boolean | "last";
onSelect: (y: number) => void;
onOpen: (file: File) => void;
}) => (
<li
style={{ background: props.selected ? "#504651" : "" }}
onMouseDown={() => props.onSelect(props.y)}
onDoubleClick={() => props.onOpen(props.file)}
>
{" "}
{props.inDirectory && (
<span className="text-[#5b515b]">
{props.inDirectory === "last" ? "└ " : "│ "}
</span>
)}
<span style={{ color: getIcon(props.file).color }}>{`${
getIcon(props.file).char
}`}</span>
{props.file.name === "README.md" ? (
<span className="font-bold text-[#d8c5a1]">README.md</span>
) : (
<span>{props.file.name}</span>
)}
</li>
);

View File

@@ -1,65 +1,44 @@
import { useApp } from "~/hooks/useApp"; import { useApp } from "~/hooks/useApp";
import { CHAR_HEIGHT, CHAR_WIDTH } from "../../Kitty"; import { CHAR_HEIGHT, CHAR_WIDTH } from "../../Kitty";
import { type ReactNode, useEffect, useState } from "react"; import { type ReactNode, useEffect, useState } from "react";
import { import { type InnerKittyProps } from "~/utils/types";
type File,
type InnerKittyProps,
type RootManifest,
type Directory,
} from "~/utils/types";
import { type Nvim } from ".."; import { type Nvim } from "..";
import { NvimTreeDirectory } from "./NvimTreeDirectory"; import { NvimTreeDirectory } from "./NvimTreeDirectory";
import { NvimTreeFile } from "./NvimTreeFile"; import { NvimTreeChild } from "./NvimTreeChild";
import { assets } from "~/assets";
import { getIcon } from "~/utils/icons";
import {
file,
folder,
project,
sortFiles,
link,
type Child,
} from "~/utils/tree";
const sortFiles = (files: Array<File | Directory>) => const buildTree = () =>
files
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) =>
a.type === "directory" && b.type !== "directory"
? -1
: a.type !== "directory" && b.type === "directory"
? 1
: 0,
);
const manifestToTree = (manifest: RootManifest) =>
sortFiles([ sortFiles([
{ folder(
type: "directory", "links",
name: "links", assets.links.map((l) => link(l.name, l.url, l.icon)),
opened: false, ),
files: manifest.links.map((link) => ({ folder(
type: "link" as const, "projects",
...link, assets.projects.map((p) =>
})), project(p.name, p.content, p.url, p.language, p.private),
}, ),
{ ),
type: "directory", file("README.md", "hey", getIcon("README.md")),
name: "projects",
opened: false,
files: manifest.projects.map((project) => ({
type: "file" as const,
repo: project.name,
fileName: "README.md",
...project,
})),
},
...manifest.files.map((file) => ({
type: "file" as const,
name: file,
repo: "pihkaal",
fileName: file,
})),
]); ]);
export const NvimTree = ( export const NvimTree = (
props: InnerKittyProps<typeof Nvim> & { props: InnerKittyProps<typeof Nvim> & {
onOpen: (file: File) => void; onOpen: (file: Child) => void;
}, },
) => { ) => {
const { rootManifest, activeKitty } = useApp(); const { activeKitty } = useApp();
const [files, setFiles] = useState(manifestToTree(rootManifest)); const [files, setFiles] = useState(buildTree());
const [selectedY, setSelectedY] = useState(0); const [selectedY, setSelectedY] = useState(0);
const tree: Array<ReactNode> = []; const tree: Array<ReactNode> = [];
@@ -67,7 +46,7 @@ export const NvimTree = (
let selectedFile = files[0]; let selectedFile = files[0];
for (const file of files) { for (const file of files) {
if (selectedY === y) selectedFile = file; if (selectedY === y) selectedFile = file;
if (file.type === "directory") { if (file.type === "folder") {
tree.push( tree.push(
<NvimTreeDirectory <NvimTreeDirectory
key={y} key={y}
@@ -83,15 +62,15 @@ export const NvimTree = (
); );
if (file.opened) { if (file.opened) {
file.files.forEach((childFile, i) => { file.children.forEach((child, i) => {
y++; y++;
if (selectedY === y) selectedFile = childFile; if (selectedY === y) selectedFile = child;
tree.push( tree.push(
<NvimTreeFile <NvimTreeChild
key={y} key={y}
file={childFile} child={child}
y={y} y={y}
inDirectory={i === file.files.length - 1 ? "last" : true} inDirectory={i === file.children.length - 1 ? "last" : true}
selected={selectedY === y} selected={selectedY === y}
onSelect={setSelectedY} onSelect={setSelectedY}
onOpen={props.onOpen} onOpen={props.onOpen}
@@ -101,9 +80,9 @@ export const NvimTree = (
} }
} else { } else {
tree.push( tree.push(
<NvimTreeFile <NvimTreeChild
key={y} key={y}
file={file} child={file}
y={y} y={y}
inDirectory={false} inDirectory={false}
selected={selectedY === y} selected={selectedY === y}
@@ -129,7 +108,7 @@ export const NvimTree = (
break; break;
case "Enter": case "Enter":
if (selectedFile.type === "directory") { if (selectedFile.type === "folder") {
selectedFile.opened = !selectedFile.opened; selectedFile.opened = !selectedFile.opened;
setFiles([...files]); setFiles([...files]);
} else { } else {

View File

@@ -5,30 +5,27 @@ import { NvimInput } from "./NvimInput";
import { NvimStatusBar } from "./NvimStatusBar"; import { NvimStatusBar } from "./NvimStatusBar";
import { NvimTree } from "./NvimTree"; import { NvimTree } from "./NvimTree";
import { useState } from "react"; import { useState } from "react";
import { File, InnerKittyProps } from "~/utils/types"; import { type InnerKittyProps } from "~/utils/types";
import { type Child, type Icon } from "~/utils/tree";
export const Nvim = (_props: {}) => { export const Nvim = (_props: unknown) => {
const kitty = useKitty(); const kitty = useKitty();
return kitty && <InnerNvimTree {...kitty} />; return kitty && <InnerNvimTree {...kitty} />;
}; };
const InnerNvimTree = (props: InnerKittyProps<typeof Nvim>) => { const InnerNvimTree = (props: InnerKittyProps<typeof Nvim>) => {
const [activeFile, setActiveFile] = useState<{ const [activeChild, setActiveChild] = useState<{
name: string; name: string;
url: string; content: string;
icon?: string; icon: Icon;
}>(); }>();
const handleOpenFile = (file: File) => { const handleOpenChild = (child: Child) => {
if (file.type === "link") { if (child.type === "link") {
window.open(file.url, "_blank")?.focus(); window.open(child.url, "_blank")?.focus();
} else { } else {
setActiveFile({ setActiveChild(child);
name: file.repo === "pihkaal" ? file.fileName : file.repo,
icon: file.icon,
url: `https://raw.githubusercontent.com/pihkaal/${file.repo}/main/${file.fileName}`,
});
} }
}; };
@@ -43,7 +40,7 @@ const InnerNvimTree = (props: InnerKittyProps<typeof Nvim>) => {
}} }}
> >
<div style={{ gridArea: "1 / 1 / 1 / 2" }}> <div style={{ gridArea: "1 / 1 / 1 / 2" }}>
<NvimTree {...props} onOpen={handleOpenFile} /> <NvimTree {...props} onOpen={handleOpenChild} />
</div> </div>
<div <div
className="overflow-y-auto break-all" className="overflow-y-auto break-all"
@@ -53,14 +50,14 @@ const InnerNvimTree = (props: InnerKittyProps<typeof Nvim>) => {
wordWrap: "break-word", wordWrap: "break-word",
}} }}
> >
<NvimEditor source={activeFile?.url} /> <NvimEditor content={activeChild?.content} />
</div> </div>
<div style={{ gridArea: "2 / 1 / 2 / 3" }}> <div style={{ gridArea: "2 / 1 / 2 / 3" }}>
<NvimStatusBar <NvimStatusBar
label=" NORMAL" label=" NORMAL"
labelColor="#7ea7ca" labelColor="#7ea7ca"
fileIcon={activeFile?.icon} fileIcon={activeChild?.icon}
fileName={activeFile?.name ?? `NvimTree_1`} fileName={activeChild?.name}
/> />
</div> </div>
<div style={{ gridArea: "3 / 1 / 3 / 3" }}> <div style={{ gridArea: "3 / 1 / 3 / 3" }}>

View File

@@ -1,9 +1,7 @@
import { createContext } from "react"; import { createContext } from "react";
import { type RootManifest } from "~/utils/types";
export const AppContext = createContext< export const AppContext = createContext<
| { | {
rootManifest: RootManifest;
activeKitty: string; activeKitty: string;
setActiveKitty: (value: string) => void; setActiveKitty: (value: string) => void;
} }

View File

@@ -1,50 +1,11 @@
import axios from "axios"; import { type ReactNode, useState } from "react";
import { type ReactNode, useState, useEffect } from "react";
import { AppContext } from "~/context/AppContext"; import { AppContext } from "~/context/AppContext";
import { type RootManifest } from "~/utils/types";
export const AppProvider = (props: { children?: ReactNode }) => { export const AppProvider = (props: { children?: ReactNode }) => {
const [activeKitty, setActiveKitty] = useState(":r0:"); const [activeKitty, setActiveKitty] = useState(":r0:");
const [rootManifest, setRootManifest] = useState<RootManifest>({
files: ["README.md", "pubkey.asc"],
projects: [
{
name: "me",
icon: "ts",
},
{
name: "tlock",
icon: "rs",
},
],
links: [
{
name: "github",
url: "https://github.com/pihkaal",
icon: "github",
},
{
name: "instagram",
url: "https://instagram.com/pihkaal",
icon: "instagram",
},
],
});
useEffect(() => {
return;
void axios
.get<RootManifest>(
"https://raw.githubusercontent.com/pihkaal/pihkaal/main/manifest.json",
)
.then((x) => setRootManifest(x.data));
}, []);
if (!rootManifest) return null;
return ( return (
<AppContext.Provider value={{ rootManifest, activeKitty, setActiveKitty }}> <AppContext.Provider value={{ activeKitty, setActiveKitty }}>
{rootManifest && props.children} {props.children}
</AppContext.Provider> </AppContext.Provider>
); );
}; };

View File

@@ -1,48 +1,48 @@
import { File, Icon } from "./types"; import { type Child, type Icon } from "./tree";
export const getIcon = (file: File | string | undefined): Icon => { export const getIcon = (file: Child | string | undefined): Icon => {
if (file === undefined) return ICONS["UNKNOWN"]; if (!file) return DEFAULT_ICON;
let iconName;
if (typeof file === "string") { if (typeof file === "string") {
const parts = file.split("."); const parts = file.split(".");
iconName = parts[parts.length - 1]; const iconName = parts[parts.length - 1];
} else {
iconName = file.icon; return ICONS[EXT_TO_LANGUAGE[iconName]] ?? DEFAULT_ICON;
if (!iconName) {
const parts = file.name.split(".");
iconName = parts[parts.length - 1];
}
} }
if (!ICONS[iconName]) iconName = "UNKNOWN"; return file.icon ?? DEFAULT_ICON;
return ICONS[iconName]; };
export const EXT_TO_LANGUAGE: Record<string, string> = {
asc: "Key",
md: "Markdown",
}; };
export const ICONS: Record<string, Icon> = { export const ICONS: Record<string, Icon> = {
md: { Markdown: {
char: " ", char: " ",
color: "#89bafa", color: "#89bafa",
}, },
asc: { Key: {
char: "󰷖 ", char: "󰷖 ",
color: "#f9e2af", color: "#f9e2af",
}, },
ts: { TypeScript: {
char: " ", char: " ",
color: "#4d86a2", color: "#4d86a2",
}, },
rs: { Rust: {
char: " ", char: " ",
color: "#be8f78", color: "#be8f78",
}, },
instagram: { Instagram: {
char: " ", char: " ",
color: "#e1306c", color: "#e1306c",
}, },
github: { Github: {
char: "󰊤 ", char: "󰊤 ",
color: "#ffffff", color: "#ffffff",
}, },
UNKNOWN: { char: "󰈚 ", color: "#f599ae" },
}; };
export const DEFAULT_ICON = { char: "󰈚 ", color: "#f599ae" };

90
src/utils/tree.ts Normal file
View File

@@ -0,0 +1,90 @@
import { DEFAULT_ICON, ICONS } from "./icons";
export type Icon = {
char: string;
color: string;
};
export type Project = {
type: "project";
name: string;
url: string;
private: boolean;
content: string;
icon: Icon;
};
export type File = {
type: "file";
name: string;
content: string;
icon: Icon;
};
export type Link = {
type: "link";
name: string;
url: string;
icon: Icon;
};
export type Folder = {
name: string;
type: "folder";
children: Array<Child>;
opened: boolean;
};
export type Child = Link | Project | File;
export const sortFiles = (files: Array<Folder | Child>) =>
files
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) =>
a.type === "folder" && b.type !== "folder"
? -1
: a.type !== "folder" && b.type === "folder"
? 1
: 0,
);
export const folder = (name: string, children: Array<Child>): Folder => ({
type: "folder",
name,
opened: false,
children,
});
export const link = (name: string, url: string, icon: Icon): Link => ({
type: "link",
name,
url,
icon,
});
export const file = (name: string, content: string, icon: Icon): File => ({
type: "file",
name,
content,
icon,
});
export const project = (
name: string,
content: string,
url: string,
language: string,
priv: boolean,
): Project => ({
type: "project",
name,
content,
url,
icon: ICONS[language] ?? DEFAULT_ICON,
private: priv,
});
export const icon = (char: string, color: string): Icon => ({
char,
color,
});

View File

@@ -6,39 +6,3 @@ export type Prettify<T> = NonNullable<{ [K in keyof T]: T[K] }>;
export type InnerKittyProps<T extends (...args: any[]) => any> = Prettify< export type InnerKittyProps<T extends (...args: any[]) => any> = Prettify<
Parameters<T>[0] & KittyContextProps Parameters<T>[0] & KittyContextProps
>; >;
export type RootManifest = {
files: Array<string>;
projects: Array<{
name: string;
icon: string;
}>;
links: Array<{
name: string;
url: string;
icon: string;
}>;
};
export type Icon = {
char: string;
color: string;
};
export type File = {
name: string;
} & (
| {
type: "link";
url: string;
icon: string;
}
| { type: "file"; repo: string; fileName: string; icon?: string }
);
export type Directory = {
name: string;
type: "directory";
files: Array<File>;
opened: boolean;
};