feat: 交互协调器拆分、附件提示、暗色图片适配、选区放大镜及文档清理
- 拆分 ContentDelegates/TextContentView 为独立协调器(InteractionCoordinator、LocationResolution、ExternalLinks、AttachmentTooltip) - 新增 RDEPUBAttachmentTooltipView/OverlayView 附件气泡提示 - 新增 RDEPUBDarkImageAdjuster 暗色模式图片亮度适配 - 新增 RDEPUBSelectionLoupeView 选区放大镜 - 新增 MetadataParseWorker/CancellationController 元数据解析取消机制 - 重构 PresentationRuntime/PaginationCoordinator 精简职责 - 优化 ChapterLoader/WarmupOrchestrator 异步章节加载 - CFI 模块微调与 NoteModels 更新 - 清理冗余文档,更新架构/UML/业务逻辑文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
extension RDEPUBReaderController {
|
||||
|
||||
func presentAttachmentTooltip(text: String, sourceView: UIView, sourceRect: CGRect, sourcePoint: CGPoint) {
|
||||
hideAttachmentTooltipIfNeeded()
|
||||
|
||||
let overlay = RDEPUBAttachmentTooltipOverlayView(frame: view.bounds)
|
||||
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
overlay.onBackgroundTap = { [weak self, weak overlay] in
|
||||
guard let self, let overlay else { return }
|
||||
self.dismissAttachmentTooltipOverlay(overlay)
|
||||
}
|
||||
|
||||
let tooltip = RDEPUBAttachmentTooltipView()
|
||||
tooltip.alpha = 0
|
||||
let horizontalPadding = max(view.safeAreaInsets.left, view.safeAreaInsets.right) + 12
|
||||
tooltip.configure(text: text, maxWidth: min(view.bounds.width - horizontalPadding * 2, 320))
|
||||
|
||||
let anchorRect = sourceView.convert(sourceRect, to: view)
|
||||
let rawAnchorPoint = sourceView.convert(sourcePoint, to: view)
|
||||
let anchorPoint = CGPoint(
|
||||
x: min(max(rawAnchorPoint.x, anchorRect.minX), anchorRect.maxX),
|
||||
y: min(max(rawAnchorPoint.y, anchorRect.minY), anchorRect.maxY)
|
||||
)
|
||||
let verticalSpacing: CGFloat = 6
|
||||
let tooltipSize = tooltip.frame.size
|
||||
let idealX = anchorPoint.x - tooltipSize.width / 2
|
||||
let minX = horizontalPadding
|
||||
let maxX = max(minX, view.bounds.width - horizontalPadding - tooltipSize.width)
|
||||
let originX = min(max(idealX, minX), maxX)
|
||||
let topSafeY = view.safeAreaInsets.top + 12
|
||||
let bottomSafeY = view.bounds.height - view.safeAreaInsets.bottom - 12
|
||||
let availableSpaceAbove = anchorRect.minY - topSafeY
|
||||
let availableSpaceBelow = bottomSafeY - anchorRect.maxY
|
||||
let prefersAbove = availableSpaceAbove >= tooltipSize.height + verticalSpacing || availableSpaceAbove >= availableSpaceBelow
|
||||
let tooltipPlacement: RDEPUBAttachmentTooltipView.ArrowPlacement = prefersAbove ? .bottom : .top
|
||||
let originY: CGFloat
|
||||
switch tooltipPlacement {
|
||||
case .bottom:
|
||||
originY = max(topSafeY, anchorRect.minY - tooltipSize.height - verticalSpacing)
|
||||
case .top:
|
||||
originY = min(bottomSafeY - tooltipSize.height, anchorRect.maxY + verticalSpacing)
|
||||
}
|
||||
let arrowTipX = min(
|
||||
max(anchorPoint.x - originX, tooltip.minimumArrowX),
|
||||
tooltipSize.width - tooltip.minimumArrowX
|
||||
)
|
||||
|
||||
tooltip.setArrowTipX(arrowTipX, placement: tooltipPlacement)
|
||||
tooltip.frame.origin = CGPoint(x: originX, y: originY)
|
||||
overlay.addSubview(tooltip)
|
||||
view.addSubview(overlay)
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
tooltip.alpha = 1
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self, weak overlay] in
|
||||
guard let self, let overlay else { return }
|
||||
self.dismissAttachmentTooltipOverlay(overlay)
|
||||
}
|
||||
}
|
||||
|
||||
private func hideAttachmentTooltipIfNeeded() {
|
||||
view.subviews
|
||||
.compactMap { $0 as? RDEPUBAttachmentTooltipOverlayView }
|
||||
.forEach { $0.removeFromSuperview() }
|
||||
}
|
||||
|
||||
private func dismissAttachmentTooltipOverlay(_ overlay: RDEPUBAttachmentTooltipOverlayView) {
|
||||
UIView.animate(withDuration: 0.18, animations: {
|
||||
overlay.tooltipView?.alpha = 0
|
||||
}, completion: { _ in
|
||||
overlay.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user