diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 0539ded..1324fa9 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -31,6 +31,11 @@ class CustomMediaPlayerViewController: UIViewController { let onWatchNext: () -> Void let aniListID: Int + private var aniListUpdatedSuccessfully = false + private var aniListUpdateImpossible: Bool = false + private var aniListRetryCount = 0 + private let aniListMaxRetries = 6 + var player: AVPlayer! var timeObserverToken: Any? var inactivityTimer: Timer? @@ -48,6 +53,9 @@ class CustomMediaPlayerViewController: UIViewController { } private var isSkip85Visible: Bool { + if UserDefaults.standard.object(forKey: "skip85Visible") == nil { + return true + } return UserDefaults.standard.bool(forKey: "skip85Visible") } @@ -85,6 +93,7 @@ class CustomMediaPlayerViewController: UIViewController { var dismissButton: UIButton! var menuButton: UIButton! var watchNextButton: UIButton! + var watchNextIconButton: UIButton! var blackCoverView: UIView! var speedButton: UIButton! var skip85Button: UIButton! @@ -174,6 +183,7 @@ class CustomMediaPlayerViewController: UIViewController { setupControls() setupSkipAndDismissGestures() addInvisibleControlOverlays() + setupWatchNextButton() setupSubtitleLabel() setupDismissButton() setupSpeedButton() @@ -181,7 +191,6 @@ class CustomMediaPlayerViewController: UIViewController { setupMenuButton() setupMarqueeLabel() setupSkip85Button() - setupWatchNextButton() addTimeObserver() startUpdateTimer() setupAudioSession() @@ -200,6 +209,9 @@ class CustomMediaPlayerViewController: UIViewController { holdForPause() } + if #available(iOS 16.0, *) { + playerViewController.allowsVideoFrameAnalysis = false + } player.play() @@ -223,13 +235,11 @@ class CustomMediaPlayerViewController: UIViewController { }) } - /// In layoutSubviews, check if the text width is larger than the available space and update the label’s properties. override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - // Safely unwrap marqueeLabel guard let marqueeLabel = marqueeLabel else { - return // or handle the error gracefully + return } let availableWidth = marqueeLabel.frame.width @@ -353,6 +363,12 @@ class CustomMediaPlayerViewController: UIViewController { backwardButton.contentMode = .scaleAspectFit backwardButton.isUserInteractionEnabled = true + backwardButton.layer.shadowColor = UIColor.black.cgColor + backwardButton.layer.shadowOffset = CGSize(width: 0, height: 2) + backwardButton.layer.shadowOpacity = 0.6 + backwardButton.layer.shadowRadius = 4 + backwardButton.layer.masksToBounds = false + let backwardTap = UITapGestureRecognizer(target: self, action: #selector(seekBackward)) backwardTap.numberOfTapsRequired = 1 backwardButton.addGestureRecognizer(backwardTap) @@ -369,7 +385,15 @@ class CustomMediaPlayerViewController: UIViewController { playPauseButton.tintColor = .white playPauseButton.contentMode = .scaleAspectFit playPauseButton.isUserInteractionEnabled = true + + playPauseButton.layer.shadowColor = UIColor.black.cgColor + playPauseButton.layer.shadowOffset = CGSize(width: 0, height: 2) + playPauseButton.layer.shadowOpacity = 0.6 + playPauseButton.layer.shadowRadius = 4 + playPauseButton.layer.masksToBounds = false + let playPauseTap = UITapGestureRecognizer(target: self, action: #selector(togglePlayPause)) + playPauseButton.addGestureRecognizer(playPauseTap) controlsContainerView.addSubview(playPauseButton) playPauseButton.translatesAutoresizingMaskIntoConstraints = false @@ -379,6 +403,12 @@ class CustomMediaPlayerViewController: UIViewController { forwardButton.contentMode = .scaleAspectFit forwardButton.isUserInteractionEnabled = true + forwardButton.layer.shadowColor = UIColor.black.cgColor + forwardButton.layer.shadowOffset = CGSize(width: 0, height: 2) + forwardButton.layer.shadowOpacity = 0.6 + forwardButton.layer.shadowRadius = 4 + forwardButton.layer.masksToBounds = false + let forwardTap = UITapGestureRecognizer(target: self, action: #selector(seekForward)) forwardTap.numberOfTapsRequired = 1 forwardButton.addGestureRecognizer(forwardTap) @@ -596,13 +626,22 @@ class CustomMediaPlayerViewController: UIViewController { } func setupDismissButton() { + let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold) + let image = UIImage(systemName: "xmark", withConfiguration: config) + dismissButton = UIButton(type: .system) - dismissButton.setImage(UIImage(systemName: "xmark"), for: .normal) + dismissButton.setImage(image, for: .normal) dismissButton.tintColor = .white dismissButton.addTarget(self, action: #selector(dismissTapped), for: .touchUpInside) controlsContainerView.addSubview(dismissButton) dismissButton.translatesAutoresizingMaskIntoConstraints = false + dismissButton.layer.shadowColor = UIColor.black.cgColor + dismissButton.layer.shadowOffset = CGSize(width: 0, height: 2) + dismissButton.layer.shadowOpacity = 0.6 + dismissButton.layer.shadowRadius = 4 + dismissButton.layer.masksToBounds = false + NSLayoutConstraint.activate([ dismissButton.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 16), dismissButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), @@ -618,12 +657,18 @@ class CustomMediaPlayerViewController: UIViewController { marqueeLabel.textColor = .white marqueeLabel.font = UIFont.systemFont(ofSize: 14, weight: .heavy) - marqueeLabel.speed = .rate(30) // Adjust scrolling speed as needed + marqueeLabel.speed = .rate(35) // Adjust scrolling speed as needed marqueeLabel.fadeLength = 10.0 // Fading at the label’s edges marqueeLabel.leadingBuffer = 1.0 // Left inset for scrolling marqueeLabel.trailingBuffer = 16.0 // Right inset for scrolling marqueeLabel.animationDelay = 2.5 + marqueeLabel.layer.shadowColor = UIColor.black.cgColor + marqueeLabel.layer.shadowOffset = CGSize(width: 0, height: 2) + marqueeLabel.layer.shadowOpacity = 0.6 + marqueeLabel.layer.shadowRadius = 4 + marqueeLabel.layer.masksToBounds = false + marqueeLabel.lineBreakMode = .byTruncatingTail marqueeLabel.textAlignment = .left @@ -632,7 +677,7 @@ class CustomMediaPlayerViewController: UIViewController { // 1. Portrait mode with button visible portraitButtonVisibleConstraints = [ - marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12), + marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 8), marqueeLabel.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor, constant: -16), marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor) ] @@ -661,28 +706,16 @@ class CustomMediaPlayerViewController: UIViewController { } func updateMarqueeConstraints() { - // First, remove any existing marquee constraints. NSLayoutConstraint.deactivate(currentMarqueeConstraints) - - // Decide on spacing constants based on orientation. - let isPortrait = UIDevice.current.orientation.isPortrait || view.bounds.height > view.bounds.width - let leftSpacing: CGFloat = isPortrait ? 2 : 1 - let rightSpacing: CGFloat = isPortrait ? 16 : 8 - - // Determine which button to use for the trailing anchor. - var trailingAnchor: NSLayoutXAxisAnchor = controlsContainerView.trailingAnchor // default fallback - if let menu = menuButton, !menu.isHidden { - trailingAnchor = menu.leadingAnchor - } else if let quality = qualityButton, !quality.isHidden { - trailingAnchor = quality.leadingAnchor - } else if let speed = speedButton, !speed.isHidden { - trailingAnchor = speed.leadingAnchor - } - - // Create new constraints for the marquee label. + + let leftSpacing: CGFloat = 8 + let rightSpacing: CGFloat = 8 + currentMarqueeConstraints = [ marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: leftSpacing), - marqueeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -rightSpacing), + + marqueeLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -rightSpacing-20), + marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor) ] @@ -691,8 +724,11 @@ class CustomMediaPlayerViewController: UIViewController { } func setupMenuButton() { + let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold) + let image = UIImage(systemName: "text.bubble", withConfiguration: config) + menuButton = UIButton(type: .system) - menuButton.setImage(UIImage(systemName: "text.bubble"), for: .normal) + menuButton.setImage(image, for: .normal) menuButton.tintColor = .white if let subtitlesURL = subtitlesURL, !subtitlesURL.isEmpty { @@ -702,20 +738,29 @@ class CustomMediaPlayerViewController: UIViewController { menuButton.isHidden = true } + dismissButton.layer.shadowColor = UIColor.black.cgColor + dismissButton.layer.shadowOffset = CGSize(width: 0, height: 2) + dismissButton.layer.shadowOpacity = 0.6 + dismissButton.layer.shadowRadius = 4 + dismissButton.layer.masksToBounds = false + controlsContainerView.addSubview(menuButton) menuButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - menuButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20), + menuButton.topAnchor.constraint(equalTo: qualityButton.topAnchor), + menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -8), menuButton.widthAnchor.constraint(equalToConstant: 40), menuButton.heightAnchor.constraint(equalToConstant: 40) ]) } func setupSpeedButton() { + let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold) + let image = UIImage(systemName: "speedometer", withConfiguration: config) + speedButton = UIButton(type: .system) - speedButton.setImage(UIImage(systemName: "speedometer"), for: .normal) + speedButton.setImage(image, for: .normal) speedButton.tintColor = .white speedButton.showsMenuAsPrimaryAction = true speedButton.menu = speedChangerMenu() @@ -724,62 +769,66 @@ class CustomMediaPlayerViewController: UIViewController { speedButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - speedButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - speedButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -20), + speedButton.topAnchor.constraint(equalTo: watchNextButton.topAnchor), + speedButton.trailingAnchor.constraint(equalTo: watchNextButton.leadingAnchor, constant: 20), speedButton.widthAnchor.constraint(equalToConstant: 40), speedButton.heightAnchor.constraint(equalToConstant: 40) ]) } func setupWatchNextButton() { - let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .regular) - let image = UIImage(systemName: "forward.fill", withConfiguration: config) + let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold) + let image = UIImage(systemName: "forward.end", withConfiguration: config) watchNextButton = UIButton(type: .system) - watchNextButton.setTitle(" Play Next", for: .normal) - watchNextButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) watchNextButton.setImage(image, for: .normal) - watchNextButton.tintColor = .black - watchNextButton.backgroundColor = .white - watchNextButton.layer.cornerRadius = 25 - watchNextButton.setTitleColor(.black, for: .normal) - watchNextButton.addTarget(self, action: #selector(watchNextTapped), for: .touchUpInside) - watchNextButton.alpha = 0.0 - watchNextButton.isHidden = true + watchNextButton.backgroundColor = .clear + watchNextButton.tintColor = .white + watchNextButton.setTitleColor(.white, for: .normal) - view.addSubview(watchNextButton) + // The shadow: + watchNextButton.layer.shadowColor = UIColor.black.cgColor + watchNextButton.layer.shadowOffset = CGSize(width: 0, height: 2) + watchNextButton.layer.shadowOpacity = 0.6 + watchNextButton.layer.shadowRadius = 4 + watchNextButton.layer.masksToBounds = false + + watchNextButton.addTarget(self, action: #selector(watchNextTapped), for: .touchUpInside) + + controlsContainerView.addSubview(watchNextButton) watchNextButton.translatesAutoresizingMaskIntoConstraints = false - watchNextButtonNormalConstraints = [ - watchNextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.centerYAnchor), - watchNextButton.heightAnchor.constraint(equalToConstant: 50), - watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) - ] - - watchNextButtonControlsConstraints = [ - watchNextButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor), + NSLayoutConstraint.activate([ + watchNextButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor, constant: 20), watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5), - watchNextButton.heightAnchor.constraint(equalToConstant: 47), - watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 97) - ] - - NSLayoutConstraint.activate(watchNextButtonNormalConstraints) + watchNextButton.heightAnchor.constraint(equalToConstant: 40), + watchNextButton.widthAnchor.constraint(equalToConstant: 80) + ]) } func setupSkip85Button() { - let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .regular) + let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold) let image = UIImage(systemName: "goforward", withConfiguration: config) skip85Button = UIButton(type: .system) skip85Button.setTitle(" Skip 85s", for: .normal) - skip85Button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + skip85Button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold) skip85Button.setImage(image, for: .normal) - skip85Button.tintColor = .black - skip85Button.backgroundColor = .white - skip85Button.layer.cornerRadius = 25 - skip85Button.setTitleColor(.black, for: .normal) - skip85Button.alpha = 0.8 + + skip85Button.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8) + skip85Button.tintColor = .white + skip85Button.setTitleColor(.white, for: .normal) + skip85Button.layer.cornerRadius = 21 + skip85Button.alpha = 0.7 + + skip85Button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) + + skip85Button.layer.shadowColor = UIColor.black.cgColor + skip85Button.layer.shadowOffset = CGSize(width: 0, height: 2) + skip85Button.layer.shadowOpacity = 0.6 + skip85Button.layer.shadowRadius = 4 + skip85Button.layer.masksToBounds = false + skip85Button.addTarget(self, action: #selector(skip85Tapped), for: .touchUpInside) view.addSubview(skip85Button) @@ -788,27 +837,37 @@ class CustomMediaPlayerViewController: UIViewController { NSLayoutConstraint.activate([ skip85Button.leadingAnchor.constraint(equalTo: sliderHostingController!.view.leadingAnchor), skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5), - skip85Button.heightAnchor.constraint(equalToConstant: 47), + skip85Button.heightAnchor.constraint(equalToConstant: 40), skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 97) ]) skip85Button.isHidden = !isSkip85Visible } + private func setupQualityButton() { + let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold) + let image = UIImage(systemName: "4k.tv", withConfiguration: config) + qualityButton = UIButton(type: .system) - qualityButton.setImage(UIImage(systemName: "4k.tv"), for: .normal) + qualityButton.setImage(image, for: .normal) qualityButton.tintColor = .white qualityButton.showsMenuAsPrimaryAction = true qualityButton.menu = qualitySelectionMenu() qualityButton.isHidden = true + speedButton.layer.shadowColor = UIColor.black.cgColor + speedButton.layer.shadowOffset = CGSize(width: 0, height: 2) + speedButton.layer.shadowOpacity = 0.6 + speedButton.layer.shadowRadius = 4 + speedButton.layer.masksToBounds = false + controlsContainerView.addSubview(qualityButton) qualityButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - qualityButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -20), + qualityButton.topAnchor.constraint(equalTo: speedButton.topAnchor), + qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -8), qualityButton.widthAnchor.constraint(equalToConstant: 40), qualityButton.heightAnchor.constraint(equalToConstant: 40) ]) @@ -887,18 +946,16 @@ class CustomMediaPlayerViewController: UIViewController { ContinueWatchingManager.shared.save(item: item) } + let remainingPercentage = (self.duration - self.currentTimeVal) / self.duration - if remainingPercentage < 0.1 && self.module.metadata.type == "anime" && self.aniListID != 0 { - let aniListMutation = AniListMutation() - aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { result in - switch result { - case .success: - Logger.shared.log("Successfully updated AniList progress for episode \(self.episodeNumber)", type: "General") - case .failure(let error): - Logger.shared.log("Failed to update AniList progress: \(error.localizedDescription)", type: "Error") - } - } + if remainingPercentage < 0.1 && + self.module.metadata.type == "anime" && + self.aniListID != 0 && + !self.aniListUpdatedSuccessfully && + !self.aniListUpdateImpossible + { + self.tryAniListUpdate() } self.sliderHostingController?.rootView = MusicProgressSlider( @@ -932,46 +989,14 @@ class CustomMediaPlayerViewController: UIViewController { && self.showWatchNextButton && self.duration != 0 + if isNearEnd { - if !self.isWatchNextVisible { - self.isWatchNextVisible = true - self.watchNextButtonAppearedAt = self.currentTimeVal - - if self.isControlsVisible { - NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) - NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - } else { - NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) - NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) - } - self.watchNextButton.isHidden = false - self.watchNextButton.alpha = 0.0 - UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { - self.watchNextButton.alpha = 0.8 - }, completion: nil) - } - - if let appearedAt = self.watchNextButtonAppearedAt, - (self.currentTimeVal - appearedAt) >= 5, - !self.isWatchNextRepositioned { - UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { - self.watchNextButton.alpha = 0.0 - }, completion: { _ in - self.watchNextButton.isHidden = true - NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) - NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - self.isWatchNextRepositioned = true - }) + if !isWatchNextVisible { + watchNextButtonAppearedAt = currentTimeVal + } } else { - self.watchNextButtonAppearedAt = nil - self.isWatchNextVisible = false - self.isWatchNextRepositioned = false - UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { - self.watchNextButton.alpha = 0.0 - }, completion: { _ in - self.watchNextButton.isHidden = true - }) + } } } @@ -1000,32 +1025,10 @@ class CustomMediaPlayerViewController: UIViewController { @objc func toggleControls() { isControlsVisible.toggle() UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: { - self.controlsContainerView.alpha = self.isControlsVisible ? 1 : 0 - self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0 - - if self.isControlsVisible { - NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) - NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - if self.isWatchNextRepositioned || self.isWatchNextVisible { - self.watchNextButton.isHidden = false - UIView.animate(withDuration: 0.3, animations: { - self.watchNextButton.alpha = 0.8 - }) - } - } else { - if !self.isWatchNextRepositioned && self.isWatchNextVisible { - NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) - NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) - } - if self.isWatchNextRepositioned { - UIView.animate(withDuration: 0.5, animations: { - self.watchNextButton.alpha = 0.0 - }, completion: { _ in - self.watchNextButton.isHidden = true - }) - } - } - self.view.layoutIfNeeded() + let alphaVal: CGFloat = self.isControlsVisible ? 1 : 0 + self.controlsContainerView.alpha = alphaVal + self.skip85Button.alpha = alphaVal + self.watchNextButton.alpha = alphaVal }) } @@ -1146,6 +1149,42 @@ class CustomMediaPlayerViewController: UIViewController { return UIMenu(title: "Playback Speed", children: playbackSpeedActions) } + private func tryAniListUpdate() { + let aniListMutation = AniListMutation() + aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + self.aniListUpdatedSuccessfully = true + Logger.shared.log("Successfully updated AniList progress for episode \(self.episodeNumber)", type: "General") + + case .failure(let error): + let errorString = error.localizedDescription.lowercased() + Logger.shared.log("AniList progress update failed: \(errorString)", type: "Error") + + if errorString.contains("access token not found") { + Logger.shared.log("AniList update will NOT retry due to missing token.", type: "Error") + self.aniListUpdateImpossible = true + + } else { + if self.aniListRetryCount < self.aniListMaxRetries { + self.aniListRetryCount += 1 + + let delaySeconds = 5.0 + Logger.shared.log("AniList update will retry in \(delaySeconds)s (attempt \(self.aniListRetryCount)).", type: "Debug") + + DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { + self.tryAniListUpdate() + } + } else { + Logger.shared.log("AniList update reached max retries. No more attempts.", type: "Error") + } + } + } + } + } + private func parseM3U8(url: URL, completion: @escaping () -> Void) { var request = URLRequest(url: url) request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index b2580e2..2afb2da 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -176,7 +176,7 @@ struct ModuleAdditionSettingsView: View { let _ = try await moduleManager.addModule(metadataUrl: moduleUrl) await MainActor.run { isLoading = false - DropManager.shared.showDrop(title: "Module Added", subtitle: "click it to select it", duration: 2.0, icon: UIImage(systemName:"gear.badge.checkmark")) + DropManager.shared.showDrop(title: "Module Added", subtitle: "Click it to select it.", duration: 2.0, icon: UIImage(systemName:"gear.badge.checkmark")) self.presentationMode.wrappedValue.dismiss() } } catch { diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 0048c41..99eb462 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -335,9 +335,11 @@ struct ContinueWatchingCell: View { let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(item.fullUrl)") if totalTime > 0 { - currentProgress = lastPlayedTime / totalTime + let ratio = lastPlayedTime / totalTime + // Clamp ratio between 0 and 1: + currentProgress = max(0, min(ratio, 1)) } else { - currentProgress = item.progress + currentProgress = max(0, min(item.progress, 1)) } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift index db620b8..1f52970 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift @@ -142,14 +142,48 @@ struct SettingsViewModule: View { } func showAddModuleAlert() { - let alert = UIAlertController(title: "Add Module", message: "Enter the URL of the module file", preferredStyle: .alert) + let pasteboardString = UIPasteboard.general.string ?? "" + + if !pasteboardString.isEmpty { + let clipboardAlert = UIAlertController( + title: "Clipboard Detected", + message: "We found some text in your clipboard. Would you like to use it as the module URL?", + preferredStyle: .alert + ) + + clipboardAlert.addAction(UIAlertAction(title: "Use Clipboard", style: .default, handler: { _ in + self.displayModuleView(url: pasteboardString) + })) + + clipboardAlert.addAction(UIAlertAction(title: "Enter Manually", style: .cancel, handler: { _ in + self.showManualUrlAlert() + })) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(clipboardAlert, animated: true, completion: nil) + } + + } else { + showManualUrlAlert() + } + } + + func showManualUrlAlert() { + let alert = UIAlertController( + title: "Add Module", + message: "Enter the URL of the module file", + preferredStyle: .alert + ) + alert.addTextField { textField in textField.placeholder = "https://real.url/module.json" } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { _ in - if let url = alert.textFields?.first?.text { - displayModuleView(url: url) + if let url = alert.textFields?.first?.text, !url.isEmpty { + self.displayModuleView(url: url) } })) @@ -158,16 +192,18 @@ struct SettingsViewModule: View { rootViewController.present(alert, animated: true, completion: nil) } } - + func displayModuleView(url: String) { DispatchQueue.main.async { - let addModuleView = ModuleAdditionSettingsView(moduleUrl: url).environmentObject(moduleManager) + let addModuleView = ModuleAdditionSettingsView(moduleUrl: url) + .environmentObject(self.moduleManager) let hostingController = UIHostingController(rootView: addModuleView) - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first { - window.rootViewController?.present(hostingController, animated: true, completion: nil) - } + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + window.rootViewController?.present(hostingController, animated: true, completion: nil) + } } } + } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 78e61a5..cff7441 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -10,7 +10,6 @@ import SwiftUI struct SettingsViewPlayer: View { @AppStorage("externalPlayer") private var externalPlayer: String = "Sora" @AppStorage("alwaysLandscape") private var isAlwaysLandscape = false - @AppStorage("hideNextButton") private var isHideNextButton = false @AppStorage("rememberPlaySpeed") private var isRememberPlaySpeed = false @AppStorage("holdSpeedPlayer") private var holdSpeedPlayer: Double = 2.0 @AppStorage("skipIncrement") private var skipIncrement: Double = 10.0 @@ -38,9 +37,6 @@ struct SettingsViewPlayer: View { } } - Toggle("Hide 'Watch Next' after 5s", isOn: $isHideNextButton) - .tint(.accentColor) - Toggle("Force Landscape", isOn: $isAlwaysLandscape) .tint(.accentColor)