262 lines
7.7 KiB
Objective-C
262 lines
7.7 KiB
Objective-C
//
|
|
// WRPageUnderline.m
|
|
// WeRead (微信读书)
|
|
// Reverse-engineered implementation reconstruction
|
|
//
|
|
// 10 methods identified from binary. WRPageUnderline renders underline
|
|
// annotations with configurable styles (solid, dashed, wavy, dotted).
|
|
//
|
|
|
|
#import "WRPageUnderline.h"
|
|
#import "WRBookmark.h"
|
|
|
|
#pragma mark - Implementation
|
|
|
|
@implementation WRPageUnderline
|
|
|
|
#pragma mark - Initialization
|
|
|
|
- (instancetype)initWithBookId:(NSString *)bookId
|
|
chapterUid:(NSString *)chapterUid
|
|
startPos:(NSInteger)startPos
|
|
endPos:(NSInteger)endPos
|
|
text:(NSString *)text
|
|
style:(WRUnderlineStyle)style
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_underlineId = [[NSUUID UUID] UUIDString];
|
|
_bookId = [bookId copy];
|
|
_chapterUid = [chapterUid copy];
|
|
_startPos = startPos;
|
|
_endPos = endPos;
|
|
_chapterOffset = startPos;
|
|
_markedText = [text copy];
|
|
_style = style;
|
|
_color = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.8];
|
|
_createTime = [[NSDate date] timeIntervalSince1970];
|
|
_isSynced = NO;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#pragma mark - Conversion
|
|
|
|
- (WRBookmark *)toBookmark
|
|
{
|
|
WRBookmark *bm = [WRBookmark bookmarkWithBookId:_bookId
|
|
chapterUid:_chapterUid
|
|
offset:_startPos
|
|
text:_markedText
|
|
type:WRBookmarkTypeUnderline];
|
|
bm.bookmarkId = _underlineId;
|
|
bm.startPos = _startPos;
|
|
bm.endPos = _endPos;
|
|
bm.rangeLength = _endPos - _startPos;
|
|
bm.chapterOffset = _chapterOffset;
|
|
bm.noteContent = _noteContent;
|
|
bm.createTime = _createTime;
|
|
bm.isSynced = _isSynced;
|
|
|
|
// Store underline style in extra metadata
|
|
bm.extraMetadata = @{@"underlineStyle": @(_style)};
|
|
|
|
return bm;
|
|
}
|
|
|
|
+ (nullable instancetype)fromBookmark:(WRBookmark *)bookmark
|
|
{
|
|
if (!bookmark || bookmark.type != WRBookmarkTypeUnderline) return nil;
|
|
|
|
WRPageUnderline *ul = [[WRPageUnderline alloc]
|
|
initWithBookId:bookmark.bookId
|
|
chapterUid:bookmark.chapterUid
|
|
startPos:bookmark.startPos
|
|
endPos:bookmark.endPos
|
|
text:bookmark.markText
|
|
style:[bookmark.extraMetadata[@"underlineStyle"] integerValue]];
|
|
|
|
ul.underlineId = bookmark.bookmarkId;
|
|
ul.chapterOffset = bookmark.chapterOffset;
|
|
ul.noteContent = bookmark.noteContent;
|
|
ul.createTime = bookmark.createTime;
|
|
ul.isSynced = bookmark.isSynced;
|
|
|
|
return ul;
|
|
}
|
|
|
|
#pragma mark - Drawing
|
|
|
|
- (UIBezierPath *)underlinePathForRect:(CGRect)rect
|
|
{
|
|
UIBezierPath *path = [UIBezierPath bezierPath];
|
|
CGFloat y = CGRectGetMaxY(rect) - 2.0; // 2pt below baseline
|
|
|
|
switch (_style) {
|
|
case WRUnderlineStyleSolid: {
|
|
[path moveToPoint:CGPointMake(CGRectGetMinX(rect), y)];
|
|
[path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), y)];
|
|
path.lineWidth = 1.5;
|
|
break;
|
|
}
|
|
|
|
case WRUnderlineStyleDashed: {
|
|
[path moveToPoint:CGPointMake(CGRectGetMinX(rect), y)];
|
|
[path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), y)];
|
|
path.lineWidth = 1.5;
|
|
// Set dash pattern: 6pt dash, 3pt gap
|
|
CGFloat pattern[] = {6.0, 3.0};
|
|
[path setLineDash:pattern count:2 phase:0];
|
|
break;
|
|
}
|
|
|
|
case WRUnderlineStyleWavy: {
|
|
// Wavy underline: sine wave approximation
|
|
CGFloat startX = CGRectGetMinX(rect);
|
|
CGFloat endX = CGRectGetMaxX(rect);
|
|
CGFloat width = endX - startX;
|
|
CGFloat amplitude = 2.0;
|
|
CGFloat wavelength = 8.0;
|
|
|
|
[path moveToPoint:CGPointMake(startX, y)];
|
|
|
|
for (CGFloat x = startX; x < endX; x += 1.0) {
|
|
CGFloat progress = (x - startX) / wavelength;
|
|
CGFloat waveY = y + sin(progress * M_PI * 2) * amplitude;
|
|
[path addLineToPoint:CGPointMake(x, waveY)];
|
|
}
|
|
|
|
path.lineWidth = 1.0;
|
|
break;
|
|
}
|
|
|
|
case WRUnderlineStyleDotted: {
|
|
[path moveToPoint:CGPointMake(CGRectGetMinX(rect), y)];
|
|
[path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), y)];
|
|
path.lineWidth = 2.0;
|
|
CGFloat pattern[] = {1.0, 4.0};
|
|
[path setLineDash:pattern count:2 phase:0];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
- (UIColor *)underlineColor
|
|
{
|
|
return _color ?: [UIColor darkGrayColor];
|
|
}
|
|
|
|
#pragma mark - Public Methods
|
|
|
|
- (void)setStyle:(WRUnderlineStyle)style
|
|
{
|
|
_style = style;
|
|
}
|
|
|
|
- (void)setNote:(NSString *)note
|
|
{
|
|
_noteContent = [note copy];
|
|
}
|
|
|
|
- (BOOL)containsPosition:(NSInteger)position
|
|
{
|
|
return position >= _startPos && position < _endPos;
|
|
}
|
|
|
|
- (BOOL)mergeWithUnderline:(WRPageUnderline *)other
|
|
{
|
|
if (!other) return NO;
|
|
|
|
// Check if ranges are adjacent or overlapping
|
|
if (other.endPos < _startPos - 1 || other.startPos > _endPos + 1) {
|
|
return NO; // Not adjacent
|
|
}
|
|
|
|
// Check same book and chapter
|
|
if (![_bookId isEqualToString:other.bookId] ||
|
|
![_chapterUid isEqualToString:other.chapterUid]) {
|
|
return NO;
|
|
}
|
|
|
|
// Merge ranges
|
|
_startPos = MIN(_startPos, other.startPos);
|
|
_endPos = MAX(_endPos, other.endPos);
|
|
|
|
// Merge text (preserve order)
|
|
if (other.startPos < _startPos) {
|
|
_markedText = [other.markedText stringByAppendingString:_markedText];
|
|
} else {
|
|
_markedText = [_markedText stringByAppendingString:other.markedText];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (NSInteger)rangeLength
|
|
{
|
|
return _endPos - _startPos;
|
|
}
|
|
|
|
- (NSDictionary *)toDictionary
|
|
{
|
|
return @{
|
|
@"underlineId" : _underlineId ?: @"",
|
|
@"bookId" : _bookId ?: @"",
|
|
@"chapterUid" : _chapterUid ?: @"",
|
|
@"startPos" : @(_startPos),
|
|
@"endPos" : @(_endPos),
|
|
@"chapterOffset" : @(_chapterOffset),
|
|
@"markedText" : _markedText ?: @"",
|
|
@"style" : @(_style),
|
|
@"pageIndex" : @(_pageIndex),
|
|
@"createTime" : @(_createTime),
|
|
@"isSynced" : @(_isSynced),
|
|
@"noteContent" : _noteContent ?: @"",
|
|
};
|
|
}
|
|
|
|
+ (nullable instancetype)fromDictionary:(NSDictionary *)dict
|
|
{
|
|
if (!dict) return nil;
|
|
|
|
WRPageUnderline *ul = [[WRPageUnderline alloc]
|
|
initWithBookId:dict[@"bookId"]
|
|
chapterUid:dict[@"chapterUid"]
|
|
startPos:[dict[@"startPos"] integerValue]
|
|
endPos:[dict[@"endPos"] integerValue]
|
|
text:dict[@"markedText"]
|
|
style:[dict[@"style"] integerValue]];
|
|
|
|
ul.underlineId = dict[@"underlineId"];
|
|
ul.chapterOffset = [dict[@"chapterOffset"] integerValue];
|
|
ul.pageIndex = [dict[@"pageIndex"] integerValue];
|
|
ul.createTime = [dict[@"createTime"] doubleValue];
|
|
ul.isSynced = [dict[@"isSynced"] boolValue];
|
|
ul.noteContent = dict[@"noteContent"];
|
|
|
|
return ul;
|
|
}
|
|
|
|
- (NSComparisonResult)compareTo:(WRPageUnderline *)other
|
|
{
|
|
if (_startPos < other.startPos) return NSOrderedAscending;
|
|
if (_startPos > other.startPos) return NSOrderedDescending;
|
|
if (_endPos < other.endPos) return NSOrderedAscending;
|
|
if (_endPos > other.endPos) return NSOrderedDescending;
|
|
return NSOrderedSame;
|
|
}
|
|
|
|
#pragma mark - NSObject
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:
|
|
@"<WRPageUnderline: %@ style=%ld range=[%ld,%ld] '%@'>",
|
|
_underlineId, (long)_style, (long)_startPos, (long)_endPos,
|
|
[_markedText substringToIndex:MIN(30, _markedText.length)]];
|
|
}
|
|
|
|
@end
|