ReadViewSDK/Doc/WXRead/decompiled/WRReaderViewController.m

877 lines
34 KiB
Objective-C

//
// 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<NSNumber *, WRChapterData *> *chapterDataCache;
@property (nonatomic, strong) NSMutableDictionary<NSString *, WRChapterPageCount *> *pageCountCache;
@property (nonatomic, strong) NSMutableArray<WRPageView *> *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<UIViewController *> *)previousViewControllers
transitionCompleted:(BOOL)completed {
if (completed) {
[self didFlipPage];
}
}
/// Called before a page transition begins.
- (void)pageViewController:(WRPageViewController *)pageViewController
willTransitionToViewControllers:(NSArray<UIViewController *> *)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