877 lines
34 KiB
Objective-C
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
|