@@ -27,6 +27,14 @@ const IconPlus = () => (
2727 < line x1 = "12" y1 = "5" x2 = "12" y2 = "19" /> < line x1 = "5" y1 = "12" x2 = "19" y2 = "12" />
2828 </ svg >
2929)
30+ const IconRefresh = ( ) => (
31+ < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "1.8" strokeLinecap = "round" strokeLinejoin = "round" >
32+ < path d = "M21 12a9 9 0 0 1-15.5 6.2" />
33+ < path d = "M3 12A9 9 0 0 1 18.5 5.8" />
34+ < path d = "M18 2v5h-5" />
35+ < path d = "M6 22v-5h5" />
36+ </ svg >
37+ )
3038
3139// ── Cover styles ────────────────────────────────────────────────────────
3240
@@ -170,23 +178,32 @@ interface Props {
170178 onRemoveBook : ( id : string ) => void
171179 darkMode : boolean
172180 onToggleDark : ( ) => void
181+ onApplyLatestVersion : ( ) => void | Promise < void >
173182}
174183
175184type SortKey = 'recent' | 'title' | 'progress'
176185
177- const Library = ( { records, getCoverDataUrl, onAddBooks, onOpenBook, onRemoveBook, darkMode, onToggleDark } : Props ) => {
186+ const Library = ( { records, getCoverDataUrl, onAddBooks, onOpenBook, onRemoveBook, darkMode, onToggleDark, onApplyLatestVersion } : Props ) => {
178187 const fileInputRef = useRef < HTMLInputElement > ( null )
179188 const [ loading , setLoading ] = useState ( false )
180189 const [ pendingRemove , setPendingRemove ] = useState < { id : string ; title : string } | null > ( null )
181190 const [ query , setQuery ] = useState ( '' )
182191 const [ sort , setSort ] = useState < SortKey > ( 'recent' )
192+ const [ logoMenuOpen , setLogoMenuOpen ] = useState ( false )
193+ const [ applyingUpdate , setApplyingUpdate ] = useState ( false )
194+ const logoMenuRef = useRef < HTMLDivElement > ( null )
183195
184196 const handleRemoveRequest = ( id : string ) => {
185197 const record = records . find ( ( r ) => r . id === id )
186198 if ( record ) setPendingRemove ( { id, title : record . title } )
187199 }
188200 const handleConfirmRemove = ( ) => { if ( pendingRemove ) { onRemoveBook ( pendingRemove . id ) ; setPendingRemove ( null ) } }
189201 const handleCancelRemove = ( ) => setPendingRemove ( null )
202+ const handleApplyLatestVersion = async ( ) => {
203+ if ( applyingUpdate ) return
204+ setApplyingUpdate ( true )
205+ await onApplyLatestVersion ( )
206+ }
190207
191208 const handleFileChange = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
192209 const files = e . target . files
@@ -263,12 +280,47 @@ const Library = ({ records, getCoverDataUrl, onAddBooks, onOpenBook, onRemoveBoo
263280 < div style = { { borderBottom : `1px solid ${ borderCol } ` , background : paperBg } } >
264281 { /* 第一行:Logo + 標題 + 操作按鈕 */ }
265282 < div className = "flex items-center gap-2 px-4 pt-3 pb-2" >
266- < div style = { {
267- width : 26 , height : 26 , borderRadius : 6 , flexShrink : 0 ,
268- background : inkCol , color : paperBg ,
269- display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
270- fontFamily : SERIF , fontStyle : 'italic' , fontWeight : 700 , fontSize : 14 ,
271- } } > T</ div >
283+ < div ref = { logoMenuRef } style = { { position : 'relative' , flexShrink : 0 } } >
284+ < button
285+ onClick = { ( ) => setLogoMenuOpen ( ( open ) => ! open ) }
286+ style = { {
287+ width : 26 , height : 26 , borderRadius : 6 ,
288+ background : logoMenuOpen ? paperBg2 : inkCol , color : logoMenuOpen ? inkCol : paperBg ,
289+ display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
290+ fontFamily : SERIF , fontStyle : 'italic' , fontWeight : 700 , fontSize : 14 ,
291+ cursor : 'pointer' ,
292+ } }
293+ aria-label = "Travel in Time 選單"
294+ >
295+ T
296+ </ button >
297+ { logoMenuOpen && (
298+ < div
299+ style = { {
300+ position : 'absolute' , left : 0 , top : 32 , zIndex : 60 ,
301+ width : 178 , padding : 6 , borderRadius : 8 ,
302+ background : paperBg , border : `1px solid ${ borderCol } ` ,
303+ boxShadow : '0 14px 32px -14px rgba(0,0,0,0.45)' ,
304+ } }
305+ >
306+ < button
307+ onClick = { handleApplyLatestVersion }
308+ disabled = { applyingUpdate }
309+ style = { {
310+ width : '100%' , minHeight : 34 , borderRadius : 6 , padding : '8px 10px' ,
311+ display : 'flex' , alignItems : 'center' , gap : 8 ,
312+ color : applyingUpdate ? ink3Col : inkCol ,
313+ fontFamily : 'inherit' , fontSize : 13 , textAlign : 'left' ,
314+ cursor : applyingUpdate ? 'default' : 'pointer' ,
315+ opacity : applyingUpdate ? 0.7 : 1 ,
316+ } }
317+ >
318+ < IconRefresh />
319+ < span > { applyingUpdate ? '更新中…' : '套用最新版' } </ span >
320+ </ button >
321+ </ div >
322+ ) }
323+ </ div >
272324 < span style = { { fontFamily : SERIF , fontSize : 16 , fontWeight : 500 , letterSpacing : '0.01em' } } > Travel in Time</ span >
273325 < span style = { { fontFamily : MONO , fontSize : 10 , letterSpacing : '0.12em' , textTransform : 'uppercase' , color : ink3Col } } > Library</ span >
274326
0 commit comments