Files
boris/src/components/BookmarkViews/LargeView.tsx
Gigi 507288f51c feat: add image caching for offline mode
- Add imageCacheService with localStorage-based image caching and LRU eviction
- Create useImageCache hook for React components to fetch and cache images
- Integrate image caching with article service to cache cover images on load
- Add image cache settings (enable/disable, size limit) to user settings
- Update ReaderHeader to use cached images for article covers
- Update BookmarkViews (CardView, LargeView) to use cached images
- Add image cache configuration UI in OfflineModeSettings with:
  - Toggle to enable/disable image caching
  - Slider to set cache size limit (10-200 MB)
  - Display current cache stats (size and image count)
  - Clear cache button

Images are cached in localStorage for offline viewing, with a configurable
size limit (default 50MB). LRU eviction ensures cache stays within limits.
2025-10-09 17:23:31 +01:00

114 lines
3.7 KiB
TypeScript

import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IndividualBookmark } from '../../types/bookmarks'
import { formatDate } from '../../utils/bookmarkUtils'
import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles'
import { IconGetter } from './shared'
import { useImageCache } from '../../hooks/useImageCache'
import { UserSettings } from '../../services/settingsService'
interface LargeViewProps {
bookmark: IndividualBookmark
index: number
hasUrls: boolean
extractedUrls: string[]
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void
getIconForUrlType: IconGetter
firstUrlClassification: { buttonText: string } | null
previewImage: string | null
authorNpub: string
eventNevent?: string
getAuthorDisplayName: () => string
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
articleSummary?: string
settings?: UserSettings
}
export const LargeView: React.FC<LargeViewProps> = ({
bookmark,
index,
hasUrls,
extractedUrls,
onSelectUrl,
getIconForUrlType,
firstUrlClassification,
previewImage,
authorNpub,
eventNevent,
getAuthorDisplayName,
handleReadNow,
articleSummary,
settings
}) => {
const cachedImage = useImageCache(previewImage || undefined, settings)
const isArticle = bookmark.kind === 30023
return (
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark large ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
{(hasUrls || (isArticle && cachedImage)) && (
<div
className="large-preview-image"
onClick={() => {
if (isArticle) {
handleReadNow({ preventDefault: () => {} } as React.MouseEvent<HTMLButtonElement>)
} else {
onSelectUrl?.(extractedUrls[0])
}
}}
style={cachedImage ? { backgroundImage: `url(${cachedImage})` } : undefined}
>
{!previewImage && hasUrls && (
<div className="preview-placeholder">
<FontAwesomeIcon icon={getIconForUrlType(extractedUrls[0])} />
</div>
)}
</div>
)}
<div className="large-content">
{isArticle && articleSummary ? (
<div className="large-text article-summary">
<ContentWithResolvedProfiles content={articleSummary} />
</div>
) : bookmark.content && (
<div className="large-text">
<ContentWithResolvedProfiles content={bookmark.content} />
</div>
)}
<div className="large-footer">
<span className="large-author">
<a
href={`https://search.dergigi.com/p/${authorNpub}`}
target="_blank"
rel="noopener noreferrer"
className="author-link-minimal"
>
{getAuthorDisplayName()}
</a>
</span>
{eventNevent && (
<a
href={`https://search.dergigi.com/e/${eventNevent}`}
target="_blank"
rel="noopener noreferrer"
className="bookmark-date-link"
>
{formatDate(bookmark.created_at)}
</a>
)}
{(hasUrls && firstUrlClassification) || isArticle ? (
<button className="large-read-button" onClick={handleReadNow}>
<FontAwesomeIcon icon={isArticle ? getIconForUrlType('') : getIconForUrlType(extractedUrls[0])} />
{isArticle ? 'Read Article' : firstUrlClassification?.buttonText}
</button>
) : null}
</div>
</div>
</div>
)
}