diff --git a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate index cf85bbf..50b1dc3 100644 Binary files a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate and b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sora/Utils/CustomPlayer/CustomPlayer.swift b/Sora/Utils/CustomPlayer/CustomPlayer.swift index 5f09a12..5ed7488 100644 --- a/Sora/Utils/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/CustomPlayer/CustomPlayer.swift @@ -12,7 +12,7 @@ struct CustomVideoPlayer: UIViewControllerRepresentable { let player: AVPlayer func makeUIViewController(context: Context) -> AVPlayerViewController { - let controller = CustomAVPlayerViewController() + let controller = NormalPlayer() controller.player = player controller.showsPlaybackControls = false player.play() @@ -24,16 +24,6 @@ struct CustomVideoPlayer: UIViewControllerRepresentable { } } -class CustomAVPlayerViewController: AVPlayerViewController { - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - if UserDefaults.standard.bool(forKey: "alwaysLandscape") { - return .landscape - } else { - return .all - } - } -} - struct CustomMediaPlayer: View { @State private var player: AVPlayer @State private var isPlaying = true @@ -41,13 +31,23 @@ struct CustomMediaPlayer: View { @State private var duration: Double = 0.0 @State private var showControls = false @State private var inactivityTimer: Timer? + @State private var timeObserverToken: Any? @Environment(\.presentationMode) var presentationMode - init(urlString: String) { + let fullUrl: String + let title: String + let episodeNumber: Int + let onWatchNext: () -> Void + + init(urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) { guard let url = URL(string: urlString) else { fatalError("Invalid URL string") } _player = State(initialValue: AVPlayer(url: url)) + self.fullUrl = fullUrl + self.title = title + self.episodeNumber = episodeNumber + self.onWatchNext = onWatchNext } var body: some View { @@ -63,6 +63,7 @@ struct CustomMediaPlayer: View { } } startUpdatingCurrentTime() + addPeriodicTimeObserver(fullURL: fullUrl) } .edgesIgnoringSafeArea(.all) .overlay( @@ -118,11 +119,30 @@ struct CustomMediaPlayer: View { VStack { Spacer() - if showControls { - VStack { + VStack { + Spacer() + HStack { Spacer() - HStack { - Spacer() + if duration - currentTime <= duration * 0.06 { + Button(action: { + player.pause() + presentationMode.wrappedValue.dismiss() + onWatchNext() + }) { + HStack { + Image(systemName: "forward.fill") + .foregroundColor(Color.black) + Text("Watch Next") + .font(.headline) + .foregroundColor(Color.black) + } + .padding() + .background(Color.white.opacity(0.8)) + .cornerRadius(32) + } + .padding(.trailing, 10) + } + if showControls { Menu { Menu("Playback Speed") { Button(action: { @@ -196,8 +216,10 @@ struct CustomMediaPlayer: View { .font(.system(size: 15)) } } - .padding(.trailing, 10) - + } + .padding(.trailing, 10) + + if showControls { MusicProgressSlider( value: $currentTime, inRange: 0...duration, @@ -211,8 +233,8 @@ struct CustomMediaPlayer: View { } } ) - .frame(height: 45) - .padding(.bottom, 10) + .frame(height: 45) + .padding(.bottom, 10) } } } @@ -222,6 +244,10 @@ struct CustomMediaPlayer: View { .onDisappear { player.pause() inactivityTimer?.invalidate() + if let timeObserverToken = timeObserverToken { + player.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } } } } @@ -235,6 +261,7 @@ struct CustomMediaPlayer: View { .foregroundColor(.white) .font(.system(size: 20)) } + .frame(width: 60, height: 60) .padding() Spacer() } @@ -249,4 +276,20 @@ struct CustomMediaPlayer: View { currentTime = player.currentTime().seconds } } -} + + private func addPeriodicTimeObserver(fullURL: String) { + let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in + guard let currentItem = player.currentItem, + currentItem.duration.seconds.isFinite else { + return + } + + let currentTime = time.seconds + let duration = currentItem.duration.seconds + + UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)") + UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)") + } + } +} \ No newline at end of file diff --git a/Sora/Utils/Player/VideoPlayerView.swift b/Sora/Utils/Player/VideoPlayerView.swift index 1d4f422..b6988cd 100644 --- a/Sora/Utils/Player/VideoPlayerView.swift +++ b/Sora/Utils/Player/VideoPlayerView.swift @@ -36,13 +36,13 @@ class VideoPlayerViewController: UIViewController { let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)") if lastPlayedTime > 0 { - let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1) - self.player?.seek(to: seekTime) { _ in - self.player?.play() - } - } else { - self.player?.play() - } + let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1) + self.player?.seek(to: seekTime) { _ in + self.player?.play() + } + } else { + self.player?.play() + } } override func viewDidAppear(_ animated: Bool) { @@ -51,28 +51,28 @@ class VideoPlayerViewController: UIViewController { } override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - if let timeObserverToken = timeObserverToken { - player?.removeTimeObserver(timeObserverToken) - self.timeObserverToken = nil - } - } - - func addPeriodicTimeObserver(fullURL: String) { - guard let player = self.player else { return } - - let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in - guard let currentItem = player.currentItem, - currentItem.duration.seconds.isFinite else { - return - } - - let currentTime = time.seconds - let duration = currentItem.duration.seconds - - UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)") - UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)") - } - } + super.viewDidDisappear(animated) + if let timeObserverToken = timeObserverToken { + player?.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + func addPeriodicTimeObserver(fullURL: String) { + guard let player = self.player else { return } + + let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in + guard let currentItem = player.currentItem, + currentItem.duration.seconds.isFinite else { + return + } + + let currentTime = time.seconds + let duration = currentItem.duration.seconds + + UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)") + UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)") + } + } } diff --git a/Sora/Views/AnimeViews/AnimeInfoView.swift b/Sora/Views/AnimeViews/AnimeInfoView.swift index fd64d97..96b9a0b 100644 --- a/Sora/Views/AnimeViews/AnimeInfoView.swift +++ b/Sora/Views/AnimeViews/AnimeInfoView.swift @@ -22,6 +22,8 @@ struct AnimeInfoView: View { @State var isLoading: Bool = true @State var showFullSynopsis: Bool = false @State var animeID: Int? + @State private var selectedEpisode: String = "" + @State private var selectedEpisodeNumber: Int = 0 @AppStorage("externalPlayer") private var externalPlayer: String = "Default" @@ -138,6 +140,8 @@ struct AnimeInfoView: View { EpisodeCell(episode: episodes[index], episodeID: index, imageUrl: anime.imageUrl, progress: progress, animeID: animeID ?? 0) .onTapGesture { + selectedEpisode = episodes[index] + selectedEpisodeNumber = index + 1 fetchEpisodeStream(urlString: episodeURL) } } @@ -185,7 +189,15 @@ struct AnimeInfoView: View { return } else if externalPlayer == "Custom" { DispatchQueue.main.async { - let customMediaPlayer = CustomMediaPlayer(urlString: streamUrl) + let customMediaPlayer = CustomMediaPlayer( + urlString: streamUrl, + fullUrl: fullURL, + title: anime.name, + episodeNumber: selectedEpisodeNumber, + onWatchNext: { + selectNextEpisode() + } + ) let hostingController = UIHostingController(rootView: customMediaPlayer) hostingController.modalPresentationStyle = .fullScreen Logger.shared.log("Opening custom media player with url: \(streamUrl)") @@ -212,6 +224,17 @@ struct AnimeInfoView: View { } } + private func selectNextEpisode() { + guard let currentEpisodeIndex = episodes.firstIndex(of: selectedEpisode) else { return } + let nextEpisodeIndex = currentEpisodeIndex + 1 + if nextEpisodeIndex < episodes.count { + selectedEpisode = episodes[nextEpisodeIndex] + selectedEpisodeNumber = nextEpisodeIndex + 1 + let nextEpisodeURL = "\(module.module[0].details.baseURL)\(episodes[nextEpisodeIndex])" + fetchEpisodeStream(urlString: nextEpisodeURL) + } + } + private func openSafariViewController(with urlString: String) { guard let url = URL(string: anime.href.hasPrefix("http") ? anime.href : "\(module.module[0].details.baseURL)\(anime.href)") else { Logger.shared.log("Unable to open the webpage") diff --git a/Sora/Views/AnimeViews/EpisodeCell/CircularProgressBar.swift b/Sora/Views/AnimeViews/EpisodeCell/CircularProgressBar.swift index 7bebbc0..078410e 100644 --- a/Sora/Views/AnimeViews/EpisodeCell/CircularProgressBar.swift +++ b/Sora/Views/AnimeViews/EpisodeCell/CircularProgressBar.swift @@ -24,8 +24,13 @@ struct CircularProgressBar: View { .rotationEffect(Angle(degrees: 270.0)) .animation(.linear, value: progress) - Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0)) - .font(.system(size: 12)) + if progress >= 0.95 { + Image(systemName: "checkmark") + .font(.system(size: 12)) + } else { + Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0)) + .font(.system(size: 12)) + } } } -} +} \ No newline at end of file diff --git a/Sora/Views/AnimeViews/EpisodeCell/EpisodeCell.swift b/Sora/Views/AnimeViews/EpisodeCell/EpisodeCell.swift index c245d73..b0145b6 100644 --- a/Sora/Views/AnimeViews/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/AnimeViews/EpisodeCell/EpisodeCell.swift @@ -78,21 +78,23 @@ struct EpisodeCell: View { } do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let episodes = json["episodes"] as? [String: Any], - let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any], - let title = episodeDetails["title"] as? [String: String], - let image = episodeDetails["image"] as? String { - DispatchQueue.main.async { - self.episodeTitle = title["en"] ?? "" - self.episodeImageUrl = image - self.isLoading = false - } - } else { - print("Invalid response") - DispatchQueue.main.async { - self.isLoading = false - } + let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = jsonObject as? [String: Any], + let episodes = json["episodes"] as? [String: Any], + let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any], + let title = episodeDetails["title"] as? [String: String], + let image = episodeDetails["image"] as? String else { + print("Invalid response format") + DispatchQueue.main.async { + self.isLoading = false + } + return + } + + DispatchQueue.main.async { + self.episodeTitle = title["en"] ?? "" + self.episodeImageUrl = image + self.isLoading = false } } catch { print("Failed to parse JSON: \(error)") @@ -102,4 +104,4 @@ struct EpisodeCell: View { } }.resume() } -} \ No newline at end of file +}