382 lines
12 KiB
Objective-C
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
|