website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


0227637 — pyrossh 2025-11-23T15:43:18+05:30
update file icons

Generated files not shown: bun.lock

package.json CHANGED
@@ -28,16 +28,18 @@
28
28
  "timeago.js": "^4.0.2"
29
29
  },
30
30
  "devDependencies": {
31
- "@types/bun": "^1.3.2",
32
31
  "@astrojs/rss": "^4.0.12",
33
32
  "@astrojs/sitemap": "^3.4.1",
34
33
  "@aws-sdk/client-s3": "^3.932.0",
35
34
  "@aws-sdk/client-sts": "^3.932.0",
36
35
  "@iconify-json/material-symbols": "^1.2.39",
37
36
  "@iconify-json/mdi": "^1.2.3",
37
+ "@iconify-json/vscode-icons": "^1.2.36",
38
38
  "@playwright/test": "^1.52.0",
39
39
  "@tauri-apps/cli": "^2.9.1",
40
+ "@types/bun": "^1.3.2",
40
41
  "alchemy": "^0.77.5",
41
- "diff2html": "^3.4.52"
42
+ "diff2html": "^3.4.52",
43
+ "vscode-icons": "https://github.com/vscode-icons/vscode-icons.git"
42
44
  }
43
45
  }
src/components/RecursiveList.astro CHANGED
@@ -1,15 +1,17 @@
1
1
  ---
2
2
  import { Icon } from "astro-icon/components";
3
+ import allIcons from "@iconify-json/vscode-icons/icons.json" assert { type: "json" };
3
4
  import prettyBytes from "pretty-bytes";
5
+ import { resolveFileIcon } from "@/utils/files";
4
6
 
5
7
  interface FileNode {
6
8
  name: string;
7
9
  path: string;
8
10
  isDirectory: boolean;
11
+ size: number;
12
+ ext: string;
13
+ absolutePath: string;
9
14
  children?: FileNode[];
10
- size?: number;
11
- ext?: string;
12
- absolutePath?: string;
13
15
  }
14
16
 
15
17
  interface Props {
@@ -46,11 +48,19 @@ const { items, repoId, depth = 0 } = Astro.props;
46
48
  </li>
47
49
  );
48
50
  } else {
51
+ const iconName = resolveFileIcon(item.name, item.ext);
52
+ const fileType = allIcons.icons[`file-type-${iconName}`]
53
+ ? `file-type-${iconName}`
54
+ : "default-file";
49
55
  return (
50
56
  <li class="file" style={`padding-left: ${depth * 1}rem;`}>
51
57
  <div class="file-content">
52
58
  <div class="file-name">
59
+ <Icon
60
+ name={`vscode-icons:${fileType}`}
61
+ size="24px"
53
- <Icon name="mdi:file" size="24px" color="hsl(0deg 75% 75%)" />
62
+ color="hsl(0deg 75% 75%)"
63
+ />
54
64
  <a href={`/repos/${repoId}/files/${item.path}`} rel="nofollow">
55
65
  {item.name}
56
66
  </a>
src/layouts/FileLayout.astro CHANGED
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  import { Icon } from "astro-icon/components";
3
3
  import { type CollectionEntry } from "astro:content";
4
+ import allIcons from "@iconify-json/vscode-icons/icons.json" assert { type: "json" };
4
5
  import RepoLayout from "@/layouts/RepoLayout.astro";
5
- import type { FileNode } from "@/utils/files";
6
+ import { resolveFileIcon, type FileNode } from "@/utils/files";
6
7
 
7
8
  type Props = {
8
9
  repo: CollectionEntry<"repos">;
@@ -11,6 +12,10 @@ type Props = {
11
12
 
12
13
  const { repo, file } = Astro.props;
13
14
  const { pathname } = Astro.url;
15
+ const iconName = resolveFileIcon(file.name, file.ext);
16
+ const fileType = allIcons.icons[`file-type-${iconName}`]
17
+ ? `file-type-${iconName}`
18
+ : "default-file";
14
19
  ---
15
20
 
16
21
  <RepoLayout repo={repo}>
@@ -19,8 +24,11 @@ const { pathname } = Astro.url;
19
24
  <div>
20
25
  <hr />
21
26
  <div class="title">
27
+ <Icon
28
+ name={`vscode-icons:${fileType}`}
29
+ size="24px"
22
- <span>file:</span>
30
+ color="hsl(0deg 75% 75%)"
23
- <Icon name="mdi:file" size="24px" color="hsl(0deg 75% 75%)" />
31
+ />
24
32
  <h3>{file.name}</h3>
25
33
  </div>
26
34
  <hr />
@@ -106,11 +114,9 @@ const { pathname } = Astro.url;
106
114
  font-size: 1.1rem;
107
115
  font-weight: 500;
108
116
  }
117
+
109
- span {
118
+ svg[data-icon] {
110
119
  font-weight: 600;
111
120
  margin-right: 4px;
112
121
  }
113
- h3 {
114
- text-decoration: underline;
115
- }
116
122
  </style>
src/pages/repos/[...repoId]/files/[...file]/index.astro CHANGED
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  import fs from "fs/promises";
3
- import path from "path";
4
3
  import { type CollectionEntry, getCollection } from "astro:content";
5
4
  import { Code } from "astro-expressive-code/components";
6
5
  import FileLayout from "@/layouts/FileLayout.astro";
@@ -12,15 +11,15 @@ export async function getStaticPaths() {
12
11
  const routes = [];
13
12
  for (const repo of repos) {
14
13
  const files = await getFiles(repo.data.absolutePath);
14
+ for (const file of files) {
15
- routes.push(
15
+ routes.push({
16
- ...files.map((file) => ( {
17
16
  params: { repoId: repo.id, file: file.name },
18
17
  props: {
19
18
  repo: repo,
20
19
  file: file,
21
20
  },
22
- }))
21
+ });
23
- );
22
+ }
24
23
  }
25
24
  return routes;
26
25
  }
@@ -32,8 +31,7 @@ type Props = {
32
31
 
33
32
  const { repo, file } = Astro.props;
34
33
  const contentBuffer = await fs.readFile(file.absolutePath);
35
- const ext = path.extname(file.name).replace(".", "");
36
- const isBinary = ["apk", "dex", "ap_", "jar", "fnt"].includes(ext);
34
+ const isBinary = ["apk", "dex", "ap_", "jar", "fnt"].includes(file.ext);
37
35
  const isLarge = file.size > 1024 * 512; // 512KB
38
36
  const isImage = [
39
37
  "png",
@@ -46,7 +44,7 @@ const isImage = [
46
44
  "icns",
47
45
  "curve",
48
46
  "atlas",
49
- ].includes(ext);
47
+ ].includes(file.ext);
50
48
  ---
51
49
 
52
50
  <FileLayout repo={repo} file={file}>
@@ -56,7 +54,7 @@ const isImage = [
56
54
  <p>The file is too large to be displayed ({file.size} bytes).</p>
57
55
  ) : isImage ? (
58
56
  <img
59
- src={`data:image/${ext};base64,${Buffer.from(contentBuffer).toString("base64")}`}
57
+ src={`data:image/${file.ext};base64,${Buffer.from(contentBuffer).toString("base64")}`}
60
58
  alt={file.path}
61
59
  />
62
60
  ) : (
@@ -65,10 +63,10 @@ const isImage = [
65
63
  Buffer.from(contentBuffer).toString("utf-8") ||
66
64
  "No content to display."
67
65
  }
68
- lang={ext || "txt"}
66
+ lang={file.ext || "txt"}
69
67
  wrap
70
68
  />
71
69
  )
72
70
  }
73
71
  </div>
74
- </FileLayout>
72
+ </FileLayout>
src/utils/commit.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import Diff2html from "diff2html";
2
2
  import fs from "fs/promises";
3
+ import path from "path";
3
4
  import { simpleGit } from "simple-git";
4
5
  import { type FileNode } from "./files";
5
6
 
@@ -38,10 +39,13 @@ export const getFiles = async (repoPath: string): Promise<FileNode[]> => {
38
39
  for (const p of paths.split("\n").filter((p) => p.length > 0)) {
39
40
  try {
40
41
  const stat = await fs.stat(`${repoPath}/${p}`);
42
+ const ext = path.extname(p).replace(".", "");
41
43
  files.push({
42
44
  name: p,
45
+ ext,
46
+ size: stat.size,
47
+ isDirectory: stat.isDirectory(),
43
48
  absolutePath: `${repoPath}/${p}`,
44
- ...stat,
45
49
  })
46
50
  } catch (e) {
47
51
  // Ignore files not found
src/utils/files.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import fs from "fs/promises";
2
+ import { extensions as folderExtensions } from "vscode-icons/src/iconsManifest/supportedFolders.ts";
3
+ import { extensions as fileExtensions } from "vscode-icons/src/iconsManifest/supportedExtensions.ts";
2
4
 
3
5
  export interface FileNode {
4
6
  name: string;
@@ -73,4 +75,34 @@ export const checkFileExists = async (filePath: string) => {
73
75
  } catch (error) {
74
76
  return false; // File does not exist or cannot be accessed
75
77
  }
76
- }
78
+ }
79
+
80
+ export const resolveFolderIcon = (foldername: string) => {
81
+ for (const item of folderExtensions.supported) {
82
+ if (item.extensions?.includes(foldername)) {
83
+ return item.icon;
84
+ }
85
+ }
86
+ return folderExtensions.default.folder?.icon;
87
+ }
88
+
89
+ export const resolveFileIcon = (filename: string, fileExt: string) => {
90
+ for (const item of fileExtensions.supported) {
91
+ if (item.filename && item.extensions?.includes(filename)) {
92
+ return item.icon;
93
+ }
94
+ }
95
+ for (const item of fileExtensions.supported) {
96
+ if (item.extensions?.includes(fileExt)) {
97
+ return item.icon;
98
+ }
99
+ for (const lang of item.languages ?? []) {
100
+ if (lang?.knownExtensions?.includes(fileExt)) {
101
+ return item.icon;
102
+ }
103
+ }
104
+ }
105
+ return fileExtensions.default.file?.icon;
106
+ }
107
+
108
+ console.log(resolveFileIcon('examples/public/index.html', 'html'));