// // WRReaderViewController.m // WeRead (微信读书) // // Detailed pseudo-code reconstruction of the main reader controller. // 431 methods total. This file covers the key methods for page rendering, // chapter navigation, progress saving, and typesetter management. // #import "WRReaderViewController.h" #import "WRPageViewController.h" #import "WRPageView.h" #import "WRChapterData.h" #import "WRChapterPageCount.h" #import "WRCoreTextLayouter.h" #import "WRCoreTextLayoutFrame.h" #import "WRBook.h" // ============================================================================ #pragma mark - Constants // ============================================================================ static NSString *const kReadingProgressKeyPrefix = @"weread_progress_"; static NSString *const kChapterCachePrefix = @"weread_chapter_"; static NSString *const kPageCountCachePrefix = @"weread_pagecount_"; static NSString *const kReaderDidFlipPage = @"WRReaderDidFlipPage"; static NSString *const kReaderChapterLoaded = @"WRReaderChapterLoaded"; static NSString *const kReaderProgressSaved = @"WRReaderProgressSaved"; // ============================================================================ #pragma mark - WRReadingProgress // ============================================================================ @implementation WRReadingProgress - (instancetype)init { self = [super init]; if (self) { _chapterIndex = 0; _pageIndex = 0; _charIndex = 0; _scrollOffset = 0.0; _readPercentage = 0.0; } return self; } - (instancetype)copyWithZone:(NSZone *)zone { WRReadingProgress *copy = [[WRReadingProgress alloc] init]; copy.bookId = self.bookId; copy.chapterIndex = self.chapterIndex; copy.pageIndex = self.pageIndex; copy.charIndex = self.charIndex; copy.scrollOffset = self.scrollOffset; copy.chapterId = self.chapterId; copy.readPercentage = self.readPercentage; return copy; } @end // ============================================================================ #pragma mark - WRReaderViewController () // ============================================================================ @interface WRReaderViewController () @property (nonatomic, strong) WRBook *book; @property (nonatomic, copy) NSString *bookId; @property (nonatomic, strong) WRPageViewController *pageViewController; @property (nonatomic, assign) BOOL doodleMode; @property (nonatomic, assign) BOOL autoReadEnabled; @property (nonatomic, assign) BOOL isRecomposing; @property (nonatomic, assign) BOOL forceUseInitialProgress; @property (nonatomic, strong) WRReadingProgress *initialProgress; @property (nonatomic, strong) NSMutableDictionary *chapterDataCache; @property (nonatomic, strong) NSMutableDictionary *pageCountCache; @property (nonatomic, strong) NSMutableArray *activePageViews; @property (nonatomic, strong) dispatch_queue_t chapterLoadQueue; @property (nonatomic, assign) BOOL isLoadingChapter; @property (nonatomic, assign) NSUInteger loadRetryCount; @property (nonatomic, assign) NSUInteger maxRetryCount; @property (nonatomic, strong) NSTimer *autoReadTimer; @property (nonatomic, strong) NSTimer *progressSaveTimer; @end // ============================================================================ #pragma mark - WRReaderViewController Implementation // ============================================================================ @implementation WRReaderViewController // ============================================================================ #pragma mark - Initialization // ============================================================================ - (instancetype)initWithBook:(WRBook *)book progress:(WRReadingProgress *)progress forceUseInitialProgress:(BOOL)forceUseInitialProgress doodleMode:(BOOL)doodleMode autoRead:(BOOL)autoRead { self = [super initWithNibName:nil bundle:nil]; if (self) { _book = book; _bookId = book.bookId; _initialProgress = progress; _forceUseInitialProgress = forceUseInitialProgress; _doodleMode = doodleMode; _autoReadEnabled = autoRead; _maxRetryCount = 3; _loadRetryCount = 0; _chapterDataCache = [NSMutableDictionary dictionary]; _pageCountCache = [NSMutableDictionary dictionary]; _activePageViews = [NSMutableArray array]; _chapterLoadQueue = dispatch_queue_create( "com.weread.chapterload", DISPATCH_QUEUE_SERIAL); if (forceUseInitialProgress && progress) { _readingProgress = [progress copy]; } else { _readingProgress = [self _loadSavedProgress] ?: progress; } if (!_readingProgress) { _readingProgress = [[WRReadingProgress alloc] init]; _readingProgress.bookId = _bookId; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleAutoReadAdvance:) name:@"WRPageViewAutoReadAdvance" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleFontSizeChange:) name:@"WRPageViewFontSizeChange" object:nil]; } return self; } - (instancetype)initWithBookId:(NSString *)bookId { self = [super initWithNibName:nil bundle:nil]; if (self) { _bookId = bookId; _maxRetryCount = 3; _chapterDataCache = [NSMutableDictionary dictionary]; _pageCountCache = [NSMutableDictionary dictionary]; _activePageViews = [NSMutableArray array]; _chapterLoadQueue = dispatch_queue_create( "com.weread.chapterload", DISPATCH_QUEUE_SERIAL); [self _loadBookDataWithBookId:bookId]; } return self; } - (void)dealloc { [_autoReadTimer invalidate]; [_progressSaveTimer invalidate]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } // ============================================================================ #pragma mark - View Lifecycle // ============================================================================ - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; // Create and embed the page view controller. WRPageFlippingStyle flipStyle = [self _userPreferredFlipStyle]; UIPageViewControllerTransitionStyle uiStyle = (flipStyle == WRPageFlippingStyleCurl) ? UIPageViewControllerTransitionStylePageCurl : UIPageViewControllerTransitionStyleScroll; self.pageViewController = [[WRPageViewController alloc] initWithDelegate:self withPageType:uiStyle pageFlippingStyle:flipStyle]; [self addChildViewController:self.pageViewController]; self.pageViewController.view.frame = self.view.bounds; self.pageViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self.view addSubview:self.pageViewController.view]; [self.pageViewController didMoveToParentViewController:self]; // Load the initial chapter. [self _loadChapterAtIndex:self.readingProgress.chapterIndex completion:^(WRChapterData *chapterData, NSError *error) { if (chapterData) { [self _displayChapter:chapterData atPageIndex:self.readingProgress.pageIndex animated:NO]; } else { [self _showChapterLoadError:error]; } }]; // Start the progress save timer (every 30 seconds). _progressSaveTimer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:self selector:@selector(_periodicProgressSave) userInfo:nil repeats:YES]; if (_autoReadEnabled) { [self _startAutoRead]; } } // ============================================================================ #pragma mark - Page Rendering // ============================================================================ /// /// The main rendering pipeline: /// 1. Look up or compute WRChapterData for the progress's chapter. /// 2. Compute pagination via WRChapterPageCount. /// 3. Get the character range for the target page. /// 4. Create a WRCoreTextLayoutFrame for that range. /// 5. Assign the layout frame to the WRPageView. /// 6. Trigger -setNeedsDisplay on the page view. /// - (void)renderPageView:(WRPageView *)pageView progressData:(WRReadingProgress *)progressData source:(NSString *)source { if (!pageView || !progressData) return; NSLog(@"[WRReader] renderPageView source=%@ ch=%lu page=%lu", source, (unsigned long)progressData.chapterIndex, (unsigned long)progressData.pageIndex); // Step 1: Get or compute chapter data. WRChapterData *chapterData = [self _chapterDataForIndex:progressData.chapterIndex]; if (!chapterData) { [pageView showLoadingWithProgress:0.0]; return; } // Step 2: Compute pagination. WRChapterPageCount *pageCount = [self _pageCountForChapterData:chapterData]; if (!pageCount || pageCount.totalPages == 0) { [pageView showErrorWithMessage:@"页面计算失败"]; return; } // Step 3: Get the page range. NSUInteger pageIdx = progressData.pageIndex; if (pageIdx >= pageCount.totalPages) { pageIdx = pageCount.totalPages - 1; } NSRange pageRange = [pageCount rangeForPageAtIndex:pageIdx]; if (pageRange.location == NSNotFound) { [pageView showErrorWithMessage:@"页面范围无效"]; return; } // Step 4: Create layout frame for this page range. WRCoreTextLayouter *layouter = chapterData.layouter; WRCoreTextLayoutFrame *layoutFrame = [layouter layoutFrameForRange:pageRange size:self.view.bounds.size insets:chapterData.contentInsets]; // Step 5: Assign to page view. pageView.chapterData = chapterData; pageView.layoutFrame = layoutFrame; pageView.pageIndex = pageIdx; // Step 6: Trigger redraw. [pageView setNeedsDisplay]; // Step 7: Update friend reviews button. [pageView friendReviewsCount:0]; // Step 8: Prefetch adjacent chapters. [self _prefetchAdjacentChaptersForIndex:progressData.chapterIndex]; } /// Recomposes the current page view after a typesetter change. - (void)recomposeCurrentPageViewWithSource:(NSString *)source { self.isRecomposing = YES; NSLog(@"[WRReader] recomposeCurrentPageView source=%@", source); [self.pageCountCache removeAllObjects]; WRChapterData *chapterData = self.currentChapterData; if (chapterData) { [self _reTypesetChapterData:chapterData]; } [self renderPageView:self.activePageViews.firstObject progressData:self.readingProgress source:source]; self.isRecomposing = NO; } /// Reloads all page views after a full settings change. - (void)reloadPageViewsWithProgressData:(WRReadingProgress *)progressData source:(NSString *)source { [self.chapterDataCache removeAllObjects]; [self.pageCountCache removeAllObjects]; self.readingProgress = progressData; [self _loadChapterAtIndex:progressData.chapterIndex completion:^(WRChapterData *chapterData, NSError *error) { if (chapterData) { [self _displayChapter:chapterData atPageIndex:progressData.pageIndex animated:NO]; } }]; } // ============================================================================ #pragma mark - Chapter Navigation // ============================================================================ /// Jumps to a specific chapter and position. - (void)gotoChapterIdx:(NSUInteger)chapterIdx position:(NSUInteger)position positionOfFile:(NSUInteger)positionOfFile { NSLog(@"[WRReader] gotoChapterIdx:%lu position:%lu posFile:%lu", (unsigned long)chapterIdx, (unsigned long)position, (unsigned long)positionOfFile); self.readingProgress.chapterIndex = chapterIdx; self.readingProgress.pageIndex = position; self.readingProgress.charIndex = positionOfFile; // If the chapter is already cached, display it directly. WRChapterData *cachedData = self.chapterDataCache[@(chapterIdx)]; if (cachedData) { self.currentChapterData = cachedData; [self _displayChapter:cachedData atPageIndex:position animated:YES]; return; } // Otherwise, load asynchronously. [self _loadChapterAtIndex:chapterIdx completion:^(WRChapterData *chapterData, NSError *error) { if (chapterData) { if (positionOfFile > 0) { WRChapterPageCount *pageCount = [self _pageCountForChapterData:chapterData]; NSUInteger pageIdx = [pageCount pageIndexForCharacterIndex:positionOfFile]; self.readingProgress.pageIndex = pageIdx; } [self _displayChapter:chapterData atPageIndex:self.readingProgress.pageIndex animated:YES]; } else { [self _showChapterLoadError:error]; } }]; } /// Advances to the next chapter. - (void)jumpReadingToNextChapter { NSUInteger nextIdx = self.readingProgress.chapterIndex + 1; if (nextIdx >= self.totalChapters) return; [self gotoChapterIdx:nextIdx position:0 positionOfFile:0]; } /// Goes back to the previous chapter. - (void)jumpReadingToPreChapter { if (self.readingProgress.chapterIndex == 0) return; NSUInteger prevIdx = self.readingProgress.chapterIndex - 1; [self gotoChapterIdx:prevIdx position:0 positionOfFile:0]; } // ============================================================================ #pragma mark - Page Flip Callback // ============================================================================ /// Called by WRPageViewController when a page flip animation completes. - (void)didFlipPage { // Step 1: Update reading progress. NSUInteger newPageIndex = self.pageViewController.currentPageIndex; self.readingProgress.pageIndex = newPageIndex; WRChapterPageCount *pageCount = self.currentChapterPageCount; if (pageCount) { NSRange pageRange = [pageCount rangeForPageAtIndex:newPageIndex]; if (pageRange.location != NSNotFound) { self.readingProgress.charIndex = pageRange.location; } } // Step 2: Calculate read percentage. [self _updateReadPercentage]; // Step 3: Save progress (async). [self _saveReadingProgressAndIsAsync:YES]; // Step 4: Notify delegate. if ([self.readerDelegate respondsToSelector: @selector(readerViewController:didUpdateProgress:)]) { [self.readerDelegate readerViewController:self didUpdateProgress:self.readingProgress]; } // Step 5: Prefetch adjacent chapters. [self _prefetchAdjacentChaptersForIndex:self.readingProgress.chapterIndex]; // Step 6: Post notification. [[NSNotificationCenter defaultCenter] postNotificationName:kReaderDidFlipPage object:self userInfo:@{@"progress": self.readingProgress}]; } // ============================================================================ #pragma mark - Progress Persistence // ============================================================================ /// Saves the reading progress to NSUserDefaults. - (void)_saveReadingProgressAndIsAsync:(BOOL)isAsync { WRReadingProgress *progress = self.readingProgress; if (!progress || !progress.bookId) return; NSString *key = [kReadingProgressKeyPrefix stringByAppendingString:progress.bookId]; NSDictionary *dict = @{ @"bookId": progress.bookId ?: @"", @"chapterIndex": @(progress.chapterIndex), @"pageIndex": @(progress.pageIndex), @"charIndex": @(progress.charIndex), @"scrollOffset": @(progress.scrollOffset), @"chapterId": progress.chapterId ?: @"", @"readPercentage": @(progress.readPercentage), @"timestamp": @([[NSDate date] timeIntervalSince1970]), }; if (isAsync) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[NSUserDefaults standardUserDefaults] setObject:dict forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:kReaderProgressSaved object:self]; }); }); } else { [[NSUserDefaults standardUserDefaults] setObject:dict forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; } } /// Loads the saved progress from NSUserDefaults. - (WRReadingProgress * __nullable)_loadSavedProgress { NSString *key = [kReadingProgressKeyPrefix stringByAppendingString:self.bookId ?: @""]; NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:key]; if (!dict) return nil; WRReadingProgress *progress = [[WRReadingProgress alloc] init]; progress.bookId = dict[@"bookId"]; progress.chapterIndex = [dict[@"chapterIndex"] unsignedIntegerValue]; progress.pageIndex = [dict[@"pageIndex"] unsignedIntegerValue]; progress.charIndex = [dict[@"charIndex"] unsignedIntegerValue]; progress.scrollOffset = [dict[@"scrollOffset"] doubleValue]; progress.chapterId = dict[@"chapterId"]; progress.readPercentage = [dict[@"readPercentage"] doubleValue]; return progress; } // ============================================================================ #pragma mark - Typesetter // ============================================================================ /// Modifies typesetter attributes and triggers re-typeset. - (void)changeTypesetterAttributesWithBlock:(void (^ __nonnull)(NSMutableDictionary *attrs))block { NSMutableDictionary *attrs = [self _currentTypesetterAttributes].mutableCopy; block(attrs); [self _saveTypesetterAttributes:attrs]; [self.pageCountCache removeAllObjects]; WRCoreTextLayouter *layouter = self.currentChapterData.layouter; [layouter updateAttributes:attrs]; [self recomposeCurrentPageViewWithSource:@"typesetterChange"]; } /// Reads the current typesetter attributes from UserDefaults. - (NSDictionary *)_currentTypesetterAttributes { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; return @{ @"fontSize": @([defaults floatForKey:@"WRTypesetterFontSize"] ?: 18.0), @"lineSpacing": @([defaults floatForKey:@"WRTypesetterLineSpacing"] ?: 1.5), @"fontFamily": [defaults stringForKey:@"WRTypesetterFontFamily"] ?: @"PingFang SC", @"paragraphSpacing": @([defaults floatForKey:@"WRTypesetterParaSpacing"] ?: 8.0), }; } /// Saves typesetter attributes to UserDefaults. - (void)_saveTypesetterAttributes:(NSDictionary *)attrs { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setFloat:[attrs[@"fontSize"] floatValue] forKey:@"WRTypesetterFontSize"]; [defaults setFloat:[attrs[@"lineSpacing"] floatValue] forKey:@"WRTypesetterLineSpacing"]; [defaults setObject:attrs[@"fontFamily"] forKey:@"WRTypesetterFontFamily"]; [defaults setFloat:[attrs[@"paragraphSpacing"] floatValue] forKey:@"WRTypesetterParaSpacing"]; [defaults synchronize]; } // ============================================================================ #pragma mark - Pagination // ============================================================================ /// Initializes or re-initializes the chapter page count calculator. - (void)initChapterPageCount { WRChapterData *chapterData = self.currentChapterData; if (!chapterData) return; // Generate the cache key based on book ID and current typesetter settings. NSString *cacheKey = [WRChapterPageCount currentCacheKeyWithBookId:self.bookId]; // Check cache first. WRChapterPageCount *cached = self.pageCountCache[cacheKey]; if (cached) { self.currentChapterPageCount = cached; return; } // Create a new page count calculator. WRChapterPageCount *pageCount = [[WRChapterPageCount alloc] init]; pageCount.bookId = self.bookId; pageCount.chapterId = chapterData.chapterId; // Compute page ranges by simulating CoreText typesetting. [pageCount recalculatePageRangesForAttributedString:chapterData.typesetAttributedString drawingSize:self.view.bounds.size margins:chapterData.contentInsets]; // Cache the result. self.pageCountCache[cacheKey] = pageCount; self.currentChapterPageCount = pageCount; } // ============================================================================ #pragma mark - Internal Chapter Loading // ============================================================================ /// Loads a chapter by index, with caching and retry logic. - (void)_loadChapterAtIndex:(NSUInteger)index completion:(void (^)(WRChapterData *, NSError *))completion { // Check cache first. WRChapterData *cached = self.chapterDataCache[@(index)]; if (cached) { if (completion) completion(cached, nil); return; } // Prevent duplicate loads. if (self.isLoadingChapter) return; self.isLoadingChapter = YES; __weak typeof(self) weakSelf = self; dispatch_async(self.chapterLoadQueue, ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; // In the real implementation, this: // 1. Fetches the chapter HTML/content from the server or local cache. // 2. Parses the HTML into an NSAttributedString. // 3. Runs WRCoreTextLayouter to typeset the chapter. // 4. Stores the result in the cache. // For this reconstruction, we simulate the flow: NSError *error = nil; WRChapterData *chapterData = [strongSelf _fetchChapterDataForIndex:index error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ strongSelf.isLoadingChapter = NO; if (chapterData) { strongSelf.chapterDataCache[@(index)] = chapterData; strongSelf.loadRetryCount = 0; if (completion) completion(chapterData, nil); } else { // Retry logic. if (strongSelf.loadRetryCount < strongSelf.maxRetryCount) { strongSelf.loadRetryCount++; [strongSelf _loadChapterAtIndex:index completion:completion]; } else { strongSelf.loadRetryCount = 0; if (completion) completion(nil, error); } } }); }); } /// Fetches and typesets a chapter (placeholder for the real network/cache logic). - (WRChapterData * __nullable)_fetchChapterDataForIndex:(NSUInteger)index error:(NSError **)errorOut { // In the real app, this method: // 1. Checks local SQLite/LevelDB cache for the chapter content. // 2. If not cached, makes an API request to the WeRead server. // 3. Receives chapter HTML (potentially encrypted/obfuscated). // 4. Decrypts and parses the HTML into an NSAttributedString. // 5. Creates a WRCoreTextLayouter with the current typesetter attributes. // 6. Runs the layouter to compute line breaks and page breaks. // 7. Returns the populated WRChapterData. // Placeholder: return nil to simulate a network fetch that needs to happen // in the real binary. if (errorOut) { *errorOut = [NSError errorWithDomain:@"com.weread.reader" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Not implemented in reconstruction"}]; } return nil; } /// Returns the chapter data for the given index (from cache or nil). - (WRChapterData * __nullable)_chapterDataForIndex:(NSUInteger)index { return self.chapterDataCache[@(index)]; } /// Returns the page count for the given chapter data, computing it if needed. - (WRChapterPageCount * __nullable)_pageCountForChapterData:(WRChapterData *)chapterData { if (!chapterData) return nil; NSString *cacheKey = [WRChapterPageCount currentCacheKeyWithBookId:self.bookId]; WRChapterPageCount *cached = self.pageCountCache[cacheKey]; if (cached) return cached; // Compute now. [self initChapterPageCount]; return self.currentChapterPageCount; } /// Re-typesets a chapter data object (after font/spacing changes). - (void)_reTypesetChapterData:(WRChapterData *)chapterData { if (!chapterData.sourceAttributedString) return; // Re-create the layouter with updated attributes. NSDictionary *attrs = [self _currentTypesetterAttributes]; WRCoreTextLayouter *layouter = [[WRCoreTextLayouter alloc] initWithAttributedString:chapterData.sourceAttributedString attributes:attrs]; chapterData.layouter = layouter; // Re-generate the typeset attributed string. chapterData.typesetAttributedString = [layouter typesetAttributedString]; // Re-generate page ranges. [self initChapterPageCount]; } // ============================================================================ #pragma mark - Chapter Prefetching // ============================================================================ /// Prefetches the next and previous chapters so they're ready when the /// user flips to them. - (void)_prefetchAdjacentChaptersForIndex:(NSUInteger)index { // Prefetch next chapter. if (index + 1 < self.totalChapters) { NSUInteger nextIdx = index + 1; if (!self.chapterDataCache[@(nextIdx)]) { dispatch_async(self.chapterLoadQueue, ^{ [self _fetchChapterDataForIndex:nextIdx error:NULL]; }); } } // Prefetch previous chapter. if (index > 0) { NSUInteger prevIdx = index - 1; if (!self.chapterDataCache[@(prevIdx)]) { dispatch_async(self.chapterLoadQueue, ^{ [self _fetchChapterDataForIndex:prevIdx error:NULL]; }); } } } // ============================================================================ #pragma mark - Display Helpers // ============================================================================ /// Displays a chapter at the given page index. - (void)_displayChapter:(WRChapterData *)chapterData atPageIndex:(NSUInteger)pageIndex animated:(BOOL)animated { self.currentChapterData = chapterData; [self initChapterPageCount]; // Create a page view for the initial display. WRPageView *pageView = [[WRPageView alloc] initWithFrame:self.view.bounds]; [self.activePageViews removeAllObjects]; [self.activePageViews addObject:pageView]; // Render the page. WRReadingProgress *progress = [self.readingProgress copy]; progress.pageIndex = pageIndex; [self renderPageView:pageView progressData:progress source:@"displayChapter"]; // Set the page view controller's initial view controller. UIViewController *pageContentVC = [[UIViewController alloc] init]; pageContentVC.view = pageView; [self.pageViewController setViewControllers:@[pageContentVC] direction:UIPageViewControllerNavigationDirectionForward animated:animated completion:nil]; } /// Shows an error state when chapter loading fails. - (void)_showChapterLoadError:(NSError *)error { NSLog(@"[WRReader] Chapter load error: %@", error.localizedDescription); // In the real app, this shows a toast or error overlay. } /// Calculates and updates the overall read percentage. - (void)_updateReadPercentage { NSUInteger totalChapters = self.totalChapters; if (totalChapters == 0) return; NSUInteger currentChapter = self.readingProgress.chapterIndex; WRChapterPageCount *pageCount = self.currentChapterPageCount; NSUInteger totalPages = pageCount.totalPages; NSUInteger currentPage = self.readingProgress.pageIndex; // Calculate: (chaptersCompleted + currentPage/totalPages) / totalChapters double chapterProgress = (totalPages > 0) ? (double)currentPage / (double)totalPages : 0.0; double overall = ((double)currentChapter + chapterProgress) / (double)totalChapters; self.readingProgress.readPercentage = MIN(MAX(overall, 0.0), 1.0); } // ============================================================================ #pragma mark - Auto-Read // ============================================================================ /// Starts the auto-read timer. - (void)_startAutoRead { [_autoReadTimer invalidate]; _autoReadTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(_autoReadTick) userInfo:nil repeats:YES]; } /// Stops auto-read. - (void)_stopAutoRead { [_autoReadTimer invalidate]; _autoReadTimer = nil; } /// Called each auto-read tick to advance the page. - (void)_autoReadTick { BOOL advanced = [self.pageViewController goToNextPageAnimated:YES]; if (!advanced) { // At the end of the chapter, try to go to the next chapter. if (self.readingProgress.chapterIndex + 1 < self.totalChapters) { [self jumpReadingToNextChapter]; } else { // At the end of the book, stop auto-read. [self _stopAutoRead]; } } } // ============================================================================ #pragma mark - Notification Handlers // ============================================================================ /// Handles memory warning by purging non-current chapter data from cache. - (void)_handleMemoryWarning:(NSNotification *)note { NSLog(@"[WRReader] Memory warning received, purging chapter cache."); NSUInteger currentIdx = self.readingProgress.chapterIndex; WRChapterData *currentData = self.chapterDataCache[@(currentIdx)]; [self.chapterDataCache removeAllObjects]; if (currentData) { self.chapterDataCache[@(currentIdx)] = currentData; } [self.pageCountCache removeAllObjects]; } /// Handles the auto-read advance notification from WRPageView. - (void)_handleAutoReadAdvance:(NSNotification *)note { if (!_autoReadEnabled) return; [self _autoReadTick]; } /// Handles font size change notification from WRPageView. - (void)_handleFontSizeChange:(NSNotification *)note { NSInteger delta = [note.userInfo[@"delta"] integerValue]; if (delta == 0) return; [self changeTypesetterAttributesWithBlock:^(NSMutableDictionary *attrs) { CGFloat currentSize = [attrs[@"fontSize"] floatValue]; CGFloat newSize = currentSize + (CGFloat)delta; newSize = MAX(12.0, MIN(36.0, newSize)); // Clamp to reasonable range. attrs[@"fontSize"] = @(newSize); }]; } // ============================================================================ #pragma mark - Book Data Loading // ============================================================================ /// Loads book metadata from cache or server. - (void)_loadBookDataWithBookId:(NSString *)bookId { // In the real app, this makes an API call to fetch book metadata: // - Title, author, cover image URL // - Chapter list (IDs, titles) // - User's reading progress (if synced) // - Trial/free chapter limits // // The response populates self.book and self.readingProgress. } // ============================================================================ #pragma mark - User Preferences // ============================================================================ /// Returns the user's preferred page flipping style from UserDefaults. - (WRPageFlippingStyle)_userPreferredFlipStyle { NSInteger style = [[NSUserDefaults standardUserDefaults] integerForKey:@"WRReaderFlipStyle"]; return (WRPageFlippingStyle)style; } /// Periodic progress save callback. - (void)_periodicProgressSave { [self _saveReadingProgressAndIsAsync:YES]; } // ============================================================================ #pragma mark - WRPageViewControllerDelegate // ============================================================================ /// Called when the page view controller finishes a page transition. - (void)pageViewController:(WRPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed { if (completed) { [self didFlipPage]; } } /// Called before a page transition begins. - (void)pageViewController:(WRPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers { // Pre-render the upcoming page view. if (pendingViewControllers.count > 0) { // The pending VC's page view needs its layout frame set. // This happens in renderPageView: when the transition completes. } } // ============================================================================ #pragma mark - Total Chapters (computed) // ============================================================================ /// Returns the total number of chapters in the book. - (NSUInteger)totalChapters { // In the real app, this comes from the book model. return self.book.chapterCount ?: 0; } @end