Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ npm run lint

### 代码格式化

我们使用 `EditorConfig` 进行一致的格式化,请确保你的编辑器安装了相应插件以自动应用格式规则。
我们使用 `EditorConfig` 进行统一格式化,请确保你的编辑器安装了相应插件以自动应用格式规则。

## 提交 issue

Expand All @@ -104,7 +104,7 @@ npm run lint

### [enhancement]

- 清晰的增强描述,杜绝抽象概念
- 清晰地描述增强点,杜绝抽象概念
- 建议的实现逻辑

### [feature]
Expand All @@ -115,8 +115,6 @@ npm run lint

> 推荐保持与历史 issue 一致的标题格式

---

### 许可证

通过贡献,您同意您的贡献将按照与项目相同的许可证 [AGPL-3.0](LICENSE) 进行许可。
Expand Down
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
<h1 align="center">
<em>Repo-Viewer</em>
</h1>

<p align="center">
<strong>基于 Material Design 3设计风格的 GitHub仓库浏览应用</strong>
&nbsp;&nbsp;
<a href="https://deepwiki.com/UE-DND/Repo-Viewer">
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
</a>
</p>
<div style="text-align: center;">
<h1><em>Repo-Viewer</em></h1>
</div>

<div style="text-align: center;">
<p>
<strong>基于 Material Design 3设计风格的 GitHub仓库浏览应用</strong>
&nbsp;&nbsp;
<a href="https://deepwiki.com/UE-DND/Repo-Viewer">
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
</a>
</p>
</div>

![Preview Dark](docs/image/dark.png)

Expand All @@ -20,9 +22,17 @@
<tr>
</table>

## 为 Repo Viewer 贡献代码

此项目已进入稳定阶段,本人将不再花过多精力维护。若发现已知的问题,欢迎任何形式的贡献!无论是修复错误、改进功能,还是提升代码质量,我们都非常欢迎您的参与。

> 此组织的所有成员均有管理员权限,若不想提交 Pull Request,直接推送代码是被允许的。
>
> 但在提交贡献前,推荐阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 以了解建议的代码规范和提交流程。

## 主要功能

- 📁 **仓库浏览**:直观的文件结构导航,同时提供首页文件和文件夹排除选项.
- 📁 **仓库浏览**:直观的文件结构导航,同时提供首页文件与文件夹排除选项.
- 🔎 **文件搜索**:支持基于自建索引和 Github API 的快速文件搜索,可按分支、路径前缀和扩展名过滤.
- 📄 **文件预览**:多种文件格式预览,目前支持 `Markdown`、 `PDF` 和 `图片`.
- ⬇️ **文件下载**:可下载单个文件或整个文件夹.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "repo-viewer",
"private": true,
"version": "1.3.9",
"version": "1.4.0",
"type": "module",
"engines": {
"node": "24.x"
Expand All @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/generateDocfindIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ const resolveBranchRef = async (repoPath: string, branch: string): Promise<strin
await runCommandText("git", ["rev-parse", "--verify", candidate], repoPath);
return candidate;
} catch {
continue;

}
}
return null;
Expand Down
6 changes: 4 additions & 2 deletions scripts/generateInitialContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const fetchJson = async <T>(url: string): Promise<T> => {
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<T>;
return (await response.json()) as T;
};

const fetchText = async (url: string): Promise<string> => {
Expand Down Expand Up @@ -139,7 +139,9 @@ const run = async (): Promise<void> => {
const contents = await fetchJson<unknown>(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);
Expand Down
4 changes: 1 addition & 3 deletions scripts/src/generated/initialContent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import type { InitialContentHydrationPayload } from "@/types";

export const initialContentPayload: InitialContentHydrationPayload | null = null;
export {};
39 changes: 11 additions & 28 deletions src/components/file/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -70,17 +70,8 @@ const FileList = React.memo<FileListProps>(

// 计算每个文件项的高度(包括间距)
// 这个计算需要与 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]);

/**
Expand Down Expand Up @@ -200,6 +191,7 @@ const FileList = React.memo<FileListProps>(
isScrolling,
scrollSpeed,
highlightedIndex,
rowPaddingBottom,
}),
[
contents,
Expand All @@ -214,26 +206,17 @@ const FileList = React.memo<FileListProps>(
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(() => {
Expand Down
5 changes: 3 additions & 2 deletions src/components/file/FileListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -143,7 +144,7 @@ const FileListItem = memo<FileListItemProps>(

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;
}
Expand Down
48 changes: 4 additions & 44 deletions src/components/file/FileListRow.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -15,8 +13,6 @@ const RowComponent = ({
style,
...rowData
}: RowComponentProps<VirtualListItemData>): ReactElement => {
const rowRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(true);
const {
contents,
downloadingPath,
Expand All @@ -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 (
<div
ref={rowRef}
style={style}
{...ariaAttributes}
aria-hidden="true"
Expand All @@ -75,7 +44,7 @@ const RowComponent = ({

const isHighlighted = highlightedIndex === index;

const adjustedStyle: React.CSSProperties = {
const adjustedStyle: CSSProperties = {
...style,
boxSizing: "border-box",
alignItems: "flex-start",
Expand All @@ -85,7 +54,7 @@ const RowComponent = ({
height: "100%",
width: "100%",
paddingTop: 0,
paddingBottom: FILE_ITEM_CONFIG.spacing.marginBottom,
paddingBottom: rowPaddingBottom,
paddingRight: "12px",
boxSizing: "border-box",
...optimizedAnimationStyle,
Expand All @@ -95,11 +64,9 @@ const RowComponent = ({

return (
<div
ref={rowRef}
style={adjustedStyle}
className="file-list-item-container"
{...ariaAttributes}
aria-hidden={!isVisible ? "true" : undefined}
data-oid="_c:db-1"
>
<motion.div
Expand All @@ -122,18 +89,11 @@ const RowComponent = ({
currentPath={currentPath}
contents={contents}
isHighlighted={isHighlighted}
isVisible={isVisible}
data-oid="k4zj3qr"
/>
</motion.div>
</div>
);
};

const Row = React.memo(RowComponent);

Row.displayName = "FileListRow";

export { RowComponent };

export default Row;
38 changes: 37 additions & 1 deletion src/components/file/utils/fileListLayout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
FILE_ITEM_CONFIG,
LIST_HEIGHT_CONFIG,
TOP_ELEMENTS_ESTIMATE,
BOTTOM_RESERVED_SPACE,
Expand All @@ -15,6 +16,42 @@ interface LayoutMetricsParams {
viewportHeight: number | null;
}

export const getRowMetrics = (isSmallScreen: boolean): {
rowHeight: number;
rowPaddingBottom: number;
} => {
const baseHeight = isSmallScreen
? FILE_ITEM_CONFIG.baseHeight.xs
: FILE_ITEM_CONFIG.baseHeight.sm;

const rowGap = FILE_ITEM_CONFIG.spacing.marginBottom;

return {
rowHeight: baseHeight + rowGap,
rowPaddingBottom: rowGap,
};
};

export const getListPadding = (needsScrolling: boolean, isSmallScreen: boolean): {
paddingTop: number;
paddingBottom: number;
} => {
// 非滚动模式使用对称内边距,让短列表更居中
if (!needsScrolling) {
const padding = isSmallScreen ? 16 : 20;
return {
paddingTop: padding - 4,
paddingBottom: padding,
};
}

// 滚动模式收紧内边距,尽可能展示更多行
return {
paddingTop: 0,
paddingBottom: 8,
};
};

export const calculateLayoutMetrics = ({
fileCount,
rowHeight,
Expand Down Expand Up @@ -93,4 +130,3 @@ export const calculateLayoutMetrics = ({
needsScrolling: true,
};
};

Loading