ReadViewSDK/Doc/WXRead/decompiled/WRPageMark.m
2026-05-21 19:40:51 +08:00

382 lines
12 KiB
Objective-C

//
// 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<WRPageHighlight *> *_mutableHighlights;
NSMutableArray<WRPageUnderline *> *_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<NSString *> *)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<WRPageMark *> *)marksSortedByPosition:(NSArray<WRPageMark *> *)marks
{
return [marks sortedArrayUsingSelector:@selector(compareByPosition:)];
}
+ (NSArray<WRPageMark *> *)marksSortedByDate:(NSArray<WRPageMark *> *)marks
{
return [marks sortedArrayUsingSelector:@selector(compareByDate:)];
}
+ (NSArray<WRPageMark *> *)marksInChapter:(NSString *)chapterUid
fromMarks:(NSArray<WRPageMark *> *)marks
{
return [marks filteredArrayUsingPredicate:
[NSPredicate predicateWithFormat:@"chapterUid == %@", chapterUid]];
}
+ (NSArray<WRPageMark *> *)marksInRange:(NSRange)range
fromMarks:(NSArray<WRPageMark *> *)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:
@"<WRPageMark: %@ ch=%@ offset=%ld page=%ld highlights=%lu underlines=%lu>",
_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