Fixes and additions (#222)

* Remove title shadow (tested and still easy to read)

* fix icons

* Maybe fixed icon

* fixed title :3

* disable title in portrait

* Fix controls in portrait

* Fix skip 85s buttons showing

* now accounts for force landscape

* Auto hide UI on opening reader

* Disable text sliding

* Fix reader fake network issue

* Fix tab bar in library

* hide status bar with UI in reader

* added progressbar

* amde progressbar usable

* Update ReaderView.swift

---------

Co-authored-by: cranci <100066266+cranci1@users.noreply.github.com>
This commit is contained in:
50/50 2025-07-12 17:02:35 +02:00 committed by GitHub
parent a926327362
commit fe0750078e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 529 additions and 195 deletions

View file

@ -93,7 +93,6 @@ struct ContentView: View {
) { _ in
lastHideTime = Date()
tabBarVisible = false
Logger.shared.log("Tab bar hidden", type: "Debug")
}
NotificationCenter.default.addObserver(
@ -104,9 +103,6 @@ struct ContentView: View {
let timeSinceHide = Date().timeIntervalSince(lastHideTime)
if timeSinceHide > 0.2 {
tabBarVisible = true
Logger.shared.log("Tab bar shown after \(timeSinceHide) seconds", type: "Debug")
} else {
Logger.shared.log("Tab bar show request ignored, only \(timeSinceHide) seconds since hide", type: "Debug")
}
}
}

View file

@ -178,6 +178,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private var dimButtonTimer: Timer?
let cfg = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
let bottomControlsCfg = UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
private var controlsToHide: [UIView] {
var views = [
@ -362,6 +363,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
setupTopRowLayout()
updateSkipButtonsVisibility()
if !isSkip85Visible {
skip85Button.isHidden = true
skip85Button.alpha = 0.0
}
updateTitleVisibilityForCurrentOrientation()
isControlsVisible = true
for control in controlsToHide {
control.alpha = 1.0
@ -490,29 +498,55 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
capsuleContainer.addSubview(btn)
}
NSLayoutConstraint.activate([
capsuleContainer.leadingAnchor.constraint(equalTo: dismissButton.superview!.trailingAnchor, constant: 12),
capsuleContainer.centerYAnchor.constraint(equalTo: dismissButton.superview!.centerYAnchor),
capsuleContainer.heightAnchor.constraint(equalToConstant: 42)
])
let forcedLandscape = UserDefaults.standard.bool(forKey: "alwaysLandscape")
let isLandscape = forcedLandscape || UIDevice.current.orientation.isLandscape || view.bounds.width > view.bounds.height
for (index, btn) in buttons.enumerated() {
if isLandscape {
NSLayoutConstraint.activate([
btn.centerYAnchor.constraint(equalTo: capsuleContainer.centerYAnchor),
btn.widthAnchor.constraint(equalToConstant: 40),
btn.heightAnchor.constraint(equalToConstant: 40)
capsuleContainer.leadingAnchor.constraint(equalTo: dismissButton.superview!.trailingAnchor, constant: 12),
capsuleContainer.centerYAnchor.constraint(equalTo: dismissButton.superview!.centerYAnchor),
capsuleContainer.heightAnchor.constraint(equalToConstant: 42)
])
if index == 0 {
btn.leadingAnchor.constraint(equalTo: capsuleContainer.leadingAnchor, constant: 20).isActive = true
} else {
btn.leadingAnchor.constraint(equalTo: buttons[index - 1].trailingAnchor, constant: 18).isActive = true
for (index, btn) in buttons.enumerated() {
NSLayoutConstraint.activate([
btn.centerYAnchor.constraint(equalTo: capsuleContainer.centerYAnchor),
btn.widthAnchor.constraint(equalToConstant: 40),
btn.heightAnchor.constraint(equalToConstant: 40)
])
if index == 0 {
btn.leadingAnchor.constraint(equalTo: capsuleContainer.leadingAnchor, constant: 20).isActive = true
} else {
btn.leadingAnchor.constraint(equalTo: buttons[index - 1].trailingAnchor, constant: 18).isActive = true
}
if index == buttons.count - 1 {
btn.trailingAnchor.constraint(equalTo: capsuleContainer.trailingAnchor, constant: -10).isActive = true
}
}
if index == buttons.count - 1 {
btn.trailingAnchor.constraint(equalTo: capsuleContainer.trailingAnchor, constant: -10).isActive = true
} else {
NSLayoutConstraint.activate([
capsuleContainer.topAnchor.constraint(equalTo: dismissButton.superview!.bottomAnchor, constant: 12),
capsuleContainer.leadingAnchor.constraint(equalTo: dismissButton.superview!.leadingAnchor),
capsuleContainer.widthAnchor.constraint(equalToConstant: 42)
])
for (index, btn) in buttons.enumerated() {
NSLayoutConstraint.activate([
btn.centerXAnchor.constraint(equalTo: capsuleContainer.centerXAnchor),
btn.widthAnchor.constraint(equalToConstant: 40),
btn.heightAnchor.constraint(equalToConstant: 40)
])
if index == 0 {
btn.topAnchor.constraint(equalTo: capsuleContainer.topAnchor, constant: 20).isActive = true
} else {
btn.topAnchor.constraint(equalTo: buttons[index - 1].bottomAnchor, constant: 18).isActive = true
}
if index == buttons.count - 1 {
btn.bottomAnchor.constraint(equalTo: capsuleContainer.bottomAnchor, constant: -10).isActive = true
}
}
}
view.bringSubviewToFront(skip85Button)
if let volumeSlider = volumeSliderHostingView {
@ -522,13 +556,38 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
titleLabel.shutdownLabel()
coordinator.animate(alongsideTransition: { _ in
self.updateMarqueeConstraints()
self.setupTopRowLayout()
}, completion: { _ in
self.updateTitleVisibilityForCurrentOrientation()
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
})
}
private func updateTitleVisibilityForCurrentOrientation() {
let forcedLandscape = UserDefaults.standard.bool(forKey: "alwaysLandscape")
let isLandscape = forcedLandscape || UIDevice.current.orientation.isLandscape || view.bounds.width > view.bounds.height
episodeNumberLabel.isHidden = !isLandscape
titleLabel.isHidden = !isLandscape
titleStackView.isHidden = !isLandscape
episodeNumberLabel.alpha = isLandscape ? 1.0 : 0.0
titleLabel.alpha = isLandscape ? 1.0 : 0.0
titleStackView.alpha = isLandscape ? 1.0 : 0.0
if isLandscape {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.titleLabel.restartLabel()
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let pipController = pipController {
@ -547,7 +606,20 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} else {
episodeNumberLabel.lineBreakMode = .byClipping
}
updateMarqueeConstraintsForBottom()
let forcedLandscape = UserDefaults.standard.bool(forKey: "alwaysLandscape")
let isLandscape = forcedLandscape || view.bounds.width > view.bounds.height
let wasLandscape = UserDefaults.standard.bool(forKey: "wasLandscapeOrientation")
if isLandscape != wasLandscape {
UserDefaults.standard.set(isLandscape, forKey: "wasLandscapeOrientation")
resetMarqueeAfterOrientationChange()
setupTopRowLayout()
} else {
updateMarqueeConstraintsForBottom()
}
updateTitleVisibilityForCurrentOrientation()
}
override func viewDidAppear(_ animated: Bool) {
@ -560,6 +632,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidChange), name: .AVPlayerItemNewAccessLogEntry, object: nil)
skip85Button?.isHidden = !isSkip85Visible
if !isSkip85Visible {
skip85Button?.alpha = 0.0
}
}
override func viewWillDisappear(_ animated: Bool) {
@ -1161,14 +1236,20 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
func setupMarqueeLabel() {
let titleContainer = UIView()
titleContainer.translatesAutoresizingMaskIntoConstraints = false
titleContainer.backgroundColor = .clear
controlsContainerView.addSubview(titleContainer)
episodeNumberLabel = UILabel()
episodeNumberLabel.text = "Episode \(episodeNumber)"
episodeNumberLabel.textColor = UIColor(white: 1.0, alpha: 0.6)
episodeNumberLabel.font = UIFont.systemFont(ofSize: 14, weight: .semibold)
episodeNumberLabel.textAlignment = .left
episodeNumberLabel.setContentHuggingPriority(.required, for: .vertical)
episodeNumberLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel = MarqueeLabel()
titleLabel = MarqueeLabel(frame: .zero, duration: 8.0, fadeLength: 10.0)
titleLabel.text = titleText
titleLabel.type = .continuous
titleLabel.textColor = .white
@ -1178,14 +1259,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
titleLabel.leadingBuffer = 1.0
titleLabel.trailingBuffer = 16.0
titleLabel.animationDelay = 2.5
titleLabel.layer.shadowColor = UIColor.black.cgColor
titleLabel.layer.shadowOffset = CGSize(width: 0, height: 2)
titleLabel.layer.shadowOpacity = 0.6
titleLabel.layer.shadowRadius = 4
titleLabel.layer.masksToBounds = false
titleLabel.lineBreakMode = .byTruncatingTail
titleLabel.textAlignment = .left
titleLabel.setContentHuggingPriority(.defaultLow, for: .vertical)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleStackView = UIStackView(arrangedSubviews: [episodeNumberLabel, titleLabel])
titleStackView.axis = .vertical
@ -1193,8 +1270,32 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
titleStackView.spacing = 0
titleStackView.clipsToBounds = false
titleStackView.isLayoutMarginsRelativeArrangement = true
controlsContainerView.addSubview(titleStackView)
titleStackView.translatesAutoresizingMaskIntoConstraints = false
titleContainer.addSubview(titleStackView)
NSLayoutConstraint.activate([
titleContainer.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 18),
titleContainer.widthAnchor.constraint(lessThanOrEqualTo: controlsContainerView.widthAnchor, multiplier: 0.7),
titleContainer.heightAnchor.constraint(equalToConstant: 50)
])
NSLayoutConstraint.activate([
titleStackView.leadingAnchor.constraint(equalTo: titleContainer.leadingAnchor),
titleStackView.trailingAnchor.constraint(equalTo: titleContainer.trailingAnchor),
titleStackView.topAnchor.constraint(equalTo: titleContainer.topAnchor),
titleStackView.bottomAnchor.constraint(equalTo: titleContainer.bottomAnchor)
])
NSLayoutConstraint.activate([
episodeNumberLabel.leadingAnchor.constraint(equalTo: titleStackView.leadingAnchor),
episodeNumberLabel.trailingAnchor.constraint(lessThanOrEqualTo: titleStackView.trailingAnchor),
titleLabel.leadingAnchor.constraint(equalTo: titleStackView.leadingAnchor),
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: titleStackView.trailingAnchor)
])
titleStackAboveSkipButtonConstraints = []
titleStackAboveSliderConstraints = []
}
func volumeSlider() {
@ -1225,17 +1326,34 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.volumeSliderHostingView = hostingController.view
NSLayoutConstraint.activate([
volumeCapsule.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
volumeCapsule.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16),
volumeCapsule.heightAnchor.constraint(equalToConstant: 42),
volumeCapsule.widthAnchor.constraint(equalToConstant: 200),
hostingController.view.centerYAnchor.constraint(equalTo: volumeCapsule.centerYAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: volumeCapsule.leadingAnchor, constant: 20),
hostingController.view.trailingAnchor.constraint(equalTo: volumeCapsule.trailingAnchor, constant: -20),
hostingController.view.heightAnchor.constraint(equalToConstant: 30)
])
let forcedLandscape = UserDefaults.standard.bool(forKey: "alwaysLandscape")
let isLandscape = forcedLandscape || UIDevice.current.orientation.isLandscape || view.bounds.width > view.bounds.height
if isLandscape {
NSLayoutConstraint.activate([
volumeCapsule.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
volumeCapsule.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16),
volumeCapsule.heightAnchor.constraint(equalToConstant: 42),
volumeCapsule.widthAnchor.constraint(equalToConstant: 200),
hostingController.view.centerYAnchor.constraint(equalTo: volumeCapsule.centerYAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: volumeCapsule.leadingAnchor, constant: 20),
hostingController.view.trailingAnchor.constraint(equalTo: volumeCapsule.trailingAnchor, constant: -20),
hostingController.view.heightAnchor.constraint(equalToConstant: 30)
])
} else {
NSLayoutConstraint.activate([
volumeCapsule.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
volumeCapsule.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16),
volumeCapsule.heightAnchor.constraint(equalToConstant: 42),
volumeCapsule.widthAnchor.constraint(equalToConstant: 200),
hostingController.view.centerYAnchor.constraint(equalTo: volumeCapsule.centerYAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: volumeCapsule.leadingAnchor, constant: 20),
hostingController.view.trailingAnchor.constraint(equalTo: volumeCapsule.trailingAnchor, constant: -20),
hostingController.view.heightAnchor.constraint(equalToConstant: 30)
])
}
self.volumeSliderHostingView = volumeCapsule
@ -1297,6 +1415,53 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
holdSpeedIndicator.titleLabel?.textAlignment = .center
}
func updateMarqueeConstraintsForBottom() {
NSLayoutConstraint.deactivate(titleStackAboveSkipButtonConstraints)
NSLayoutConstraint.deactivate(titleStackAboveSliderConstraints)
let forcedLandscape = UserDefaults.standard.bool(forKey: "alwaysLandscape")
let isLandscape = forcedLandscape || view.bounds.width > view.bounds.height
titleStackView.isHidden = !isLandscape
titleStackView.alpha = isLandscape ? 1.0 : 0.0
episodeNumberLabel.isHidden = !isLandscape
titleLabel.isHidden = !isLandscape
episodeNumberLabel.alpha = isLandscape ? 1.0 : 0.0
titleLabel.alpha = isLandscape ? 1.0 : 0.0
if !isLandscape {
return
}
titleLabel.textAlignment = .left
episodeNumberLabel.textAlignment = .left
let skipIntroVisible = !(skipIntroButton?.isHidden ?? true) && (skipIntroButton?.alpha ?? 0) > 0.1
let skip85Visible = !(skip85Button?.isHidden ?? true) && (skip85Button?.alpha ?? 0) > 0.1
if skipIntroVisible && skipIntroButton?.superview != nil {
titleStackAboveSkipButtonConstraints = [
titleStackView.superview!.bottomAnchor.constraint(equalTo: skipIntroButton.topAnchor, constant: -4)
]
NSLayoutConstraint.activate(titleStackAboveSkipButtonConstraints)
} else if skip85Visible && skip85Button?.superview != nil {
titleStackAboveSkipButtonConstraints = [
titleStackView.superview!.bottomAnchor.constraint(equalTo: skip85Button.topAnchor, constant: -4)
]
NSLayoutConstraint.activate(titleStackAboveSkipButtonConstraints)
} else if let sliderView = sliderHostingController?.view {
titleStackAboveSliderConstraints = [
titleStackView.superview!.bottomAnchor.constraint(equalTo: sliderView.topAnchor, constant: -4)
]
NSLayoutConstraint.activate(titleStackAboveSliderConstraints)
}
if isLandscape {
titleLabel.restartLabel()
}
view.layoutIfNeeded()
}
func updateSkipButtonsVisibility() {
if !isControlsVisible { return }
let t = currentTimeVal
@ -1325,18 +1490,20 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
}
if shouldShowSkip85 {
if shouldShowSkip85 && (skip85Button.isHidden || skip85Button.alpha < 0.1) {
skip85Button.setTitle(" Skip 85s", for: .normal)
skip85Button.setImage(UIImage(systemName: "goforward"), for: .normal)
skip85Button.isHidden = false
UIView.animate(withDuration: 0.2) {
self.skip85Button.alpha = 1.0
}
} else {
} else if !shouldShowSkip85 && (!skip85Button.isHidden || skip85Button.alpha > 0) {
UIView.animate(withDuration: 0.2) {
self.skip85Button.alpha = 0.0
} completion: { _ in
self.skip85Button.isHidden = true
if !shouldShowSkip85 {
self.skip85Button.isHidden = true
}
}
}
@ -1507,7 +1674,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipIntroButton.tintColor = .white
skipIntroButton.setTitleColor(.white, for: .normal)
skipIntroButton.layer.cornerRadius = 21
skipIntroButton.clipsToBounds = true
skipIntroButton.clipsToBounds = false
skipIntroButton.alpha = 0.0
skipIntroButton.addTarget(self, action: #selector(skipIntro), for: .touchUpInside)
controlsContainerView.addSubview(skipIntroButton)
@ -1529,7 +1696,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipOutroButton.tintColor = .white
skipOutroButton.setTitleColor(.white, for: .normal)
skipOutroButton.layer.cornerRadius = 21
skipOutroButton.clipsToBounds = true
skipOutroButton.clipsToBounds = false
skipOutroButton.alpha = 0.0
skipOutroButton.addTarget(self, action: #selector(skipOutro), for: .touchUpInside)
skipOutroButton.translatesAutoresizingMaskIntoConstraints = false
@ -1537,17 +1704,19 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
func setupSkip85Button() {
let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
let image = UIImage(systemName: "goforward", withConfiguration: config)
let image = UIImage(systemName: "goforward", withConfiguration: config)?.withRenderingMode(.alwaysTemplate)
skip85Button = GradientBlurButton(type: .system)
skip85Button.setTitle(" Skip 85s", for: .normal)
skip85Button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
skip85Button.setImage(image, for: .normal)
skip85Button.imageView?.contentMode = .scaleAspectFit
skip85Button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
skip85Button.tintColor = .white
skip85Button.setTitleColor(.white, for: .normal)
skip85Button.layer.cornerRadius = 21
skip85Button.clipsToBounds = true
skip85Button.clipsToBounds = false
skip85Button.alpha = 0.0
skip85Button.isHidden = !isSkip85Visible
skip85Button.addTarget(self, action: #selector(skip85Tapped), for: .touchUpInside)
controlsContainerView.addSubview(skip85Button)
skip85Button.translatesAutoresizingMaskIntoConstraints = false
@ -1562,7 +1731,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
private func setupQualityButton() {
let image = UIImage(systemName: "tv", withConfiguration: cfg)
let image = UIImage(systemName: "tv", withConfiguration: bottomControlsCfg)
qualityButton = UIButton(type: .system)
qualityButton.setImage(image, for: .normal)
@ -1879,7 +2048,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
if self.isControlsVisible {
self.skip85Button.isHidden = false
if self.isSkip85Visible {
self.skip85Button.isHidden = false
}
self.skipIntroButton.alpha = 0.0
self.skipOutroButton.alpha = 0.0
}
@ -3193,43 +3364,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
isMenuOpen = true
}
func updateMarqueeConstraintsForBottom() {
NSLayoutConstraint.deactivate(titleStackAboveSkipButtonConstraints)
NSLayoutConstraint.deactivate(titleStackAboveSliderConstraints)
let skipIntroVisible = !(skipIntroButton?.isHidden ?? true) && (skipIntroButton?.alpha ?? 0) > 0.1
let skip85Visible = !(skip85Button?.isHidden ?? true) && (skip85Button?.alpha ?? 0) > 0.1
let skipOutroVisible = skipOutroButton.superview != nil && !skipOutroButton.isHidden && skipOutroButton.alpha > 0.1
let isLandscape = view.bounds.width > view.bounds.height
let widthMultiplier: CGFloat = isLandscape ? 0.5 : 0.7
if skipIntroVisible && skipIntroButton?.superview != nil && titleStackView.superview != nil {
titleStackAboveSkipButtonConstraints = [
titleStackView.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 18),
titleStackView.bottomAnchor.constraint(equalTo: skipIntroButton.topAnchor, constant: -4),
titleStackView.widthAnchor.constraint(lessThanOrEqualTo: controlsContainerView.widthAnchor, multiplier: widthMultiplier)
]
NSLayoutConstraint.activate(titleStackAboveSkipButtonConstraints)
} else if skip85Visible && skip85Button?.superview != nil && titleStackView.superview != nil {
titleStackAboveSkipButtonConstraints = [
titleStackView.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 18),
titleStackView.bottomAnchor.constraint(equalTo: skip85Button.topAnchor, constant: -4),
titleStackView.widthAnchor.constraint(lessThanOrEqualTo: controlsContainerView.widthAnchor, multiplier: widthMultiplier)
]
NSLayoutConstraint.activate(titleStackAboveSkipButtonConstraints)
} else if let sliderView = sliderHostingController?.view, titleStackView.superview != nil {
titleStackAboveSliderConstraints = [
titleStackView.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 18),
titleStackView.bottomAnchor.constraint(equalTo: sliderView.topAnchor, constant: -4),
titleStackView.widthAnchor.constraint(lessThanOrEqualTo: controlsContainerView.widthAnchor, multiplier: widthMultiplier)
]
NSLayoutConstraint.activate(titleStackAboveSliderConstraints)
}
view.layoutIfNeeded()
}
func setupWatchNextButton() {
let image = UIImage(systemName: "forward.end", withConfiguration: cfg)
@ -3253,7 +3387,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
NSLayoutConstraint.activate([
dimButton.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
dimButton.widthAnchor.constraint(equalToConstant: 24),
dimButton.heightAnchor.constraint(equalToConstant: 24)
dimButton.heightAnchor.constraint(equalToConstant: 24)
])
dimButtonToSlider = dimButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor)
@ -3261,7 +3395,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
func setupSpeedButton() {
let image = UIImage(systemName: "speedometer", withConfiguration: cfg)
let image = UIImage(systemName: "speedometer", withConfiguration: bottomControlsCfg)
speedButton = UIButton(type: .system)
speedButton.setImage(image, for: .normal)
@ -3275,7 +3409,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
func setupMenuButton() {
let image = UIImage(systemName: "captions.bubble", withConfiguration: cfg)
let image = UIImage(systemName: "captions.bubble", withConfiguration: bottomControlsCfg)
menuButton = UIButton(type: .system)
menuButton.setImage(image, for: .normal)
@ -3409,8 +3543,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
NSLayoutConstraint.activate([
pipButton.centerYAnchor.constraint(equalTo: dimButton.centerYAnchor),
pipButton.trailingAnchor.constraint(equalTo: dimButton.leadingAnchor, constant: -8),
pipButton.widthAnchor.constraint(equalToConstant: 40),
pipButton.heightAnchor.constraint(equalToConstant: 40),
pipButton.widthAnchor.constraint(equalToConstant: 24),
pipButton.heightAnchor.constraint(equalToConstant: 24),
airplayButton.centerYAnchor.constraint(equalTo: pipButton.centerYAnchor),
airplayButton.trailingAnchor.constraint(equalTo: pipButton.leadingAnchor, constant: -4),
airplayButton.widthAnchor.constraint(equalToConstant: 24),
@ -3423,29 +3557,50 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
func updateMarqueeConstraints() {
UIView.performWithoutAnimation {
NSLayoutConstraint.deactivate(currentMarqueeConstraints)
let leftSpacing: CGFloat = 2
let rightSpacing: CGFloat = 6
let trailingAnchor: NSLayoutXAxisAnchor = (volumeSliderHostingView?.isHidden == false)
? volumeSliderHostingView!.leadingAnchor
: view.safeAreaLayoutGuide.trailingAnchor
currentMarqueeConstraints = [
episodeNumberLabel.leadingAnchor.constraint(
equalTo: dismissButton.trailingAnchor, constant: leftSpacing),
episodeNumberLabel.trailingAnchor.constraint(
equalTo: trailingAnchor, constant: -rightSpacing - 10),
episodeNumberLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
]
NSLayoutConstraint.activate(currentMarqueeConstraints)
updateMarqueeConstraintsForBottom()
view.layoutIfNeeded()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.titleLabel?.restartLabel()
titleStackView.isHidden = false
episodeNumberLabel.isHidden = false
titleLabel.isHidden = false
titleStackView.alpha = 1.0
episodeNumberLabel.alpha = 1.0
titleLabel.alpha = 1.0
titleLabel.textAlignment = .left
episodeNumberLabel.textAlignment = .left
view.layoutIfNeeded()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.titleLabel.restartLabel()
}
}
private func resetMarqueeAfterOrientationChange() {
let forcedLandscape = UserDefaults.standard.bool(forKey: "alwaysLandscape")
let isLandscape = forcedLandscape || view.bounds.width > view.bounds.height
episodeNumberLabel.isHidden = !isLandscape
titleLabel.isHidden = !isLandscape
titleStackView.isHidden = !isLandscape
episodeNumberLabel.alpha = isLandscape ? 1.0 : 0.0
titleLabel.alpha = isLandscape ? 1.0 : 0.0
titleStackView.alpha = isLandscape ? 1.0 : 0.0
if !isLandscape {
return
}
titleLabel.textAlignment = .left
episodeNumberLabel.textAlignment = .left
view.setNeedsLayout()
view.layoutIfNeeded()
titleLabel.shutdownLabel()
if isLandscape {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.titleLabel.restartLabel()
}
}
}

View file

@ -86,6 +86,7 @@ class NetworkMonitor: ObservableObject {
@Published var currentNetworkType: NetworkType = .unknown
@Published var isConnected: Bool = false
private var isInitialized: Bool = false
private init() {
startMonitoring()
@ -96,6 +97,7 @@ class NetworkMonitor: ObservableObject {
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
self?.currentNetworkType = self?.getNetworkType(from: path) ?? .unknown
self?.isInitialized = true
}
}
monitor.start(queue: queue)
@ -115,6 +117,21 @@ class NetworkMonitor: ObservableObject {
return shared.currentNetworkType
}
func ensureNetworkStatusInitialized() async -> Bool {
if isInitialized {
return isConnected
}
for _ in 0..<10 {
if isInitialized {
return isConnected
}
try? await Task.sleep(nanoseconds: 100_000_000)
}
return isConnected
}
deinit {
monitor.cancel()
}

View file

@ -31,6 +31,7 @@ struct LibraryView: View {
@State private var continueReadingItems: [ContinueReadingItem] = []
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
@State private var selectedTab: Int = 0
@State private var isActive: Bool = false
private var librarySectionsOrder: [String] {
(try? JSONDecoder().decode([String].self, from: librarySectionsOrderData)) ?? ["continueWatching", "continueReading", "collections"]
@ -99,15 +100,51 @@ struct LibraryView: View {
.scrollViewBottomPadding()
.deviceScaled()
.onAppear {
isActive = true
fetchContinueWatching()
fetchContinueReading()
NotificationCenter.default.post(name: .showTabBar, object: nil)
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
if !isMediaInfoActive && !isReaderActive {
NotificationCenter.default.post(name: .showTabBar, object: nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
if !isMediaInfoActive && !isReaderActive {
NotificationCenter.default.post(name: .showTabBar, object: nil)
}
}
}
.onDisappear {
isActive = false
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
fetchContinueWatching()
fetchContinueReading()
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
if !isMediaInfoActive && !isReaderActive {
NotificationCenter.default.post(name: .showTabBar, object: nil)
}
}
}
.onReceive(Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()) { _ in
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
if isActive && !isMediaInfoActive && !isReaderActive {
NotificationCenter.default.post(name: .showTabBar, object: nil)
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
isActive = true
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
if !isMediaInfoActive && !isReaderActive {
NotificationCenter.default.post(name: .showTabBar, object: nil)
}
}
}

View file

@ -31,7 +31,7 @@ struct ReaderView: View {
let chapterHref: String
let chapterTitle: String
let chapters: [[String: Any]]
let mediaTitle: String
let mediaTitle: String
let chapterNumber: Int
@State private var htmlContent: String = ""
@ -55,6 +55,9 @@ struct ReaderView: View {
@StateObject private var navigator = ChapterNavigator.shared
// Status bar control
@State private var statusBarHidden = false
private let fontOptions = [
("-apple-system", "System"),
("Georgia", "Georgia"),
@ -150,6 +153,8 @@ struct ReaderView: View {
.onTapGesture {
withAnimation(.easeInOut(duration: 0.6)) {
isHeaderVisible.toggle()
statusBarHidden = !isHeaderVisible
setStatusBarHidden(!isHeaderVisible)
if !isHeaderVisible {
isSettingsExpanded = false
}
@ -157,40 +162,42 @@ struct ReaderView: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
HTMLView(
htmlContent: htmlContent,
fontSize: fontSize,
fontFamily: selectedFont,
fontWeight: fontWeight,
textAlignment: textAlignment,
lineSpacing: lineSpacing,
margin: margin,
isAutoScrolling: $isAutoScrolling,
autoScrollSpeed: autoScrollSpeed,
colorPreset: colorPresets[selectedColorPreset],
chapterHref: chapterHref,
onProgressChanged: { progress in
self.readingProgress = progress
if Date().timeIntervalSince(self.lastProgressUpdate) > 2.0 {
self.updateReadingProgress(progress: progress)
self.lastProgressUpdate = Date()
Logger.shared.log("Progress updated to \(progress)", type: "Debug")
HTMLView(
htmlContent: htmlContent,
fontSize: fontSize,
fontFamily: selectedFont,
fontWeight: fontWeight,
textAlignment: textAlignment,
lineSpacing: lineSpacing,
margin: margin,
isAutoScrolling: $isAutoScrolling,
autoScrollSpeed: autoScrollSpeed,
colorPreset: colorPresets[selectedColorPreset],
chapterHref: chapterHref,
onProgressChanged: { progress in
self.readingProgress = progress
if Date().timeIntervalSince(self.lastProgressUpdate) > 2.0 {
self.updateReadingProgress(progress: progress)
self.lastProgressUpdate = Date()
Logger.shared.log("Progress updated to \(progress)", type: "Debug")
}
}
}
)
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal)
.simultaneousGesture(TapGesture().onEnded {
withAnimation(.easeInOut(duration: 0.6)) {
isHeaderVisible.toggle()
statusBarHidden = !isHeaderVisible
setStatusBarHidden(!isHeaderVisible)
if !isHeaderVisible {
isSettingsExpanded = false
}
}
})
}
.padding(.top, isHeaderVisible ? 0 : (UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first?.windows.first?.safeAreaInsets.top ?? 0))
.padding(.top, UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first?.windows.first?.safeAreaInsets.top ?? 0)
}
headerView
@ -198,12 +205,12 @@ struct ReaderView: View {
.offset(y: isHeaderVisible ? 0 : -100)
.allowsHitTesting(isHeaderVisible)
.animation(.easeInOut(duration: 0.6), value: isHeaderVisible)
.zIndex(1)
.zIndex(1)
if isHeaderVisible {
footerView
.transition(.move(edge: .bottom))
.zIndex(2)
.zIndex(2)
}
}
.navigationBarHidden(true)
@ -216,11 +223,20 @@ struct ReaderView: View {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first,
let navigationController = window.rootViewController?.children.first as? UINavigationController {
navigationController.interactivePopGestureRecognizer?.isEnabled = false
navigationController.interactivePopGestureRecognizer?.isEnabled = true
navigationController.interactivePopGestureRecognizer?.delegate = nil
}
NotificationCenter.default.post(name: .hideTabBar, object: nil)
UserDefaults.standard.set(true, forKey: "isReaderActive")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
withAnimation(.easeInOut(duration: 0.6)) {
isHeaderVisible = false
statusBarHidden = true
setStatusBarHidden(true)
}
}
}
.onDisappear {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
@ -261,8 +277,8 @@ struct ReaderView: View {
}
} else {
if !htmlContent.isEmpty {
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil {
@ -286,19 +302,31 @@ struct ReaderView: View {
}
}
UserDefaults.standard.set(false, forKey: "isReaderActive")
setStatusBarHidden(false)
}
.task {
do {
ensureModuleLoaded()
let isOffline = !(NetworkMonitor.shared.isConnected)
if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref),
!cachedContent.isEmpty &&
!cachedContent.contains("undefined") &&
let isConnected = await NetworkMonitor.shared.ensureNetworkStatusInitialized()
let isOffline = !isConnected
if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref),
!cachedContent.isEmpty &&
!cachedContent.contains("undefined") &&
cachedContent.count > 50 {
Logger.shared.log("Using cached HTML content for \(chapterHref)", type: "Debug")
htmlContent = cachedContent
isLoading = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(.easeInOut(duration: 0.3)) {
isHeaderVisible = false
statusBarHidden = true
setStatusBarHidden(true)
}
}
} else if isOffline {
let offlineError = NSError(domain: "Sora", code: -1009, userInfo: [NSLocalizedDescriptionKey: "No network connection."])
self.error = offlineError
@ -331,6 +359,14 @@ struct ReaderView: View {
htmlContent = content
isLoading = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(.easeInOut(duration: 0.3)) {
isHeaderVisible = false
statusBarHidden = true
setStatusBarHidden(true)
}
}
if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref),
cachedContent.isEmpty || cachedContent.contains("undefined") || cachedContent.count < 50 {
let item = ContinueReadingItem(
@ -367,6 +403,7 @@ struct ReaderView: View {
}
}
}
.statusBar(hidden: statusBarHidden)
}
private func stopAutoScroll() {
@ -383,12 +420,13 @@ struct ReaderView: View {
dismiss()
}) {
Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .bold))
.font(.system(size: 16, weight: .bold))
.foregroundColor(currentTheme.text)
.padding(12)
.background(currentTheme.background.opacity(0.8))
.clipShape(Circle())
.circularGradientOutline()
.frame(width: 44, height: 44)
}
.padding(.leading)
@ -397,6 +435,7 @@ struct ReaderView: View {
.foregroundColor(currentTheme.text)
.lineLimit(1)
.truncationMode(.tail)
.padding(.trailing, 100)
Spacer()
@ -421,12 +460,13 @@ struct ReaderView: View {
goToNextChapter()
}) {
Image(systemName: "forward.end.fill")
.font(.system(size: 14, weight: .bold))
.font(.system(size: 16, weight: .bold))
.foregroundColor(currentTheme.text)
.padding(8)
.padding(12)
.background(currentTheme.background.opacity(0.8))
.clipShape(Circle())
.circularGradientOutline()
.frame(width: 44, height: 44)
}
.opacity(isHeaderVisible ? 1 : 0)
.offset(y: isHeaderVisible ? 0 : -100)
@ -438,20 +478,21 @@ struct ReaderView: View {
}
}) {
Image(systemName: "ellipsis")
.font(.system(size: 18, weight: .bold))
.font(.system(size: 16, weight: .bold))
.foregroundColor(currentTheme.text)
.padding(12)
.background(currentTheme.background.opacity(0.8))
.clipShape(Circle())
.circularGradientOutline()
.frame(width: 44, height: 44)
.rotationEffect(.degrees(isSettingsExpanded ? 90 : 0))
}
.opacity(isHeaderVisible ? 1 : 0)
.offset(y: isHeaderVisible ? 0 : -100)
.animation(.easeInOut(duration: 0.6), value: isHeaderVisible)
}
.padding(.trailing, 8)
}
.padding(.trailing, 8)
.padding(.top, (UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first?.windows.first?.safeAreaInsets.top ?? 0))
.padding(.bottom, 30)
.background(ProgressiveBlurView())
@ -581,6 +622,7 @@ struct ReaderView: View {
.background(currentTheme.background.opacity(0.8))
.clipShape(Circle())
.circularGradientOutline()
.frame(width: 44, height: 44)
.rotationEffect(.degrees(-90))
}
@ -628,6 +670,8 @@ struct ReaderView: View {
}
}
.padding(.top, 80)
.padding(.trailing, 8)
.frame(width: 60, alignment: .trailing)
.transition(.opacity)
}
}, alignment: .topTrailing
@ -642,43 +686,77 @@ struct ReaderView: View {
VStack {
Spacer()
HStack(spacing: 20) {
Spacer()
Button(action: {
isAutoScrolling.toggle()
}) {
Image(systemName: isAutoScrolling ? "pause.fill" : "play.fill")
.font(.system(size: 18, weight: .bold))
.foregroundColor(isAutoScrolling ? .red : currentTheme.text)
.padding(12)
.background(currentTheme.background.opacity(0.8))
.clipShape(Circle())
.circularGradientOutline()
}
.contextMenu {
VStack {
Text("Auto Scroll Speed")
.font(.headline)
.padding(.bottom, 8)
Slider(value: $autoScrollSpeed, in: 0.2...3.0, step: 0.1) {
Text("Speed")
}
.padding(.horizontal)
Text("Speed: \(String(format: "%.1f", autoScrollSpeed))x")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 4)
VStack(spacing: 0) {
HStack(spacing: 20) {
Spacer()
Button(action: {
isAutoScrolling.toggle()
}) {
Image(systemName: isAutoScrolling ? "pause.fill" : "play.fill")
.font(.system(size: 18, weight: .bold))
.foregroundColor(isAutoScrolling ? .red : currentTheme.text)
.padding(12)
.background(currentTheme.background.opacity(0.8))
.clipShape(Circle())
.circularGradientOutline()
}
.contextMenu {
VStack {
Text("Auto Scroll Speed")
.font(.headline)
.padding(.bottom, 8)
Slider(value: $autoScrollSpeed, in: 0.2...3.0, step: 0.1) {
Text("Speed")
}
.padding(.horizontal)
Text("Speed: \(String(format: "%.1f", autoScrollSpeed))x")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 4)
}
.padding()
}
.padding()
}
.padding(.horizontal, 20)
.padding(.top, 12)
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.fill(currentTheme.text.opacity(0.2))
.frame(height: 4)
Rectangle()
.fill(Color.accentColor)
.frame(width: max(0, min(CGFloat(readingProgress) * geometry.size.width, geometry.size.width)), height: 4)
Circle()
.fill(Color.accentColor)
.frame(width: 16, height: 16)
.shadow(color: Color.black.opacity(0.3), radius: 2, x: 0, y: 1)
.offset(x: max(0, min(CGFloat(readingProgress) * geometry.size.width, geometry.size.width)) - 8)
}
.cornerRadius(2)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
let percentage = min(max(value.location.x / geometry.size.width, 0), 1)
scrollToPosition(percentage)
}
)
}
.frame(height: 24)
.padding(.horizontal, 20)
.padding(.top, 8)
.padding(.bottom, (UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first?.windows.first?.safeAreaInsets.bottom ?? 0) + 16)
.frame(maxWidth: .infinity)
.background(ProgressiveBlurView())
}
.padding(.horizontal, 20)
.padding(.top, 20)
.padding(.bottom, (UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first?.windows.first?.safeAreaInsets.bottom ?? 0) + 20)
.frame(maxWidth: .infinity)
.background(ProgressiveBlurView())
.opacity(isHeaderVisible ? 1 : 0)
.offset(y: isHeaderVisible ? 0 : 100)
.animation(.easeInOut(duration: 0.6), value: isHeaderVisible)
@ -838,7 +916,7 @@ struct ReaderView: View {
UserDefaults.standard.set(imageUrl, forKey: "novelImageUrl_\(moduleId)_\(novelTitle)")
var progress = UserDefaults.standard.double(forKey: "readingProgress_\(chapterHref)")
var progress = UserDefaults.standard.double(forKey: "readingProgress_\(chapterHref)")
if progress < 0.01 {
progress = 0.01
@ -846,8 +924,8 @@ struct ReaderView: View {
Logger.shared.log("Saving continue reading item: title=\(novelTitle), chapter=\(chapterTitle), number=\(currentChapterNumber), href=\(chapterHref), progress=\(progress), imageUrl=\(imageUrl)", type: "Debug")
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil && !htmlContent.isEmpty {
@ -858,7 +936,7 @@ struct ReaderView: View {
mediaTitle: novelTitle,
chapterTitle: chapterTitle,
chapterNumber: currentChapterNumber,
imageUrl: imageUrl,
imageUrl: imageUrl,
href: chapterHref,
moduleId: moduleId,
progress: progress,
@ -919,8 +997,8 @@ struct ReaderView: View {
Logger.shared.log("Updating reading progress: \(roundedProgress) for \(chapterHref), title: \(novelTitle), image: \(imageUrl)", type: "Debug")
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil && !htmlContent.isEmpty {
@ -944,7 +1022,7 @@ struct ReaderView: View {
mediaTitle: novelTitle,
chapterTitle: chapterTitle,
chapterNumber: currentChapterNumber,
imageUrl: imageUrl,
imageUrl: imageUrl,
href: chapterHref,
moduleId: moduleId,
progress: roundedProgress,
@ -956,6 +1034,56 @@ struct ReaderView: View {
ContinueReadingManager.shared.save(item: item, htmlContent: validHtmlContent)
}
}
private func setStatusBarHidden(_ hidden: Bool) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
let windows = windowScene.windows
windows.forEach { window in
let viewController = window.rootViewController
viewController?.setNeedsStatusBarAppearanceUpdate()
}
}
}
private func scrollToPosition(_ percentage: CGFloat) {
readingProgress = Double(percentage)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first,
let rootVC = window.rootViewController {
let script = """
(function() {
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrollPosition = scrollHeight * \(percentage);
window.scrollTo({
top: scrollPosition,
behavior: 'auto'
});
return scrollPosition;
})();
"""
findWebView(in: rootVC.view)?.evaluateJavaScript(script, completionHandler: { _, error in
if let error = error {
Logger.shared.log("Error scrolling to position: \(error)", type: "Error")
}
})
}
}
private func findWebView(in view: UIView) -> WKWebView? {
if let webView = view as? WKWebView {
return webView
}
for subview in view.subviews {
if let webView = findWebView(in: subview) {
return webView
}
}
return nil
}
}
struct ColorPreviewCircle: View {
@ -1048,8 +1176,8 @@ struct HTMLView: UIViewRepresentable {
func startAutoScroll(webView: WKWebView) {
stopAutoScroll()
scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in
let scrollAmount = self.parent.autoScrollSpeed * 0.5
scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in
let scrollAmount = self.parent.autoScrollSpeed * 0.5
webView.evaluateJavaScript("window.scrollBy(0, \(scrollAmount));") { _, error in
if let error = error {
@ -1218,6 +1346,7 @@ struct HTMLView: UIViewRepresentable {
line-height: \(lineSpacing);
text-align: \(textAlignment);
padding: \(margin)px;
padding-top: calc(\(margin)px + 20px); /* Add extra padding at the top */
margin: 0;
color: \(colorPreset.text);
background-color: \(colorPreset.background);