diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 046e00a..239de9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ npm run lint ### 代码格式化 -我们使用 `EditorConfig` 进行一致的格式化,请确保你的编辑器安装了相应插件以自动应用格式规则。 +我们使用 `EditorConfig` 进行统一格式化,请确保你的编辑器安装了相应插件以自动应用格式规则。 ## 提交 issue @@ -104,7 +104,7 @@ npm run lint ### [enhancement] -- 清晰的增强描述,杜绝抽象概念 +- 清晰地描述增强点,杜绝抽象概念 - 建议的实现逻辑 ### [feature] @@ -115,8 +115,6 @@ npm run lint > 推荐保持与历史 issue 一致的标题格式 ---- - ### 许可证 通过贡献,您同意您的贡献将按照与项目相同的许可证 [AGPL-3.0](LICENSE) 进行许可。 diff --git a/README.md b/README.md index 9754ac4..2e4ca9f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -

- Repo-Viewer -

- -

- 基于 Material Design 3设计风格的 GitHub仓库浏览应用 -    - - Ask DeepWiki - -

+
+

Repo-Viewer

+
+ +
+

+ 基于 Material Design 3设计风格的 GitHub仓库浏览应用 +    + + Ask DeepWiki + +

+
![Preview Dark](docs/image/dark.png) @@ -20,9 +22,17 @@ +## 为 Repo Viewer 贡献代码 + +此项目已进入稳定阶段,本人将不再花过多精力维护。若发现已知的问题,欢迎任何形式的贡献!无论是修复错误、改进功能,还是提升代码质量,我们都非常欢迎您的参与。 + +> 此组织的所有成员均有管理员权限,若不想提交 Pull Request,直接推送代码是被允许的。 +> +> 但在提交贡献前,推荐阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 以了解建议的代码规范和提交流程。 + ## 主要功能 -- 📁 **仓库浏览**:直观的文件结构导航,同时提供首页文件和文件夹排除选项. +- 📁 **仓库浏览**:直观的文件结构导航,同时提供首页文件与文件夹排除选项. - 🔎 **文件搜索**:支持基于自建索引和 Github API 的快速文件搜索,可按分支、路径前缀和扩展名过滤. - 📄 **文件预览**:多种文件格式预览,目前支持 `Markdown`、 `PDF` 和 `图片`. - ⬇️ **文件下载**:可下载单个文件或整个文件夹. diff --git a/package-lock.json b/package-lock.json index 514fee8..b22f1e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "repo-viewer", - "version": "1.3.9", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "repo-viewer", - "version": "1.3.9", + "version": "1.4.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/package.json b/package.json index 5d419f0..1c2eaf7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "repo-viewer", "private": true, - "version": "1.3.9", + "version": "1.4.0", "type": "module", "engines": { "node": "24.x" @@ -10,7 +10,7 @@ "dev": "vite", "build": "tsc -p scripts/tsconfig.json && node scripts/dist/generateInitialContent.js && node scripts/dist/generateDocfindIndex.js && tsc && vite build", "generate:index": "tsc -p scripts/tsconfig.json && node scripts/dist/generateDocfindIndex.js", - "install-deps": "npm install --legacy-peer-deps", + "install-deps": "npm install", "lint": "eslint . --ext ts,tsx", "preview": "vite preview" }, diff --git a/scripts/generateDocfindIndex.ts b/scripts/generateDocfindIndex.ts index e8bf0e1..563b122 100644 --- a/scripts/generateDocfindIndex.ts +++ b/scripts/generateDocfindIndex.ts @@ -402,7 +402,7 @@ const resolveBranchRef = async (repoPath: string, branch: string): Promise(url: string): Promise => { if (!response.ok) { throw new Error(`Request failed: ${response.status} ${response.statusText}`); } - return response.json() as Promise; + return (await response.json()) as T; }; const fetchText = async (url: string): Promise => { @@ -139,7 +139,9 @@ const run = async (): Promise => { const contents = await fetchJson(contentsUrl); if (!Array.isArray(contents)) { - throw new Error("Unexpected contents response"); + console.warn("[hydration] Unexpected contents response, writing null payload."); + await writeOutput(null); + return; } const contentItems = contents.filter(isRecord); diff --git a/scripts/src/generated/initialContent.ts b/scripts/src/generated/initialContent.ts index 59b4eec..cb0ff5c 100644 --- a/scripts/src/generated/initialContent.ts +++ b/scripts/src/generated/initialContent.ts @@ -1,3 +1 @@ -import type { InitialContentHydrationPayload } from "@/types"; - -export const initialContentPayload: InitialContentHydrationPayload | null = null; +export {}; diff --git a/src/components/file/FileList.tsx b/src/components/file/FileList.tsx index d4ae429..2322d1d 100644 --- a/src/components/file/FileList.tsx +++ b/src/components/file/FileList.tsx @@ -5,7 +5,7 @@ import { AutoSizer } from "react-virtualized-auto-sizer"; import AlphabetIndex from "./AlphabetIndex"; import { RowComponent } from "./FileListRow"; import { FILE_ITEM_CONFIG, LIST_HEIGHT_CONFIG } from "./utils/fileListConfig"; -import { calculateLayoutMetrics } from "./utils/fileListLayout"; +import { calculateLayoutMetrics, getListPadding, getRowMetrics } from "./utils/fileListLayout"; import type { VirtualListItemData, FileListLayoutMetrics } from "./utils/types"; import type { GitHubContent } from "@/types"; import { theme } from "@/utils"; @@ -70,17 +70,8 @@ const FileList = React.memo( // 计算每个文件项的高度(包括间距) // 这个计算需要与 FileListItem 的实际高度保持一致 - const rowHeight = useMemo(() => { - // 基础高度 - const baseHeight = isSmallScreen - ? FILE_ITEM_CONFIG.baseHeight.xs - : FILE_ITEM_CONFIG.baseHeight.sm; - - // 行间距(上下各分一半) - const rowGap = FILE_ITEM_CONFIG.spacing.marginBottom; - - // 计算总高度:基础高度 + 行间距 - return baseHeight + rowGap; + const { rowHeight, rowPaddingBottom } = useMemo(() => { + return getRowMetrics(isSmallScreen); }, [isSmallScreen]); /** @@ -200,6 +191,7 @@ const FileList = React.memo( isScrolling, scrollSpeed, highlightedIndex, + rowPaddingBottom, }), [ contents, @@ -214,26 +206,17 @@ const FileList = React.memo( isScrolling, scrollSpeed, highlightedIndex, + rowPaddingBottom, ], ); // 简化的列表内边距计算 - const listPadding = useMemo((): { paddingTop: number; paddingBottom: number } => { - // 非滚动模式:使用固定的对称内边距,稍微增加一点 - if (!needsScrolling) { - const padding = isSmallScreen ? 16 : 20; - return { - paddingTop: padding - 4, - paddingBottom: padding, - }; - } - - // 滚动模式:使用较小的内边距 - return { - paddingTop: 0, - paddingBottom: 8, - }; - }, [needsScrolling, isSmallScreen]); + // 列表内边距规则集中管理,区分滚动与非滚动模式。 + const listPadding = useMemo( + (): { paddingTop: number; paddingBottom: number } => + getListPadding(needsScrolling, isSmallScreen), + [needsScrolling, isSmallScreen], + ); // 处理滚动事件(监听列表容器) React.useEffect(() => { diff --git a/src/components/file/FileListItem.tsx b/src/components/file/FileListItem.tsx index 2e8b72b..da6b67f 100644 --- a/src/components/file/FileListItem.tsx +++ b/src/components/file/FileListItem.tsx @@ -17,7 +17,8 @@ import { Download as DownloadIcon, Cancel as CancelIcon, } from "@mui/icons-material"; -import { file, logger, theme as themeUtils } from "@/utils"; +import { logger, theme as themeUtils } from "@/utils"; +import { fileExtensionIcons } from "@/utils/files/fileHelpers"; import type { GitHubContent } from "@/types"; import { getFeaturesConfig } from "@/config"; import { useI18n } from "@/contexts/I18nContext"; @@ -143,7 +144,7 @@ const FileListItem = memo( const extension = item.name.split('.').pop()?.toLowerCase(); if (typeof extension === "string" && extension.length > 0) { - const icon = file.fileExtensionIcons[extension]; + const icon = fileExtensionIcons[extension]; if (icon !== undefined) { return icon; } diff --git a/src/components/file/FileListRow.tsx b/src/components/file/FileListRow.tsx index 18ca862..5b9a384 100644 --- a/src/components/file/FileListRow.tsx +++ b/src/components/file/FileListRow.tsx @@ -1,11 +1,9 @@ -import React, { useEffect, useRef, useState } from "react"; -import type { ReactElement } from "react"; +import type { CSSProperties, ReactElement } from "react"; import { motion } from "framer-motion"; import type { MotionStyle } from "framer-motion"; import type { RowComponentProps } from "react-window"; import FileListItem from "./FileListItem"; -import { FILE_ITEM_CONFIG } from "./utils/fileListConfig"; import { getDynamicItemVariants, optimizedAnimationStyle } from "./utils/fileListAnimations"; import type { VirtualListItemData } from "./utils/types"; @@ -15,8 +13,6 @@ const RowComponent = ({ style, ...rowData }: RowComponentProps): ReactElement => { - const rowRef = useRef(null); - const [isVisible, setIsVisible] = useState(true); const { contents, downloadingPath, @@ -30,42 +26,15 @@ const RowComponent = ({ isScrolling, scrollSpeed, highlightedIndex, + rowPaddingBottom, } = rowData; const item = contents[index]; - useEffect(() => { - const element = rowRef.current; - if (element === null) { - return; - } - - const scrollContainer = element.closest('.virtual-file-list'); - - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - setIsVisible(entry.isIntersecting); - }); - }, - { - root: scrollContainer ?? null, - rootMargin: "100px", - threshold: 0.01, - } - ); - - observer.observe(element); - - return () => { - observer.disconnect(); - }; - }, []); if (item === undefined) { return (