// // 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: @"", _underlineId, (long)_style, (long)_startPos, (long)_endPos, [_markedText substringToIndex:MIN(30, _markedText.length)]]; } @end