mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
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:
parent
a926327362
commit
fe0750078e
5 changed files with 529 additions and 195 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue