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

View file

@ -31,6 +31,7 @@ struct LibraryView: View {
@State private var continueReadingItems: [ContinueReadingItem] = [] @State private var continueReadingItems: [ContinueReadingItem] = []
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
@State private var selectedTab: Int = 0 @State private var selectedTab: Int = 0
@State private var isActive: Bool = false
private var librarySectionsOrder: [String] { private var librarySectionsOrder: [String] {
(try? JSONDecoder().decode([String].self, from: librarySectionsOrderData)) ?? ["continueWatching", "continueReading", "collections"] (try? JSONDecoder().decode([String].self, from: librarySectionsOrderData)) ?? ["continueWatching", "continueReading", "collections"]
@ -99,15 +100,51 @@ struct LibraryView: View {
.scrollViewBottomPadding() .scrollViewBottomPadding()
.deviceScaled() .deviceScaled()
.onAppear { .onAppear {
isActive = true
fetchContinueWatching() fetchContinueWatching()
fetchContinueReading() 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 .onChange(of: scenePhase) { newPhase in
if newPhase == .active { if newPhase == .active {
fetchContinueWatching() fetchContinueWatching()
fetchContinueReading() 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 chapterHref: String
let chapterTitle: String let chapterTitle: String
let chapters: [[String: Any]] let chapters: [[String: Any]]
let mediaTitle: String let mediaTitle: String
let chapterNumber: Int let chapterNumber: Int
@State private var htmlContent: String = "" @State private var htmlContent: String = ""
@ -55,6 +55,9 @@ struct ReaderView: View {
@StateObject private var navigator = ChapterNavigator.shared @StateObject private var navigator = ChapterNavigator.shared
// Status bar control
@State private var statusBarHidden = false
private let fontOptions = [ private let fontOptions = [
("-apple-system", "System"), ("-apple-system", "System"),
("Georgia", "Georgia"), ("Georgia", "Georgia"),
@ -150,6 +153,8 @@ struct ReaderView: View {
.onTapGesture { .onTapGesture {
withAnimation(.easeInOut(duration: 0.6)) { withAnimation(.easeInOut(duration: 0.6)) {
isHeaderVisible.toggle() isHeaderVisible.toggle()
statusBarHidden = !isHeaderVisible
setStatusBarHidden(!isHeaderVisible)
if !isHeaderVisible { if !isHeaderVisible {
isSettingsExpanded = false isSettingsExpanded = false
} }
@ -157,40 +162,42 @@ struct ReaderView: View {
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
HTMLView( HTMLView(
htmlContent: htmlContent, htmlContent: htmlContent,
fontSize: fontSize, fontSize: fontSize,
fontFamily: selectedFont, fontFamily: selectedFont,
fontWeight: fontWeight, fontWeight: fontWeight,
textAlignment: textAlignment, textAlignment: textAlignment,
lineSpacing: lineSpacing, lineSpacing: lineSpacing,
margin: margin, margin: margin,
isAutoScrolling: $isAutoScrolling, isAutoScrolling: $isAutoScrolling,
autoScrollSpeed: autoScrollSpeed, autoScrollSpeed: autoScrollSpeed,
colorPreset: colorPresets[selectedColorPreset], colorPreset: colorPresets[selectedColorPreset],
chapterHref: chapterHref, chapterHref: chapterHref,
onProgressChanged: { progress in onProgressChanged: { progress in
self.readingProgress = progress self.readingProgress = progress
if Date().timeIntervalSince(self.lastProgressUpdate) > 2.0 { if Date().timeIntervalSince(self.lastProgressUpdate) > 2.0 {
self.updateReadingProgress(progress: progress) self.updateReadingProgress(progress: progress)
self.lastProgressUpdate = Date() self.lastProgressUpdate = Date()
Logger.shared.log("Progress updated to \(progress)", type: "Debug") Logger.shared.log("Progress updated to \(progress)", type: "Debug")
}
} }
} )
)
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal) .padding(.horizontal)
.simultaneousGesture(TapGesture().onEnded { .simultaneousGesture(TapGesture().onEnded {
withAnimation(.easeInOut(duration: 0.6)) { withAnimation(.easeInOut(duration: 0.6)) {
isHeaderVisible.toggle() isHeaderVisible.toggle()
statusBarHidden = !isHeaderVisible
setStatusBarHidden(!isHeaderVisible)
if !isHeaderVisible { if !isHeaderVisible {
isSettingsExpanded = false 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 headerView
@ -198,12 +205,12 @@ struct ReaderView: View {
.offset(y: isHeaderVisible ? 0 : -100) .offset(y: isHeaderVisible ? 0 : -100)
.allowsHitTesting(isHeaderVisible) .allowsHitTesting(isHeaderVisible)
.animation(.easeInOut(duration: 0.6), value: isHeaderVisible) .animation(.easeInOut(duration: 0.6), value: isHeaderVisible)
.zIndex(1) .zIndex(1)
if isHeaderVisible { if isHeaderVisible {
footerView footerView
.transition(.move(edge: .bottom)) .transition(.move(edge: .bottom))
.zIndex(2) .zIndex(2)
} }
} }
.navigationBarHidden(true) .navigationBarHidden(true)
@ -216,11 +223,20 @@ struct ReaderView: View {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first, let window = windowScene.windows.first,
let navigationController = window.rootViewController?.children.first as? UINavigationController { 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) NotificationCenter.default.post(name: .hideTabBar, object: nil)
UserDefaults.standard.set(true, forKey: "isReaderActive") 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 { .onDisappear {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
@ -261,8 +277,8 @@ struct ReaderView: View {
} }
} else { } else {
if !htmlContent.isEmpty { if !htmlContent.isEmpty {
let validHtmlContent = (!htmlContent.isEmpty && let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") && !htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil { if validHtmlContent == nil {
@ -286,19 +302,31 @@ struct ReaderView: View {
} }
} }
UserDefaults.standard.set(false, forKey: "isReaderActive") UserDefaults.standard.set(false, forKey: "isReaderActive")
setStatusBarHidden(false)
} }
.task { .task {
do { do {
ensureModuleLoaded() ensureModuleLoaded()
let isOffline = !(NetworkMonitor.shared.isConnected)
if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref), let isConnected = await NetworkMonitor.shared.ensureNetworkStatusInitialized()
!cachedContent.isEmpty && let isOffline = !isConnected
!cachedContent.contains("undefined") &&
if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref),
!cachedContent.isEmpty &&
!cachedContent.contains("undefined") &&
cachedContent.count > 50 { cachedContent.count > 50 {
Logger.shared.log("Using cached HTML content for \(chapterHref)", type: "Debug") Logger.shared.log("Using cached HTML content for \(chapterHref)", type: "Debug")
htmlContent = cachedContent htmlContent = cachedContent
isLoading = false isLoading = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(.easeInOut(duration: 0.3)) {
isHeaderVisible = false
statusBarHidden = true
setStatusBarHidden(true)
}
}
} else if isOffline { } else if isOffline {
let offlineError = NSError(domain: "Sora", code: -1009, userInfo: [NSLocalizedDescriptionKey: "No network connection."]) let offlineError = NSError(domain: "Sora", code: -1009, userInfo: [NSLocalizedDescriptionKey: "No network connection."])
self.error = offlineError self.error = offlineError
@ -331,6 +359,14 @@ struct ReaderView: View {
htmlContent = content htmlContent = content
isLoading = false 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), if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref),
cachedContent.isEmpty || cachedContent.contains("undefined") || cachedContent.count < 50 { cachedContent.isEmpty || cachedContent.contains("undefined") || cachedContent.count < 50 {
let item = ContinueReadingItem( let item = ContinueReadingItem(
@ -367,6 +403,7 @@ struct ReaderView: View {
} }
} }
} }
.statusBar(hidden: statusBarHidden)
} }
private func stopAutoScroll() { private func stopAutoScroll() {
@ -383,12 +420,13 @@ struct ReaderView: View {
dismiss() dismiss()
}) { }) {
Image(systemName: "chevron.left") Image(systemName: "chevron.left")
.font(.system(size: 20, weight: .bold)) .font(.system(size: 16, weight: .bold))
.foregroundColor(currentTheme.text) .foregroundColor(currentTheme.text)
.padding(12) .padding(12)
.background(currentTheme.background.opacity(0.8)) .background(currentTheme.background.opacity(0.8))
.clipShape(Circle()) .clipShape(Circle())
.circularGradientOutline() .circularGradientOutline()
.frame(width: 44, height: 44)
} }
.padding(.leading) .padding(.leading)
@ -397,6 +435,7 @@ struct ReaderView: View {
.foregroundColor(currentTheme.text) .foregroundColor(currentTheme.text)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
.padding(.trailing, 100)
Spacer() Spacer()
@ -421,12 +460,13 @@ struct ReaderView: View {
goToNextChapter() goToNextChapter()
}) { }) {
Image(systemName: "forward.end.fill") Image(systemName: "forward.end.fill")
.font(.system(size: 14, weight: .bold)) .font(.system(size: 16, weight: .bold))
.foregroundColor(currentTheme.text) .foregroundColor(currentTheme.text)
.padding(8) .padding(12)
.background(currentTheme.background.opacity(0.8)) .background(currentTheme.background.opacity(0.8))
.clipShape(Circle()) .clipShape(Circle())
.circularGradientOutline() .circularGradientOutline()
.frame(width: 44, height: 44)
} }
.opacity(isHeaderVisible ? 1 : 0) .opacity(isHeaderVisible ? 1 : 0)
.offset(y: isHeaderVisible ? 0 : -100) .offset(y: isHeaderVisible ? 0 : -100)
@ -438,20 +478,21 @@ struct ReaderView: View {
} }
}) { }) {
Image(systemName: "ellipsis") Image(systemName: "ellipsis")
.font(.system(size: 18, weight: .bold)) .font(.system(size: 16, weight: .bold))
.foregroundColor(currentTheme.text) .foregroundColor(currentTheme.text)
.padding(12) .padding(12)
.background(currentTheme.background.opacity(0.8)) .background(currentTheme.background.opacity(0.8))
.clipShape(Circle()) .clipShape(Circle())
.circularGradientOutline() .circularGradientOutline()
.frame(width: 44, height: 44)
.rotationEffect(.degrees(isSettingsExpanded ? 90 : 0)) .rotationEffect(.degrees(isSettingsExpanded ? 90 : 0))
} }
.opacity(isHeaderVisible ? 1 : 0) .opacity(isHeaderVisible ? 1 : 0)
.offset(y: isHeaderVisible ? 0 : -100) .offset(y: isHeaderVisible ? 0 : -100)
.animation(.easeInOut(duration: 0.6), value: isHeaderVisible) .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(.top, (UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first?.windows.first?.safeAreaInsets.top ?? 0))
.padding(.bottom, 30) .padding(.bottom, 30)
.background(ProgressiveBlurView()) .background(ProgressiveBlurView())
@ -581,6 +622,7 @@ struct ReaderView: View {
.background(currentTheme.background.opacity(0.8)) .background(currentTheme.background.opacity(0.8))
.clipShape(Circle()) .clipShape(Circle())
.circularGradientOutline() .circularGradientOutline()
.frame(width: 44, height: 44)
.rotationEffect(.degrees(-90)) .rotationEffect(.degrees(-90))
} }
@ -628,6 +670,8 @@ struct ReaderView: View {
} }
} }
.padding(.top, 80) .padding(.top, 80)
.padding(.trailing, 8)
.frame(width: 60, alignment: .trailing)
.transition(.opacity) .transition(.opacity)
} }
}, alignment: .topTrailing }, alignment: .topTrailing
@ -642,43 +686,77 @@ struct ReaderView: View {
VStack { VStack {
Spacer() Spacer()
HStack(spacing: 20) { VStack(spacing: 0) {
Spacer() HStack(spacing: 20) {
Button(action: { Spacer()
isAutoScrolling.toggle() Button(action: {
}) { isAutoScrolling.toggle()
Image(systemName: isAutoScrolling ? "pause.fill" : "play.fill") }) {
.font(.system(size: 18, weight: .bold)) Image(systemName: isAutoScrolling ? "pause.fill" : "play.fill")
.foregroundColor(isAutoScrolling ? .red : currentTheme.text) .font(.system(size: 18, weight: .bold))
.padding(12) .foregroundColor(isAutoScrolling ? .red : currentTheme.text)
.background(currentTheme.background.opacity(0.8)) .padding(12)
.clipShape(Circle()) .background(currentTheme.background.opacity(0.8))
.circularGradientOutline() .clipShape(Circle())
} .circularGradientOutline()
.contextMenu { }
VStack { .contextMenu {
Text("Auto Scroll Speed") VStack {
.font(.headline) Text("Auto Scroll Speed")
.padding(.bottom, 8) .font(.headline)
.padding(.bottom, 8)
Slider(value: $autoScrollSpeed, in: 0.2...3.0, step: 0.1) {
Text("Speed") Slider(value: $autoScrollSpeed, in: 0.2...3.0, step: 0.1) {
} Text("Speed")
.padding(.horizontal) }
.padding(.horizontal)
Text("Speed: \(String(format: "%.1f", autoScrollSpeed))x")
.font(.caption) Text("Speed: \(String(format: "%.1f", autoScrollSpeed))x")
.foregroundColor(.secondary) .font(.caption)
.padding(.top, 4) .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) .opacity(isHeaderVisible ? 1 : 0)
.offset(y: isHeaderVisible ? 0 : 100) .offset(y: isHeaderVisible ? 0 : 100)
.animation(.easeInOut(duration: 0.6), value: isHeaderVisible) .animation(.easeInOut(duration: 0.6), value: isHeaderVisible)
@ -838,7 +916,7 @@ struct ReaderView: View {
UserDefaults.standard.set(imageUrl, forKey: "novelImageUrl_\(moduleId)_\(novelTitle)") 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 { if progress < 0.01 {
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") 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 && let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") && !htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil && !htmlContent.isEmpty { if validHtmlContent == nil && !htmlContent.isEmpty {
@ -858,7 +936,7 @@ struct ReaderView: View {
mediaTitle: novelTitle, mediaTitle: novelTitle,
chapterTitle: chapterTitle, chapterTitle: chapterTitle,
chapterNumber: currentChapterNumber, chapterNumber: currentChapterNumber,
imageUrl: imageUrl, imageUrl: imageUrl,
href: chapterHref, href: chapterHref,
moduleId: moduleId, moduleId: moduleId,
progress: progress, progress: progress,
@ -919,8 +997,8 @@ struct ReaderView: View {
Logger.shared.log("Updating reading progress: \(roundedProgress) for \(chapterHref), title: \(novelTitle), image: \(imageUrl)", type: "Debug") Logger.shared.log("Updating reading progress: \(roundedProgress) for \(chapterHref), title: \(novelTitle), image: \(imageUrl)", type: "Debug")
let validHtmlContent = (!htmlContent.isEmpty && let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") && !htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil && !htmlContent.isEmpty { if validHtmlContent == nil && !htmlContent.isEmpty {
@ -944,7 +1022,7 @@ struct ReaderView: View {
mediaTitle: novelTitle, mediaTitle: novelTitle,
chapterTitle: chapterTitle, chapterTitle: chapterTitle,
chapterNumber: currentChapterNumber, chapterNumber: currentChapterNumber,
imageUrl: imageUrl, imageUrl: imageUrl,
href: chapterHref, href: chapterHref,
moduleId: moduleId, moduleId: moduleId,
progress: roundedProgress, progress: roundedProgress,
@ -956,6 +1034,56 @@ struct ReaderView: View {
ContinueReadingManager.shared.save(item: item, htmlContent: validHtmlContent) 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 { struct ColorPreviewCircle: View {
@ -1048,8 +1176,8 @@ struct HTMLView: UIViewRepresentable {
func startAutoScroll(webView: WKWebView) { func startAutoScroll(webView: WKWebView) {
stopAutoScroll() stopAutoScroll()
scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in
let scrollAmount = self.parent.autoScrollSpeed * 0.5 let scrollAmount = self.parent.autoScrollSpeed * 0.5
webView.evaluateJavaScript("window.scrollBy(0, \(scrollAmount));") { _, error in webView.evaluateJavaScript("window.scrollBy(0, \(scrollAmount));") { _, error in
if let error = error { if let error = error {
@ -1218,6 +1346,7 @@ struct HTMLView: UIViewRepresentable {
line-height: \(lineSpacing); line-height: \(lineSpacing);
text-align: \(textAlignment); text-align: \(textAlignment);
padding: \(margin)px; padding: \(margin)px;
padding-top: calc(\(margin)px + 20px); /* Add extra padding at the top */
margin: 0; margin: 0;
color: \(colorPreset.text); color: \(colorPreset.text);
background-color: \(colorPreset.background); background-color: \(colorPreset.background);