1- import React , { useEffect , useState , useCallback , useRef } from 'react' ;
1+ import React , { useEffect , useState , useCallback , useRef , useMemo } from 'react' ;
22import { X , Loader2 , AlertCircle , FileText , ExternalLink , List , Type , ArrowUp , Languages , Eye } from 'lucide-react' ;
33import BilingualMarkdownRenderer , { DisplayMode , BilingualMarkdownRendererHandle , TranslationStatus } from './BilingualMarkdownRenderer' ;
44import { stripMarkdownFormatting } from '../utils/markdownUtils' ;
55import { Repository } from '../types' ;
66import { GitHubApiService } from '../services/githubApi' ;
77import { backend } from '../services/backendAdapter' ;
88import { useAppStore } from '../store/useAppStore' ;
9+ import { buildReadmeVariants , DEFAULT_README_VARIANT , type GitHubReadmeCandidateItem , type ReadmeVariant } from '../utils/readmeVariants' ;
910
1011interface TocItem {
1112 id : string ;
@@ -27,6 +28,11 @@ const FONT_SIZES = [
2728
2829const TOC_MAX_LEVEL = 6 ;
2930
31+ const getDefaultReadmeVariant = ( language : 'zh' | 'en' ) : ReadmeVariant => ( {
32+ ...DEFAULT_README_VARIANT ,
33+ label : language === 'zh' ? '默认 README' : 'Default README' ,
34+ } ) ;
35+
3036export const ReadmeModal : React . FC < ReadmeModalProps > = ( {
3137 isOpen,
3238 onClose,
@@ -47,11 +53,18 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
4753 const [ errorExpanded , setErrorExpanded ] = useState ( false ) ;
4854 const [ tocWidth , setTocWidth ] = useState ( 224 ) ;
4955 const [ translatedHeadingMap , setTranslatedHeadingMap ] = useState < Map < string , string > > ( new Map ( ) ) ;
56+ const [ readmeVariants , setReadmeVariants ] = useState < ReadmeVariant [ ] > ( ( ) => [ getDefaultReadmeVariant ( language ) ] ) ;
57+ const [ selectedReadmeKey , setSelectedReadmeKey ] = useState ( 'default' ) ;
58+ const [ variantsLoading , setVariantsLoading ] = useState ( false ) ;
59+ const [ readmeCache , setReadmeCache ] = useState < Record < string , string > > ( { } ) ;
60+
61+ const defaultReadmeVariant = useMemo ( ( ) => getDefaultReadmeVariant ( language ) , [ language ] ) ;
5062
5163 const modalRef = useRef < HTMLDivElement > ( null ) ;
5264 const contentRef = useRef < HTMLDivElement > ( null ) ;
5365 const previousFocusRef = useRef < HTMLElement | null > ( null ) ;
5466 const abortControllerRef = useRef < AbortController | null > ( null ) ;
67+ const variantsAbortControllerRef = useRef < AbortController | null > ( null ) ;
5568 const isResizingRef = useRef ( false ) ;
5669 const startXRef = useRef ( 0 ) ;
5770 const startWidthRef = useRef ( 0 ) ;
@@ -284,7 +297,26 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
284297 setTranslatedHeadingMap ( map ) ;
285298 } , [ ] ) ;
286299
287- const fetchReadme = useCallback ( async ( ) => {
300+ const resetTranslationState = useCallback ( ( ) => {
301+ bilingualRef . current ?. revert ( ) ;
302+ setDisplayMode ( 'bilingual' ) ;
303+ setTranslateStatus ( 'idle' ) ;
304+ setTranslateProgress ( { current : 0 , total : 0 } ) ;
305+ setTranslateError ( null ) ;
306+ setTranslatedHeadingMap ( new Map ( ) ) ;
307+ } , [ ] ) ;
308+
309+ const resetReadmeViewState = useCallback ( ( ) => {
310+ resetTranslationState ( ) ;
311+ setTocItems ( [ ] ) ;
312+ setHeadingIdMap ( new Map ( ) ) ;
313+ setActiveHeadingId ( null ) ;
314+ setScrollProgress ( 0 ) ;
315+ setShowBackToTop ( false ) ;
316+ scrollToTop ( ) ;
317+ } , [ resetTranslationState , scrollToTop ] ) ;
318+
319+ const fetchReadmeContent = useCallback ( async ( variant : ReadmeVariant ) => {
288320 if ( ! repository ) return ;
289321
290322 if ( abortControllerRef . current ) {
@@ -301,39 +333,127 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
301333 let content = '' ;
302334
303335 if ( backend . isAvailable ) {
304- content = await backend . getRepositoryReadme ( owner , name ) ;
336+ content = variant . isDefault || ! variant . path
337+ ? await backend . getRepositoryReadme ( owner , name , abortController . signal )
338+ : await backend . getRepositoryReadmeByPath ( owner , name , variant . path , abortController . signal ) ;
305339 } else if ( githubToken ) {
306340 const githubApi = new GitHubApiService ( githubToken ) ;
307- content = await githubApi . getRepositoryReadme ( owner , name , abortController . signal ) ;
341+ content = variant . isDefault || ! variant . path
342+ ? await githubApi . getRepositoryReadme ( owner , name , abortController . signal )
343+ : await githubApi . getRepositoryReadmeByPath ( owner , name , variant . path , abortController . signal ) ;
308344 } else {
345+ setReadmeContent ( '' ) ;
309346 setError ( language === 'zh' ? '未登录且后端不可用,无法加载 README' : 'Not logged in and backend unavailable, cannot load README' ) ;
310347 setLoading ( false ) ;
311348 return ;
312349 }
313350
314351 if ( abortController . signal . aborted ) return ;
315352
353+ setReadmeCache ( prev => ( { ...prev , [ variant . key ] : content } ) ) ;
354+
316355 if ( content . trim ( ) ) {
317356 setReadmeContent ( content ) ;
357+ setError ( null ) ;
318358 } else {
319- setError ( language === 'zh' ? '该仓库没有 README 文件' : 'This repository has no README file' ) ;
359+ setReadmeContent ( '' ) ;
360+ setError ( variant . isDefault
361+ ? ( language === 'zh' ? '该仓库没有 README 文件' : 'This repository has no README file' )
362+ : ( language === 'zh' ? '该 README 文件为空' : 'This README file is empty' ) ) ;
320363 }
321364 } catch ( err ) {
322365 if ( abortController . signal . aborted ) return ;
323366 console . error ( 'Failed to fetch README:' , err ) ;
324- setError ( language === 'zh' ? '加载 README 失败,请检查网络连接或稍后重试' : 'Failed to load README. Please check your network connection and try again later' ) ;
367+ setReadmeContent ( '' ) ;
368+ setError ( variant . isDefault
369+ ? ( language === 'zh' ? '加载 README 失败,请检查网络连接或稍后重试' : 'Failed to load README. Please check your network connection and try again later' )
370+ : ( language === 'zh' ? '加载所选 README 失败,请稍后重试' : 'Failed to load selected README. Please try again later' ) ) ;
325371 } finally {
326372 if ( ! abortController . signal . aborted ) {
327373 setLoading ( false ) ;
328374 }
329375 }
330376 } , [ repository , githubToken , language ] ) ;
331377
378+ const fetchReadmeVariants = useCallback ( async ( ) => {
379+ if ( ! repository ) return ;
380+
381+ if ( variantsAbortControllerRef . current ) {
382+ variantsAbortControllerRef . current . abort ( ) ;
383+ }
384+ const abortController = new AbortController ( ) ;
385+ variantsAbortControllerRef . current = abortController ;
386+
387+ setVariantsLoading ( true ) ;
388+
389+ try {
390+ const [ owner , name ] = repository . full_name . split ( '/' ) ;
391+ const defaultBranch = ( repository as Repository & { default_branch ?: string } ) . default_branch ;
392+ let candidates : GitHubReadmeCandidateItem [ ] = [ ] ;
393+
394+ if ( backend . isAvailable ) {
395+ candidates = await backend . listRepositoryReadmeCandidates ( owner , name , defaultBranch , abortController . signal ) ;
396+ } else if ( githubToken ) {
397+ const githubApi = new GitHubApiService ( githubToken ) ;
398+ candidates = await githubApi . listRepositoryReadmeCandidates ( owner , name , defaultBranch , abortController . signal ) ;
399+ } else {
400+ return ;
401+ }
402+
403+ if ( abortController . signal . aborted ) return ;
404+ setReadmeVariants ( buildReadmeVariants ( candidates , language ) ) ;
405+ } catch ( err ) {
406+ if ( ! abortController . signal . aborted ) {
407+ console . warn ( 'Failed to detect README variants:' , err ) ;
408+ setReadmeVariants ( [ defaultReadmeVariant ] ) ;
409+ }
410+ } finally {
411+ if ( ! abortController . signal . aborted ) {
412+ setVariantsLoading ( false ) ;
413+ }
414+ }
415+ } , [ repository , githubToken , language , defaultReadmeVariant ] ) ;
416+
417+ const fetchReadme = useCallback ( async ( ) => {
418+ const currentVariant = readmeVariants . find ( variant => variant . key === selectedReadmeKey ) || defaultReadmeVariant ;
419+ await fetchReadmeContent ( currentVariant ) ;
420+ } , [ readmeVariants , selectedReadmeKey , defaultReadmeVariant , fetchReadmeContent ] ) ;
421+
422+ const handleReadmeVariantChange = useCallback ( ( event : React . ChangeEvent < HTMLSelectElement > ) => {
423+ const nextKey = event . target . value ;
424+ if ( nextKey === selectedReadmeKey ) return ;
425+
426+ const nextVariant = readmeVariants . find ( variant => variant . key === nextKey ) ;
427+ if ( ! nextVariant ) return ;
428+
429+ setSelectedReadmeKey ( nextKey ) ;
430+ resetReadmeViewState ( ) ;
431+
432+ const cachedContent = readmeCache [ nextKey ] ;
433+ if ( cachedContent !== undefined ) {
434+ setReadmeContent ( cachedContent ) ;
435+ setError ( cachedContent . trim ( )
436+ ? null
437+ : nextVariant . isDefault
438+ ? ( language === 'zh' ? '该仓库没有 README 文件' : 'This repository has no README file' )
439+ : ( language === 'zh' ? '该 README 文件为空' : 'This README file is empty' ) ) ;
440+ return ;
441+ }
442+
443+ void fetchReadmeContent ( nextVariant ) ;
444+ } , [ selectedReadmeKey , readmeVariants , readmeCache , resetReadmeViewState , language , fetchReadmeContent ] ) ;
445+
332446 useEffect ( ( ) => {
333447 if ( isOpen && repository ) {
334- fetchReadme ( ) ;
448+ const defaultVariant = getDefaultReadmeVariant ( language ) ;
449+ setReadmeVariants ( [ defaultVariant ] ) ;
450+ setSelectedReadmeKey ( 'default' ) ;
451+ setReadmeCache ( { } ) ;
452+ resetReadmeViewState ( ) ;
453+ void fetchReadmeContent ( defaultVariant ) ;
454+ void fetchReadmeVariants ( ) ;
335455 }
336- } , [ isOpen , repository , fetchReadme ] ) ;
456+ } , [ isOpen , repository , language , fetchReadmeContent , fetchReadmeVariants , resetReadmeViewState ] ) ;
337457
338458 useEffect ( ( ) => {
339459 if ( displayContent ) {
@@ -355,9 +475,17 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
355475 abortControllerRef . current . abort ( ) ;
356476 abortControllerRef . current = null ;
357477 }
478+ if ( variantsAbortControllerRef . current ) {
479+ variantsAbortControllerRef . current . abort ( ) ;
480+ variantsAbortControllerRef . current = null ;
481+ }
358482 setReadmeContent ( '' ) ;
359483 setError ( null ) ;
360484 setLoading ( false ) ;
485+ setReadmeVariants ( [ getDefaultReadmeVariant ( language ) ] ) ;
486+ setSelectedReadmeKey ( 'default' ) ;
487+ setVariantsLoading ( false ) ;
488+ setReadmeCache ( { } ) ;
361489 setTocItems ( [ ] ) ;
362490 setHeadingIdMap ( new Map ( ) ) ;
363491 setScrollProgress ( 0 ) ;
@@ -376,14 +504,18 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
376504 } else {
377505 setShowToc ( true ) ;
378506 }
379- } , [ isOpen ] ) ;
507+ } , [ isOpen , language ] ) ;
380508
381509 useEffect ( ( ) => {
382510 return ( ) => {
383511 if ( abortControllerRef . current ) {
384512 abortControllerRef . current . abort ( ) ;
385513 abortControllerRef . current = null ;
386514 }
515+ if ( variantsAbortControllerRef . current ) {
516+ variantsAbortControllerRef . current . abort ( ) ;
517+ variantsAbortControllerRef . current = null ;
518+ }
387519 } ;
388520 } , [ ] ) ;
389521
@@ -441,6 +573,7 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
441573 const isTranslating = translateStatus === 'translating' ;
442574 const isTranslated = translateStatus === 'translated' ;
443575 const isTranslateError = translateStatus === 'error' ;
576+ const currentReadmeVariant = readmeVariants . find ( variant => variant . key === selectedReadmeKey ) || defaultReadmeVariant ;
444577
445578 return (
446579 < div className = "fixed inset-0 z-50 overflow-y-auto" >
@@ -478,12 +611,28 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
478611 < h3 id = "readme-modal-title" className = "text-lg font-semibold text-gray-900 dark:text-text-primary" >
479612 { repository . full_name }
480613 </ h3 >
481- < p className = "text-sm text-gray-500 dark:text-text-secondary" >
482- README
614+ < p className = "text-sm text-gray-500 dark:text-text-secondary truncate max-w-[260px]" title = { currentReadmeVariant . path || 'README' } >
615+ { currentReadmeVariant . isDefault ? ' README' : currentReadmeVariant . path }
483616 </ p >
484617 </ div >
485618 </ div >
486619 < div className = "flex items-center space-x-1" >
620+ { readmeVariants . length > 1 && (
621+ < select
622+ value = { selectedReadmeKey }
623+ onChange = { handleReadmeVariantChange }
624+ disabled = { loading || variantsLoading }
625+ className = "w-28 sm:w-auto max-w-[220px] px-2 py-2 text-sm rounded-lg border border-black/[0.06] dark:border-white/[0.08] bg-white dark:bg-panel-dark text-gray-700 dark:text-text-primary hover:bg-light-surface dark:hover:bg-white/5 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
626+ title = { t ( '切换 README 语言' , 'Switch README language' ) }
627+ aria-label = { t ( '切换 README 语言' , 'Switch README language' ) }
628+ >
629+ { readmeVariants . map ( ( variant ) => (
630+ < option key = { variant . key } value = { variant . key } >
631+ { variant . label }
632+ </ option >
633+ ) ) }
634+ </ select >
635+ ) }
487636 { readmeContent && ! loading && (
488637 isTranslated ? (
489638 < >
0 commit comments