diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift index 563f32d..6bbf386 100644 --- a/Sora/ContentView.swift +++ b/Sora/ContentView.swift @@ -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") } } } diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index c18a115..8c6ab67 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -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() } } } diff --git a/Sora/Utlis & Misc/Extensions/URLSession.swift b/Sora/Utlis & Misc/Extensions/URLSession.swift index 63beafd..72a81b1 100644 --- a/Sora/Utlis & Misc/Extensions/URLSession.swift +++ b/Sora/Utlis & Misc/Extensions/URLSession.swift @@ -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() } diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 7b370df..2b29f0a 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -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) } } } diff --git a/Sora/Views/ReaderView/ReaderView.swift b/Sora/Views/ReaderView/ReaderView.swift index cc7397a..80202d8 100644 --- a/Sora/Views/ReaderView/ReaderView.swift +++ b/Sora/Views/ReaderView/ReaderView.swift @@ -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);