Ilyas Hallak 305b8f733e Implement offline hero image caching with custom cache keys
Major improvements to offline reading functionality:

**Hero Image Offline Support:**
- Add heroImageURL field to BookmarkEntity for persistent storage
- Implement ImageCache-based caching with custom keys (bookmark-{id}-hero)
- Update CachedAsyncImage to support offline loading via cache keys
- Hero images now work offline without URL dependency

**Offline Bookmark Loading:**
- Add proactive offline detection before API calls
- Implement automatic fallback to cached bookmarks when offline
- Fix network status initialization race condition
- Network monitor now checks status synchronously on init

**Core Data Enhancements:**
- Persist hero image URLs in BookmarkEntity.heroImageURL
- Reconstruct ImageResource from cached URLs on offline load
- Add extensive logging for debugging persistence issues

**UI Updates:**
- Update BookmarkDetailView2 to use cache keys for hero images
- Update BookmarkCardView (all 3 layouts) with cache key support
- Improve BookmarksView offline state handling with task-based loading
- Add 50ms delay for network status propagation

**Technical Details:**
- NetworkMonitorRepository: Fix initial status from hardcoded true to actual network check
- BookmarksViewModel: Inject AppSettings for offline detection
- OfflineCacheRepository: Add verification logging for save/load operations
- BookmarkEntityMapper: Sync heroImageURL on save, restore on load

This enables full offline reading with hero images visible in bookmark lists
and detail views, even after app restart.
2025-11-28 23:01:20 +01:00

73 lines
6.3 KiB
XML

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24299" systemVersion="25A354" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="ArticleURLEntity" representedClassName="ArticleURLEntity" syncable="YES" codeGenerationType="class">
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="tags" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="String"/>
</entity>
<entity name="BookmarkEntity" representedClassName="BookmarkEntity" syncable="YES" codeGenerationType="class">
<attribute name="authors" optional="YES" attributeType="String"/>
<attribute name="cachedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" indexed="YES"/>
<attribute name="cacheSize" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="created" optional="YES" attributeType="String"/>
<attribute name="desc" optional="YES" attributeType="String"/>
<attribute name="documentType" optional="YES" attributeType="String"/>
<attribute name="hasArticle" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasDeleted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="heroImageURL" optional="YES" attributeType="String"/>
<attribute name="href" optional="YES" attributeType="String"/>
<attribute name="htmlContent" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="imageURLs" optional="YES" attributeType="String"/>
<attribute name="isArchived" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isMarked" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lang" optional="YES" attributeType="String"/>
<attribute name="lastAccessDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="loaded" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="published" optional="YES" attributeType="String"/>
<attribute name="readingTime" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="readProgress" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="site" optional="YES" attributeType="String"/>
<attribute name="siteName" optional="YES" attributeType="String"/>
<attribute name="state" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="textDirection" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
<attribute name="type" optional="YES" attributeType="String"/>
<attribute name="update" optional="YES" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="wordCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="resources" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BookmarkResourcesEntity"/>
</entity>
<entity name="BookmarkResourcesEntity" representedClassName="BookmarkResourcesEntity" syncable="YES" codeGenerationType="class">
<attribute name="id" optional="YES" attributeType="String"/>
<relationship name="article" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ResourceEntity"/>
<relationship name="icon" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ImageResourceEntity"/>
<relationship name="image" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ImageResourceEntity"/>
<relationship name="log" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ResourceEntity"/>
<relationship name="props" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ResourceEntity"/>
<relationship name="thumbnail" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ImageResourceEntity"/>
</entity>
<entity name="ImageResourceEntity" representedClassName="ImageResourceEntity" syncable="YES" codeGenerationType="class">
<attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="src" optional="YES" attributeType="String"/>
<attribute name="width" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<entity name="ResourceEntity" representedClassName="ResourceEntity" syncable="YES" codeGenerationType="class">
<attribute name="src" optional="YES" attributeType="String"/>
</entity>
<entity name="SettingEntity" representedClassName="SettingEntity" syncable="YES" codeGenerationType="class">
<attribute name="cardLayoutStyle" optional="YES" attributeType="String"/>
<attribute name="enableTTS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fontFamily" optional="YES" attributeType="String"/>
<attribute name="fontSize" optional="YES" attributeType="String"/>
<attribute name="tagSortOrder" optional="YES" attributeType="String"/>
<attribute name="theme" optional="YES" attributeType="String"/>
<attribute name="token" optional="YES" attributeType="String"/>
<attribute name="urlOpener" optional="YES" attributeType="String"/>
</entity>
<entity name="TagEntity" representedClassName="TagEntity" syncable="YES" codeGenerationType="class">
<attribute name="count" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
</entity>
</model>