From 96207863a9393b745b2fcf6e7d6a04a0ced18c8b Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Fri, 31 May 2024 22:56:08 +0200 Subject: [PATCH] feat(nvim): improve tree and file switching, and implements links --- package.json | 1 - pnpm-lock.yaml | 34 --- src/App.tsx | 41 ++-- src/components/Nvim/NvimEditor.tsx | 4 +- src/components/Nvim/NvimTree.tsx | 199 ------------------ .../Nvim/NvimTree/NvimTreeDirectory.tsx | 25 +++ src/components/Nvim/NvimTree/NvimTreeFile.tsx | 41 ++++ src/components/Nvim/NvimTree/index.tsx | 178 ++++++++++++++++ src/components/Nvim/index.tsx | 62 ++---- src/providers/AppProvider.tsx | 23 +- src/utils/icons.ts | 29 +++ src/utils/types.ts | 33 ++- 12 files changed, 361 insertions(+), 309 deletions(-) delete mode 100644 src/components/Nvim/NvimTree.tsx create mode 100644 src/components/Nvim/NvimTree/NvimTreeDirectory.tsx create mode 100644 src/components/Nvim/NvimTree/NvimTreeFile.tsx create mode 100644 src/components/Nvim/NvimTree/index.tsx create mode 100644 src/utils/icons.ts diff --git a/package.json b/package.json index ff76f29..91691ee 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "music-metadata-browser": "^2.5.10", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "^6.21.3", "tailwind-merge": "^2.2.1", "vite-tsconfig-paths": "^4.3.1", "zod": "^3.22.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b5db4d..40fa67e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) - react-router-dom: - specifier: ^6.21.3 - version: 6.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) tailwind-merge: specifier: ^2.2.1 version: 2.2.1 @@ -335,10 +332,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@remix-run/router@1.14.2': - resolution: {integrity: sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==} - engines: {node: '>=14.0.0'} - '@rollup/plugin-inject@5.0.5': resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} @@ -1688,19 +1681,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-router-dom@6.21.3: - resolution: {integrity: sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - - react-router@6.21.3: - resolution: {integrity: sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' - react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -2248,8 +2228,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@remix-run/router@1.14.2': {} - '@rollup/plugin-inject@5.0.5(rollup@4.9.6)': dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.9.6) @@ -3688,18 +3666,6 @@ snapshots: react-is@16.13.1: {} - react-router-dom@6.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): - dependencies: - '@remix-run/router': 1.14.2 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-router: 6.21.3(react@18.2.0) - - react-router@6.21.3(react@18.2.0): - dependencies: - '@remix-run/router': 1.14.2 - react: 18.2.0 - react@18.2.0: dependencies: loose-envify: 1.4.0 diff --git a/src/App.tsx b/src/App.tsx index 04d1714..8282ba2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,35 +3,32 @@ import { Kitty } from "./components/Kitty"; import { AppProvider } from "./providers/AppProvider"; import { Music } from "./components/Music"; import { Nvim } from "./components/Nvim"; -import { BrowserRouter } from "react-router-dom"; export default function App() { const [loggedIn, setLoggedIn] = useState(false); return ( - -
- {loggedIn ? ( -
- - - +
+ {loggedIn ? ( +
+ + + - -
- ) : ( -
- -
- )} -
- + +
+ ) : ( +
+ +
+ )} +
); } diff --git a/src/components/Nvim/NvimEditor.tsx b/src/components/Nvim/NvimEditor.tsx index 21a0ecc..4fad7d3 100644 --- a/src/components/Nvim/NvimEditor.tsx +++ b/src/components/Nvim/NvimEditor.tsx @@ -1,3 +1,3 @@ -export const NvimEditor = (props: { data: string }) => ( -
{props.data}
+export const NvimEditor = (props: { source: string | undefined }) => ( +
{props.source}
); diff --git a/src/components/Nvim/NvimTree.tsx b/src/components/Nvim/NvimTree.tsx deleted file mode 100644 index ec1ed95..0000000 --- a/src/components/Nvim/NvimTree.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { useApp } from "~/hooks/useApp"; -import { CHAR_HEIGHT, CHAR_WIDTH } from "../Kitty"; -import { type ReactNode, useEffect, useState, useCallback } from "react"; -import { type InnerKittyProps } from "~/utils/types"; -import { type Nvim } from "."; -import { useNavigate } from "react-router-dom"; - -type FileIcon = { - char: string; - color: string; -}; - -type File = { - name: string; -} & ( - | { type: "file"; directory?: File & { type: "directory" } } - | { type: "directory"; files: Array; folded: boolean } -); - -const FILE_ICONS: Record = { - md: { - char: " ", - color: "#89bafa", - }, - asc: { - char: "󰷖 ", - color: "#f9e2af", - }, - UNKNOWN: { char: "󰈚 ", color: "#f599ae" }, -}; - -const sortFiles = (files: Array) => - 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, - ); - -export const NvimTree = (props: InnerKittyProps) => { - const { rootManifest, activeKitty } = useApp(); - const navigate = useNavigate(); - - const [files, setFiles] = useState>( - sortFiles([ - { - type: "directory", - name: "projects", - files: rootManifest.projects, - folded: true, - }, - ...rootManifest.files.map((name) => ({ type: "file" as const, name })), - ]), - ); - const [selected, setSelected] = useState(files.length - 1); - - const handleOpen = (file: File) => { - if (file.type === "directory") { - file.folded = !file.folded; - } else { - let filePath = ""; - if (file.directory) { - filePath += `${file.directory.name}/`; - } - - navigate(`?view=${filePath}${file.name}`); - } - - setFiles([...files]); - }; - - const tree: Array = []; - let y = 0; - let selectedFile: File; - for (const fileOrDir of files) { - if (y === selected) selectedFile = fileOrDir; - if (fileOrDir.type === "directory") { - const dy = y; - tree.push( -
  • setSelected(dy)} - onDoubleClick={() => handleOpen(fileOrDir)} - > - {fileOrDir.folded ? ( - <> - {" "} - - ) : ( - <>  - )} - {fileOrDir.name} -
  • , - ); - if (!fileOrDir.folded) { - for (let i = 0; i < fileOrDir.files.length; i++) { - y++; - if (y === selected) - selectedFile = { - type: "file", - name: fileOrDir.files[i], - directory: fileOrDir, - }; - - const icon = FILE_ICONS.UNKNOWN; - const fy = y; - tree.push( -
  • setSelected(fy)} - onDoubleClick={() => - handleOpen({ - type: "file", - name: fileOrDir.files[i], - directory: fileOrDir, - }) - } - > - {" "} - - {i === fileOrDir.files.length - 1 ? "└ " : "│ "} - - {`${icon.char}`} - {fileOrDir.files[i]} -
  • , - ); - } - } - } else { - const parts = fileOrDir.name.split("."); - let extension = parts[parts.length - 1]; - if (!FILE_ICONS[extension]) extension = "UNKNOWN"; - - const icon = FILE_ICONS[extension]; - - const fy = y; - tree.push( -
  • setSelected(fy)} - onDoubleClick={() => handleOpen(fileOrDir)} - > - {" "} - {`${icon.char}`} - {fileOrDir.name === "README.md" ? ( - README.md - ) : ( - {fileOrDir.name} - )} -
  • , - ); - } - y++; - } - - useEffect(() => { - const onScroll = (event: KeyboardEvent) => { - if (activeKitty !== props.id) return; - switch (event.key) { - case "ArrowUp": - setSelected((x) => Math.max(0, x - 1)); - break; - - case "ArrowDown": - setSelected((x) => Math.min(y - 1, x + 1)); - break; - - case "Enter": - handleOpen(selectedFile); - break; - } - }; - - window.addEventListener("keydown", onScroll); - - return () => { - window.removeEventListener("keydown", onScroll); - }; - }); - - return ( -
    -
      - {tree} -
    -
    - ); -}; diff --git a/src/components/Nvim/NvimTree/NvimTreeDirectory.tsx b/src/components/Nvim/NvimTree/NvimTreeDirectory.tsx new file mode 100644 index 0000000..2461303 --- /dev/null +++ b/src/components/Nvim/NvimTree/NvimTreeDirectory.tsx @@ -0,0 +1,25 @@ +import { Directory } from "~/utils/types"; + +export const NvimTreeDirectory = (props: { + directory: Directory; + y: number; + selected: boolean; + onSelect: (y: number) => void; + onOpen: (directory: Directory) => void; +}) => ( +
  • props.onSelect(props.y)} + onDoubleClick={() => props.onOpen(props.directory)} + > + {props.directory.opened ? ( + <>  + ) : ( + <> + {" "} + + )} + {props.directory.name} +
  • +); diff --git a/src/components/Nvim/NvimTree/NvimTreeFile.tsx b/src/components/Nvim/NvimTree/NvimTreeFile.tsx new file mode 100644 index 0000000..1f5a22f --- /dev/null +++ b/src/components/Nvim/NvimTree/NvimTreeFile.tsx @@ -0,0 +1,41 @@ +import { ICONS } 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; +}) => { + let iconName = props.file.icon; + if (!iconName) { + const parts = props.file.name.split("."); + iconName = parts[parts.length - 1]; + } + + if (!ICONS[iconName]) iconName = "UNKNOWN"; + const icon = ICONS[iconName]; + + return ( +
  • props.onSelect(props.y)} + onDoubleClick={() => props.onOpen(props.file)} + > + {" "} + {props.inDirectory && ( + + {props.inDirectory === "last" ? "└ " : "│ "} + + )} + {`${icon.char}`} + {props.file.name === "README.md" ? ( + README.md + ) : ( + {props.file.name} + )} +
  • + ); +}; diff --git a/src/components/Nvim/NvimTree/index.tsx b/src/components/Nvim/NvimTree/index.tsx new file mode 100644 index 0000000..5c40a6e --- /dev/null +++ b/src/components/Nvim/NvimTree/index.tsx @@ -0,0 +1,178 @@ +import { useApp } from "~/hooks/useApp"; +import { CHAR_HEIGHT, CHAR_WIDTH } from "../../Kitty"; +import { type ReactNode, useEffect, useState } from "react"; +import { + type File, + type InnerKittyProps, + type RootManifest, + type Directory, +} from "~/utils/types"; +import { type Nvim } from ".."; +import { NvimTreeDirectory } from "./NvimTreeDirectory"; +import { NvimTreeFile } from "./NvimTreeFile"; +import { promiseHooks } from "v8"; + +const sortFiles = (files: Array) => + 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([ + { + type: "directory", + name: "links", + opened: false, + files: manifest.links.map((link) => ({ + type: "link" as const, + ...link, + })), + }, + { + type: "directory", + 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 = ( + props: InnerKittyProps & { + onOpen: (file: File) => void; + }, +) => { + const { rootManifest, activeKitty } = useApp(); + + const [files, setFiles] = useState(manifestToTree(rootManifest)); + const [selectedY, setSelectedY] = useState(0); + + const tree: Array = []; + let y = 0; + let selectedFile = files[0]; + for (const file of files) { + if (selectedY === y) selectedFile = file; + if (file.type === "directory") { + tree.push( + { + directory.opened = !directory.opened; + setFiles([...files]); + }} + />, + ); + + if (file.opened) { + file.files.forEach((childFile, i) => { + y++; + if (selectedY === y) selectedFile = childFile; + tree.push( + , + ); + }); + } + } else { + tree.push( + , + ); + } + + y++; + } + + useEffect(() => { + const onScroll = (event: KeyboardEvent) => { + if (activeKitty !== props.id) return; + switch (event.key) { + case "ArrowUp": + setSelectedY((x) => Math.max(0, x - 1)); + break; + + case "ArrowDown": + setSelectedY((x) => Math.min(y - 1, x + 1)); + break; + + case "Enter": + if (selectedFile.type === "directory") { + selectedFile.opened = !selectedFile.opened; + setFiles([...files]); + } else { + props.onOpen(selectedFile); + } + break; + } + }; + + window.addEventListener("keydown", onScroll); + + return () => { + window.removeEventListener("keydown", onScroll); + }; + }); + + return ( +
    +
      + {tree} +
    +
    + ); +}; + +type FileIcon = { + char: string; + color: string; +}; + +const FILE_ICONS: Record = { + md: { + char: " ", + color: "#89bafa", + }, + asc: { + char: "󰷖 ", + color: "#f9e2af", + }, + UNKNOWN: { char: "󰈚 ", color: "#f599ae" }, +}; diff --git a/src/components/Nvim/index.tsx b/src/components/Nvim/index.tsx index 369c7e0..7b4d859 100644 --- a/src/components/Nvim/index.tsx +++ b/src/components/Nvim/index.tsx @@ -4,59 +4,23 @@ import { NvimEditor } from "./NvimEditor"; import { NvimInput } from "./NvimInput"; import { NvimStatusBar } from "./NvimStatusBar"; import { NvimTree } from "./NvimTree"; -import { useLocation, useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; -import axios from "axios"; - -const fetchData = async ( - branch: string, - repo: string, - file: string, -): Promise => { - try { - const response = await axios.get( - `https://raw.githubusercontent.com/pihkaal/${repo}/${branch}/${file}`, - ); - return response.data; - } catch { - return null; - } -}; +import { useState } from "react"; +import { File } from "~/utils/types"; export const Nvim = (_props: {}) => { const kitty = useKitty(); - const location = useLocation(); - const navigate = useNavigate(); - const [data, setData] = useState(); + const [activeFile, setActiveFile] = useState(); - useEffect(() => { - const params = new URLSearchParams(location.search); - const view = params.get("view"); - if (!view) { - navigate("?view=README.md"); - return; + const handleOpenFile = (file: File) => { + if (file.type === "link") { + window.open(file.url, "_blank")?.focus(); + } else { + setActiveFile( + `https://raw.githubusercontent.com/pihkaal/${file.repo}/main/${file.fileName}`, + ); } - - const path = view.split("/"); - if (path.length === 1) { - path.splice(0, 0, "pihkaal"); - } - const repo = path[0]!; - const file = path[1]!; - - void (async () => { - const data = - (await fetchData("main", repo, file)) ?? - (await fetchData("dev", repo, file)); - if (!data) { - navigate("?view=README.md"); - return; - } - - setData(data); - })(); - }, [location, navigate]); + }; return (
    { {kitty && ( <>
    - +
    - +
    { const [activeKitty, setActiveKitty] = useState(":r0:"); const [rootManifest, setRootManifest] = useState({ files: ["README.md", "pubkey.asc"], - projects: ["me", "tlock"], + 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(() => { diff --git a/src/utils/icons.ts b/src/utils/icons.ts new file mode 100644 index 0000000..a2ef2bd --- /dev/null +++ b/src/utils/icons.ts @@ -0,0 +1,29 @@ +import { Icon } from "./types"; + +export const ICONS: Record = { + md: { + char: " ", + color: "#89bafa", + }, + asc: { + char: "󰷖 ", + color: "#f9e2af", + }, + ts: { + char: " ", + color: "#4d86a2", + }, + rs: { + char: " ", + color: "#be8f78", + }, + instagram: { + char: " ", + color: "#e1306c", + }, + github: { + char: "󰊤 ", + color: "#ffffff", + }, + UNKNOWN: { char: "󰈚 ", color: "#f599ae" }, +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index 494de6a..d758d4c 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -9,5 +9,36 @@ export type InnerKittyProps any> = Prettify< export type RootManifest = { files: Array; - projects: Array; + 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; + opened: boolean; };