mirror of
https://github.com/cranci1/Sora.git
synced 2026-05-11 20:40:39 +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
|
) { _ 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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue