feat: improve epub reader controls and annotations

This commit is contained in:
shen
2026-07-12 12:23:10 +08:00
parent 063a493b18
commit 8ccb7157b2
36 changed files with 2959 additions and 2160 deletions
@@ -1,30 +1,19 @@
import UIKit
/// Compact reader settings drawer. It deliberately keeps the book visible and
/// exposes only the controls currently supported by the reader configuration.
final class RDEPUBReaderSettingsViewController: UIViewController {
var onBrightnessChange: ((CGFloat) -> Void)?
var onFontSizeChange: ((CGFloat) -> Void)?
var onFontChoiceChange: ((RDEPUBReaderFontChoice) -> Void)?
var onLineHeightChange: ((CGFloat) -> Void)?
var onColumnCountChange: ((Int) -> Void)?
var onDisplayTypeChange: ((RDEpubReaderView.DisplayType) -> Void)?
var onThemeChange: ((RDEPUBReaderTheme) -> Void)?
var onDismiss: (() -> Void)?
private enum ThemePreset: Int, CaseIterable {
case light
case yellow
case green
case pink
case blue
case dark
case light, yellow, green, pink, blue, dark
var title: String {
switch self {
@@ -49,50 +38,24 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
}
}
private let scrollView = UIScrollView()
private let contentStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 20
return stackView
}()
private let handleView = UIView()
private let closeButton = UIButton(type: .system)
private let brightnessSlider = UISlider()
private let fontValueLabel = UILabel()
private let decreaseFontButton = UIButton(type: .system)
private let increaseFontButton = UIButton(type: .system)
private let fontChoiceControl = UISegmentedControl(items: RDEPUBReaderFontChoice.allCases.map(\.displayName))
private let lineHeightControl = UISegmentedControl(items: ["紧凑", "标准", "宽松"])
private let columnCountControl = UISegmentedControl(items: ["单栏", "双栏"])
private let displayTypeControl = UISegmentedControl(items: ["仿真", "横滑", "竖滑"])
private let themeStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = 12
return stackView
}()
private let fontSizeSlider = UISlider()
private let fontSizeLabel = UILabel()
private let fontChoiceButton = UIButton(type: .system)
private let lineHeightButton = UIButton(type: .system)
private let displayTypeButton = UIButton(type: .system)
private let themeStack = UIStackView()
private var themeButtons: [UIButton] = []
private let lineHeightValues: [CGFloat] = [1.3, 1.6, 1.9]
private var currentConfiguration: RDEPUBReaderConfiguration
private let lineHeightValues: [CGFloat] = [1.3, 1.6, 1.9]
private var hasNotifiedDismissal = false
init(configuration: RDEPUBReaderConfiguration, brightness: CGFloat) {
self.currentConfiguration = configuration
currentConfiguration = configuration
super.init(nibName: nil, bundle: nil)
brightnessSlider.value = Float(brightness)
title = "样式设置"
}
required init?(coder: NSCoder) {
@@ -101,262 +64,266 @@ final class RDEPUBReaderSettingsViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationItems()
setupViews()
syncControls()
applyTheme(currentConfiguration.theme)
}
private func setupNavigationItems() {
let doneItem = UIBarButtonItem(title: "完成", style: .done, target: self, action: #selector(doneAction))
doneItem.accessibilityIdentifier = "epub.reader.settings.done"
navigationItem.rightBarButtonItem = doneItem
}
private func setupViews() {
view.addSubview(scrollView)
scrollView.accessibilityIdentifier = "epub.reader.settings.scroll"
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentStack)
contentStack.translatesAutoresizingMaskIntoConstraints = false
view.accessibilityIdentifier = "epub.reader.settings.panel"
view.layer.cornerRadius = 24
view.layer.cornerCurve = .continuous
view.clipsToBounds = true
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
[handleView, closeButton, brightnessSlider, fontSizeSlider, fontSizeLabel,
fontChoiceButton, lineHeightButton, displayTypeButton, themeStack].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
contentStack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
contentStack.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
contentStack.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
contentStack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -24),
contentStack.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -32)
])
handleView.layer.cornerRadius = 3
closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
closeButton.accessibilityIdentifier = "epub.reader.settings.done"
closeButton.accessibilityLabel = "完成"
closeButton.addTarget(self, action: #selector(doneAction), for: .touchUpInside)
brightnessSlider.minimumValue = 0
brightnessSlider.maximumValue = 1
brightnessSlider.accessibilityIdentifier = "epub.reader.settings.brightness"
brightnessSlider.addTarget(self, action: #selector(brightnessChanged(_:)), for: .valueChanged)
fontValueLabel.font = UIFont.monospacedDigitSystemFont(ofSize: 16, weight: .semibold)
fontValueLabel.textAlignment = .center
fontValueLabel.accessibilityIdentifier = "epub.reader.settings.font.value"
fontValueLabel.setContentHuggingPriority(.required, for: .horizontal)
fontSizeSlider.minimumValue = 12
fontSizeSlider.maximumValue = 36
fontSizeSlider.accessibilityIdentifier = "epub.reader.settings.font.size"
fontSizeSlider.addTarget(self, action: #selector(fontSizeChanged(_:)), for: .valueChanged)
fontSizeLabel.font = UIFont.monospacedDigitSystemFont(ofSize: 14, weight: .semibold)
fontSizeLabel.textAlignment = .center
fontSizeLabel.accessibilityIdentifier = "epub.reader.settings.font.value"
configureFontButton(decreaseFontButton, title: "A-")
configureFontButton(increaseFontButton, title: "A+")
decreaseFontButton.accessibilityIdentifier = "epub.reader.settings.font.decrease"
increaseFontButton.accessibilityIdentifier = "epub.reader.settings.font.increase"
decreaseFontButton.addTarget(self, action: #selector(decreaseFontAction), for: .touchUpInside)
increaseFontButton.addTarget(self, action: #selector(increaseFontAction), for: .touchUpInside)
lineHeightControl.accessibilityIdentifier = "epub.reader.settings.lineHeight"
fontChoiceControl.accessibilityIdentifier = "epub.reader.settings.font.choice"
columnCountControl.accessibilityIdentifier = "epub.reader.settings.columns"
displayTypeControl.accessibilityIdentifier = "epub.reader.settings.displayType"
fontChoiceControl.addTarget(self, action: #selector(fontChoiceChanged(_:)), for: .valueChanged)
lineHeightControl.addTarget(self, action: #selector(lineHeightChanged(_:)), for: .valueChanged)
columnCountControl.addTarget(self, action: #selector(columnCountChanged(_:)), for: .valueChanged)
displayTypeControl.addTarget(self, action: #selector(displayTypeChanged(_:)), for: .valueChanged)
configurePill(fontChoiceButton, title: "字体")
configurePill(lineHeightButton, title: "行距")
configurePill(displayTypeButton, title: "翻页方式")
fontChoiceButton.accessibilityIdentifier = "epub.reader.settings.font.choice"
lineHeightButton.accessibilityIdentifier = "epub.reader.settings.lineHeight"
displayTypeButton.accessibilityIdentifier = "epub.reader.settings.displayType"
fontChoiceButton.addTarget(self, action: #selector(fontChoiceAction), for: .touchUpInside)
lineHeightButton.addTarget(self, action: #selector(lineHeightAction), for: .touchUpInside)
displayTypeButton.addTarget(self, action: #selector(displayTypeAction), for: .touchUpInside)
themeStack.axis = .horizontal
themeStack.distribution = .fillEqually
themeStack.spacing = 14
ThemePreset.allCases.forEach { preset in
let button = UIButton(type: .system)
button.tag = preset.rawValue
button.layer.cornerRadius = 18
button.layer.borderWidth = 1.5
button.backgroundColor = preset.theme.contentBackgroundColor
button.layer.cornerRadius = 15
button.layer.borderWidth = 1
button.accessibilityLabel = preset.title
button.accessibilityIdentifier = "epub.reader.settings.theme.\(preset.rawValue)"
button.addTarget(self, action: #selector(themeButtonAction(_:)), for: .touchUpInside)
themeButtons.append(button)
themeStackView.addArrangedSubview(button)
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 36)
])
themeStack.addArrangedSubview(button)
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
}
contentStack.addArrangedSubview(makeSection(title: "亮度", content: brightnessSlider))
contentStack.addArrangedSubview(makeSection(title: "", content: makeFontSizeRow()))
contentStack.addArrangedSubview(makeSection(title: "字体", content: fontChoiceControl))
contentStack.addArrangedSubview(makeSection(title: "行距", content: lineHeightControl))
contentStack.addArrangedSubview(makeSection(title: "分栏", content: columnCountControl))
contentStack.addArrangedSubview(makeSection(title: "翻页方式", content: displayTypeControl))
contentStack.addArrangedSubview(makeSection(title: "主题", content: themeStackView))
}
let brightnessLabel = makeLabel("亮度")
let fontSizeTitle = makeLabel("体大小")
let smallA = makeLabel("A", font: 18)
let largeA = makeLabel("A", font: 25)
[brightnessLabel, fontSizeTitle, smallA, largeA].forEach(view.addSubview)
private func makeSection(title: String, content: UIView) -> UIView {
let container = UIStackView()
container.axis = .vertical
container.spacing = 10
let selectorStack = UIStackView(arrangedSubviews: [fontChoiceButton, lineHeightButton, displayTypeButton])
selectorStack.axis = .horizontal
selectorStack.distribution = .fillEqually
selectorStack.spacing = 10
selectorStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(selectorStack)
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .semibold)
content.translatesAutoresizingMaskIntoConstraints = false
container.addArrangedSubview(titleLabel)
container.addArrangedSubview(content)
return container
}
private func makeFontSizeRow() -> UIView {
let stackView = UIStackView(arrangedSubviews: [decreaseFontButton, fontValueLabel, increaseFontButton])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 12
let dividerOne = makeDivider()
let dividerTwo = makeDivider()
[dividerOne, dividerTwo].forEach(view.addSubview)
NSLayoutConstraint.activate([
decreaseFontButton.widthAnchor.constraint(equalToConstant: 64),
increaseFontButton.widthAnchor.constraint(equalToConstant: 64),
decreaseFontButton.heightAnchor.constraint(equalToConstant: 36),
increaseFontButton.heightAnchor.constraint(equalToConstant: 36)
handleView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
handleView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
handleView.widthAnchor.constraint(equalToConstant: 42),
handleView.heightAnchor.constraint(equalToConstant: 5),
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
closeButton.centerYAnchor.constraint(equalTo: handleView.centerYAnchor),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32),
brightnessLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
brightnessLabel.topAnchor.constraint(equalTo: handleView.bottomAnchor, constant: 14),
brightnessSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
brightnessSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
brightnessSlider.topAnchor.constraint(equalTo: brightnessLabel.bottomAnchor, constant: 5),
dividerOne.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
dividerOne.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dividerOne.topAnchor.constraint(equalTo: brightnessSlider.bottomAnchor, constant: 10),
fontSizeTitle.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
fontSizeTitle.topAnchor.constraint(equalTo: dividerOne.bottomAnchor, constant: 10),
smallA.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
smallA.centerYAnchor.constraint(equalTo: fontSizeSlider.centerYAnchor),
fontSizeSlider.leadingAnchor.constraint(equalTo: smallA.trailingAnchor, constant: 10),
fontSizeSlider.trailingAnchor.constraint(equalTo: largeA.leadingAnchor, constant: -10),
fontSizeSlider.topAnchor.constraint(equalTo: fontSizeTitle.bottomAnchor, constant: 4),
largeA.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
largeA.centerYAnchor.constraint(equalTo: fontSizeSlider.centerYAnchor),
fontSizeLabel.centerXAnchor.constraint(equalTo: fontSizeSlider.centerXAnchor),
fontSizeLabel.bottomAnchor.constraint(equalTo: fontSizeSlider.topAnchor, constant: -1),
dividerTwo.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
dividerTwo.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dividerTwo.topAnchor.constraint(equalTo: fontSizeSlider.bottomAnchor, constant: 10),
selectorStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
selectorStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectorStack.topAnchor.constraint(equalTo: dividerTwo.bottomAnchor, constant: 12),
selectorStack.heightAnchor.constraint(equalToConstant: 42),
themeStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
themeStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
themeStack.topAnchor.constraint(equalTo: selectorStack.bottomAnchor, constant: 16),
themeStack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -14)
])
return stackView
}
private func configureFontButton(_ button: UIButton, title: String) {
private func makeLabel(_ text: String, font: CGFloat = 13) -> UILabel {
let label = UILabel()
label.text = text
label.font = UIFont.systemFont(ofSize: font, weight: font > 16 ? .regular : .semibold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
private func makeDivider() -> UIView {
let divider = UIView()
divider.translatesAutoresizingMaskIntoConstraints = false
divider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
return divider
}
private func configurePill(_ button: UIButton, title: String) {
button.setTitle(title, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
button.layer.cornerRadius = 18
button.layer.borderWidth = 1
button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
button.layer.cornerRadius = 13
}
private func syncControls() {
fontValueLabel.text = String(Int(currentConfiguration.fontSize.rounded()))
fontChoiceControl.selectedSegmentIndex = RDEPUBReaderFontChoice.allCases.firstIndex(of: currentConfiguration.fontChoice) ?? 0
let lineHeightIndex = lineHeightValues.enumerated().min { abs($0.element - currentConfiguration.lineHeightMultiple) < abs($1.element - currentConfiguration.lineHeightMultiple) }?.offset ?? 1
lineHeightControl.selectedSegmentIndex = lineHeightIndex
columnCountControl.selectedSegmentIndex = currentConfiguration.numberOfColumns > 1 ? 1 : 0
fontSizeSlider.value = Float(currentConfiguration.fontSize)
fontSizeLabel.text = String(Int(currentConfiguration.fontSize.rounded()))
updateSelectorTitles()
updateThemeSelection(ThemePreset.allCases.first(where: { $0.theme == currentConfiguration.theme }) ?? .light)
}
private func updateSelectorTitles() {
fontChoiceButton.setTitle("\(currentConfiguration.fontChoice.displayName) ", for: .normal)
let lineIndex = lineHeightValues.enumerated().min {
abs($0.element - currentConfiguration.lineHeightMultiple) < abs($1.element - currentConfiguration.lineHeightMultiple)
}?.offset ?? 1
fontChoiceButton.accessibilityValue = currentConfiguration.fontChoice.displayName
let lineTitles = ["紧凑", "标准", "宽松"]
lineHeightButton.setTitle("\(lineTitles[lineIndex]) ", for: .normal)
lineHeightButton.accessibilityValue = lineTitles[lineIndex]
let displayTitle: String
switch currentConfiguration.displayType {
case .pageCurl:
displayTypeControl.selectedSegmentIndex = 0
case .horizontalScroll:
displayTypeControl.selectedSegmentIndex = 1
case .verticalScroll:
displayTypeControl.selectedSegmentIndex = 2
case .pageCurl: displayTitle = "仿真"
case .horizontalScroll: displayTitle = "横滑"
case .verticalScroll: displayTitle = "竖滑"
}
let selectedPreset = ThemePreset.allCases.first(where: { $0.theme == currentConfiguration.theme }) ?? .light
updateThemeSelection(selectedPreset)
updateControlAccessibilityValues()
displayTypeButton.setTitle("\(displayTitle) ", for: .normal)
displayTypeButton.accessibilityValue = displayTitle
}
private func applyTheme(_ theme: RDEPUBReaderTheme) {
view.backgroundColor = theme.contentBackgroundColor
scrollView.backgroundColor = theme.contentBackgroundColor
contentStack.arrangedSubviews
.compactMap { $0 as? UIStackView }
.flatMap { $0.arrangedSubviews }
.compactMap { $0 as? UILabel }
.forEach { $0.textColor = theme.contentTextColor }
fontValueLabel.textColor = theme.contentTextColor
[decreaseFontButton, increaseFontButton].forEach { button in
button.setTitleColor(theme.toolControlTextColor, for: .normal)
button.layer.borderColor = theme.toolControlBorderUnselectColor.cgColor
button.backgroundColor = theme.toolBackgroundColor
view.backgroundColor = theme.toolBackgroundColor.withAlphaComponent(0.98)
let textColor = theme.toolControlTextColor
handleView.backgroundColor = textColor.withAlphaComponent(0.45)
closeButton.tintColor = textColor
view.subviews.compactMap { $0 as? UILabel }.forEach { $0.textColor = textColor }
view.subviews.filter { $0 !== handleView && $0 !== closeButton && !($0 is UISlider) && !($0 is UIStackView) && !($0 is UILabel) }
.forEach { $0.backgroundColor = textColor.withAlphaComponent(0.12) }
[fontChoiceButton, lineHeightButton, displayTypeButton].forEach {
$0.setTitleColor(textColor, for: .normal)
$0.backgroundColor = textColor.withAlphaComponent(0.10)
}
[fontChoiceControl, lineHeightControl, columnCountControl, displayTypeControl].forEach { control in
control.backgroundColor = theme.toolBackgroundColor
if #available(iOS 13.0, *) {
control.selectedSegmentTintColor = theme.toolControlTextColor.withAlphaComponent(0.14)
} else {
control.tintColor = theme.toolControlTextColor
}
control.setTitleTextAttributes([.foregroundColor: theme.contentTextColor], for: .normal)
control.setTitleTextAttributes([.foregroundColor: theme.toolControlTextColor], for: .selected)
}
navigationController?.navigationBar.tintColor = theme.toolControlTextColor
navigationController?.navigationBar.barTintColor = theme.toolBackgroundColor
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: theme.toolControlTextColor]
brightnessSlider.minimumTrackTintColor = .systemBlue
fontSizeSlider.minimumTrackTintColor = .systemBlue
brightnessSlider.maximumTrackTintColor = textColor.withAlphaComponent(0.22)
fontSizeSlider.maximumTrackTintColor = textColor.withAlphaComponent(0.22)
updateThemeSelection(ThemePreset.allCases.first(where: { $0.theme == theme }) ?? .light)
}
private func updateThemeSelection(_ preset: ThemePreset) {
themeButtons.forEach { button in
let isSelected = button.tag == preset.rawValue
button.layer.borderWidth = isSelected ? 2 : 1
button.layer.borderColor = isSelected ? currentConfiguration.theme.toolControlTextColor.cgColor : currentConfiguration.theme.toolControlBorderUnselectColor.cgColor
button.accessibilityValue = isSelected ? "selected" : "unselected"
let selected = button.tag == preset.rawValue
button.layer.borderWidth = selected ? 2.5 : 1
button.layer.borderColor = selected ? UIColor.systemBlue.cgColor : UIColor.white.withAlphaComponent(0.28).cgColor
button.accessibilityValue = selected ? "selected" : "unselected"
}
}
private func updateControlAccessibilityValues() {
fontChoiceControl.accessibilityValue = fontChoiceControl.titleForSegment(at: fontChoiceControl.selectedSegmentIndex)
lineHeightControl.accessibilityValue = lineHeightControl.titleForSegment(at: lineHeightControl.selectedSegmentIndex)
columnCountControl.accessibilityValue = columnCountControl.titleForSegment(at: columnCountControl.selectedSegmentIndex)
displayTypeControl.accessibilityValue = displayTypeControl.titleForSegment(at: displayTypeControl.selectedSegmentIndex)
func notifyDismissalIfNeeded() {
guard !hasNotifiedDismissal else { return }
hasNotifiedDismissal = true
onDismiss?()
}
@objc private func doneAction() {
dismiss(animated: true) { [weak self] in
self?.onDismiss?()
notifyDismissalIfNeeded()
dismiss(animated: true)
}
@objc private func brightnessChanged(_ sender: UISlider) { onBrightnessChange?(CGFloat(sender.value)) }
@objc private func fontSizeChanged(_ sender: UISlider) {
let value = CGFloat(sender.value.rounded())
guard value != currentConfiguration.fontSize else { return }
currentConfiguration.fontSize = value
fontSizeLabel.text = String(Int(value))
onFontSizeChange?(value)
}
@objc private func fontChoiceAction() {
let alert = UIAlertController(title: "字体", message: nil, preferredStyle: .actionSheet)
RDEPUBReaderFontChoice.allCases.forEach { choice in
alert.addAction(UIAlertAction(title: choice.displayName, style: .default) { [weak self] _ in
guard let self else { return }
self.currentConfiguration.fontChoice = choice
self.updateSelectorTitles()
self.onFontChoiceChange?(choice)
})
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
@objc private func brightnessChanged(_ slider: UISlider) {
onBrightnessChange?(CGFloat(slider.value))
}
@objc private func decreaseFontAction() {
let nextValue = max(12, currentConfiguration.fontSize - 1)
guard nextValue != currentConfiguration.fontSize else { return }
currentConfiguration.fontSize = nextValue
fontValueLabel.text = String(Int(nextValue.rounded()))
onFontSizeChange?(nextValue)
}
@objc private func increaseFontAction() {
let nextValue = min(36, currentConfiguration.fontSize + 1)
guard nextValue != currentConfiguration.fontSize else { return }
currentConfiguration.fontSize = nextValue
fontValueLabel.text = String(Int(nextValue.rounded()))
onFontSizeChange?(nextValue)
}
@objc private func fontChoiceChanged(_ control: UISegmentedControl) {
let choices = RDEPUBReaderFontChoice.allCases
let index = max(0, min(control.selectedSegmentIndex, choices.count - 1))
let choice = choices[index]
guard choice != currentConfiguration.fontChoice else { return }
currentConfiguration.fontChoice = choice
updateControlAccessibilityValues()
onFontChoiceChange?(choice)
}
@objc private func lineHeightChanged(_ control: UISegmentedControl) {
let index = max(0, min(control.selectedSegmentIndex, lineHeightValues.count - 1))
let value = lineHeightValues[index]
currentConfiguration.lineHeightMultiple = value
updateControlAccessibilityValues()
onLineHeightChange?(value)
}
@objc private func columnCountChanged(_ control: UISegmentedControl) {
let numberOfColumns = control.selectedSegmentIndex == 1 ? 2 : 1
currentConfiguration.numberOfColumns = numberOfColumns
updateControlAccessibilityValues()
onColumnCountChange?(numberOfColumns)
}
@objc private func displayTypeChanged(_ control: UISegmentedControl) {
let displayType: RDEpubReaderView.DisplayType
switch control.selectedSegmentIndex {
case 1:
displayType = .horizontalScroll
case 2:
displayType = .verticalScroll
default:
displayType = .pageCurl
@objc private func lineHeightAction() {
let alert = UIAlertController(title: "行距", message: nil, preferredStyle: .actionSheet)
zip(["紧凑", "标准", "宽松"], lineHeightValues).forEach { title, value in
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
guard let self else { return }
self.currentConfiguration.lineHeightMultiple = value
self.updateSelectorTitles()
self.onLineHeightChange?(value)
})
}
currentConfiguration.displayType = displayType
updateControlAccessibilityValues()
onDisplayTypeChange?(displayType)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
@objc private func displayTypeAction() {
let alert = UIAlertController(title: "翻页方式", message: nil, preferredStyle: .actionSheet)
[("仿真", RDEpubReaderView.DisplayType.pageCurl), ("横滑", .horizontalScroll), ("竖滑", .verticalScroll)].forEach { title, type in
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
guard let self else { return }
self.currentConfiguration.displayType = type
self.updateSelectorTitles()
self.onDisplayTypeChange?(type)
})
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
@objc private func themeButtonAction(_ sender: UIButton) {