// // WRPageMark.m // WeRead (微信读书) // Reverse-engineered implementation reconstruction // // 25 methods identified from binary. WRPageMark is the most comprehensive // annotation model, handling page bookmarks with linked highlights // and underlines. // #import "WRPageMark.h" #import "WRBookmark.h" #import "WRPageHighlight.h" #import "WRPageUnderline.h" #pragma mark - Implementation @implementation WRPageMark { NSMutableArray *_mutableHighlights; NSMutableArray *_mutableUnderlines; } #pragma mark - Initialization - (instancetype)initWithBookId:(NSString *)bookId chapterUid:(NSString *)chapterUid chapterOffset:(NSInteger)offset pageIndex:(NSInteger)pageIndex chapterTitle:(NSString *)title { self = [super init]; if (self) { _markId = [[NSUUID UUID] UUIDString]; _bookId = [bookId copy]; _chapterUid = [chapterUid copy]; _chapterOffset = offset; _pageIndex = pageIndex; _chapterTitle = [title copy]; _createTime = [[NSDate date] timeIntervalSince1970]; _updateTime = _createTime; _isSynced = NO; _displayMode = WRPageMarkDisplayModeIcon; _markColor = [UIColor colorWithRed:0.9 green:0.3 blue:0.2 alpha:1.0]; _mutableHighlights = [NSMutableArray array]; _mutableUnderlines = [NSMutableArray array]; _linkedHighlights = @[]; _linkedUnderlines = @[]; } return self; } #pragma mark - Conversion - (WRBookmark *)toBookmark { WRBookmark *bm = [WRBookmark bookmarkWithBookId:_bookId chapterUid:_chapterUid offset:_chapterOffset text:_excerptText ?: @"" type:WRBookmarkTypeMark]; bm.bookmarkId = _markId; bm.chapterIndex = _pageIndex; bm.chapterOffset = _chapterOffset; bm.createTime = _createTime; bm.updateTime = _updateTime; bm.isSynced = _isSynced; bm.syncKey = _syncKey; // Store linked annotation info in extra metadata NSMutableDictionary *meta = [NSMutableDictionary dictionary]; if (_linkedHighlights.count > 0) { NSMutableArray *ids = [NSMutableArray array]; for (WRPageHighlight *h in _linkedHighlights) { [ids addObject:h.highlightId]; } meta[@"linkedHighlightIds"] = ids; } if (_linkedUnderlines.count > 0) { NSMutableArray *ids = [NSMutableArray array]; for (WRPageUnderline *u in _linkedUnderlines) { [ids addObject:u.underlineId]; } meta[@"linkedUnderlineIds"] = ids; } bm.extraMetadata = meta; return bm; } + (nullable instancetype)fromBookmark:(WRBookmark *)bookmark { if (!bookmark || bookmark.type != WRBookmarkTypeMark) return nil; WRPageMark *mark = [[WRPageMark alloc] initWithBookId:bookmark.bookId chapterUid:bookmark.chapterUid chapterOffset:bookmark.chapterOffset pageIndex:bookmark.chapterIndex chapterTitle:@""]; // Title resolved separately mark.markId = bookmark.bookmarkId; mark.excerptText = bookmark.markText; mark.createTime = bookmark.createTime; mark.updateTime = bookmark.updateTime; mark.isSynced = bookmark.isSynced; mark.syncKey = bookmark.syncKey; return mark; } #pragma mark - Linked Annotations - (void)addLinkedHighlight:(WRPageHighlight *)highlight { if (!highlight) return; [_mutableHighlights addObject:highlight]; _linkedHighlights = [_mutableHighlights copy]; _updateTime = [[NSDate date] timeIntervalSince1970]; } - (void)addLinkedUnderline:(WRPageUnderline *)underline { if (!underline) return; [_mutableUnderlines addObject:underline]; _linkedUnderlines = [_mutableUnderlines copy]; _updateTime = [[NSDate date] timeIntervalSince1970]; } - (void)removeLinkedHighlightWithId:(NSString *)highlightId { [_mutableHighlights filterUsingPredicate: [NSPredicate predicateWithFormat:@"highlightId != %@", highlightId]]; _linkedHighlights = [_mutableHighlights copy]; _updateTime = [[NSDate date] timeIntervalSince1970]; } - (void)removeLinkedUnderlineWithId:(NSString *)underlineId { [_mutableUnderlines filterUsingPredicate: [NSPredicate predicateWithFormat:@"underlineId != %@", underlineId]]; _linkedUnderlines = [_mutableUnderlines copy]; _updateTime = [[NSDate date] timeIntervalSince1970]; } - (NSArray *)allLinkedAnnotationIds { NSMutableArray *ids = [NSMutableArray array]; for (WRPageHighlight *h in _linkedHighlights) { [ids addObject:h.highlightId]; } for (WRPageUnderline *u in _linkedUnderlines) { [ids addObject:u.underlineId]; } return [ids copy]; } #pragma mark - Display - (NSString *)displayTitle { if (_chapterTitle.length > 0) { return _chapterTitle; } if (_excerptText.length > 0) { return [_excerptText substringToIndex:MIN(50, _excerptText.length)]; } return [NSString stringWithFormat:@"Page %ld", (long)_pageIndex]; } - (NSString *)displaySubtitle { NSMutableArray *parts = [NSMutableArray array]; if (_linkedHighlights.count > 0) { [parts addObject:[NSString stringWithFormat:@"%lu highlights", (unsigned long)_linkedHighlights.count]]; } if (_linkedUnderlines.count > 0) { [parts addObject:[NSString stringWithFormat:@"%lu underlines", (unsigned long)_linkedUnderlines.count]]; } [parts addObject:[self formattedDate]]; return [parts componentsJoinedByString:@" | "]; } - (NSString *)formattedDate { NSDate *date = [NSDate dateWithTimeIntervalSince1970:_createTime]; NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; fmt.dateFormat = @"yyyy-MM-dd HH:mm"; return [fmt stringFromDate:date]; } - (UIImage *)markIcon { // Generate a bookmark icon image programmatically CGSize size = CGSizeMake(24, 32); UIGraphicsBeginImageContextWithOptions(size, NO, 0); UIColor *color = _markColor ?: [UIColor redColor]; [color setFill]; // Draw a bookmark/flag shape UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(0, 0)]; [path addLineToPoint:CGPointMake(size.width, 0)]; [path addLineToPoint:CGPointMake(size.width, size.height - 6)]; [path addLineToPoint:CGPointMake(size.width / 2, size.height - 12)]; [path addLineToPoint:CGPointMake(0, size.height - 6)]; [path closePath]; [path fill]; UIImage *icon = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return icon; } #pragma mark - Sorting & Filtering - (NSComparisonResult)compareByPosition:(WRPageMark *)other { if (![_chapterUid isEqualToString:other.chapterUid]) { // Compare by chapter order (would need chapter index lookup) return [_chapterUid compare:other.chapterUid]; } if (_chapterOffset < other.chapterOffset) return NSOrderedAscending; if (_chapterOffset > other.chapterOffset) return NSOrderedDescending; return NSOrderedSame; } - (NSComparisonResult)compareByDate:(WRPageMark *)other { if (_createTime < other.createTime) return NSOrderedAscending; if (_createTime > other.createTime) return NSOrderedDescending; return NSOrderedSame; } - (BOOL)isInChapter:(NSString *)chapterUid { return [_chapterUid isEqualToString:chapterUid]; } #pragma mark - Serialization - (NSDictionary *)toDictionary { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; dict[@"markId"] = _markId ?: @""; dict[@"bookId"] = _bookId ?: @""; dict[@"chapterUid"] = _chapterUid ?: @""; dict[@"chapterOffset"] = @(_chapterOffset); dict[@"pageIndex"] = @(_pageIndex); dict[@"chapterTitle"] = _chapterTitle ?: @""; dict[@"excerptText"] = _excerptText ?: @""; dict[@"createTime"] = @(_createTime); dict[@"updateTime"] = @(_updateTime); dict[@"isSynced"] = @(_isSynced); if (_syncKey) dict[@"syncKey"] = _syncKey; dict[@"displayMode"] = @(_displayMode); if (_linkedHighlights.count > 0) { NSMutableArray *highlights = [NSMutableArray array]; for (WRPageHighlight *h in _linkedHighlights) { [highlights addObject:@{@"id": h.highlightId, @"text": h.markedText ?: @""}]; } dict[@"linkedHighlights"] = highlights; } if (_linkedUnderlines.count > 0) { NSMutableArray *underlines = [NSMutableArray array]; for (WRPageUnderline *u in _linkedUnderlines) { [underlines addObject:@{@"id": u.underlineId, @"text": u.markedText ?: @""}]; } dict[@"linkedUnderlines"] = underlines; } return [dict copy]; } + (nullable instancetype)fromDictionary:(NSDictionary *)dict { if (!dict) return nil; WRPageMark *mark = [[WRPageMark alloc] initWithBookId:dict[@"bookId"] chapterUid:dict[@"chapterUid"] chapterOffset:[dict[@"chapterOffset"] integerValue] pageIndex:[dict[@"pageIndex"] integerValue] chapterTitle:dict[@"chapterTitle"] ?: @""]; mark.markId = dict[@"markId"]; mark.excerptText = dict[@"excerptText"]; mark.createTime = [dict[@"createTime"] doubleValue]; mark.updateTime = [dict[@"updateTime"] doubleValue]; mark.isSynced = [dict[@"isSynced"] boolValue]; mark.syncKey = dict[@"syncKey"]; mark.displayMode = [dict[@"displayMode"] integerValue]; return mark; } - (nullable NSData *)toJSONData { NSDictionary *dict = [self toDictionary]; return [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; } + (nullable instancetype)fromJSONData:(NSData *)data { if (!data) return nil; NSError *error = nil; NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (![dict isKindOfClass:[NSDictionary class]]) return nil; return [self fromDictionary:dict]; } #pragma mark - Batch Operations + (NSArray *)marksSortedByPosition:(NSArray *)marks { return [marks sortedArrayUsingSelector:@selector(compareByPosition:)]; } + (NSArray *)marksSortedByDate:(NSArray *)marks { return [marks sortedArrayUsingSelector:@selector(compareByDate:)]; } + (NSArray *)marksInChapter:(NSString *)chapterUid fromMarks:(NSArray *)marks { return [marks filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"chapterUid == %@", chapterUid]]; } + (NSArray *)marksInRange:(NSRange)range fromMarks:(NSArray *)marks { NSInteger start = (NSInteger)range.location; NSInteger end = start + (NSInteger)range.length; return [marks filteredArrayUsingPredicate: [NSPredicate predicateWithFormat: @"chapterOffset >= %ld AND chapterOffset < %ld", start, end]]; } #pragma mark - NSObject - (NSString *)description { return [NSString stringWithFormat: @"", _markId, _chapterUid, (long)_chapterOffset, (long)_pageIndex, (unsigned long)_linkedHighlights.count, (unsigned long)_linkedUnderlines.count]; } - (BOOL)isEqual:(id)object { if (self == object) return YES; if (![object isKindOfClass:[WRPageMark class]]) return NO; return [self.markId isEqualToString:((WRPageMark *)object).markId]; } - (NSUInteger)hash { return self.markId.hash; } @end