diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index e3c8713..61a717d 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -61,6 +61,14 @@ class CustomMediaPlayerViewController: UIViewController { var watchNextButtonControlsConstraints: [NSLayoutConstraint] = [] var isControlsVisible = false + var subtitleBottomConstraint: NSLayoutConstraint? + + var subtitleBottomPadding: CGFloat = 10.0 { + didSet { + updateSubtitleLabelConstraints() + } + } + init(module: ScrapingModule, urlString: String, fullUrl: String, @@ -106,6 +114,10 @@ class CustomMediaPlayerViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black + + // Load persistent subtitle settings on launch + loadSubtitleSettings() + setupPlayerViewController() setupControls() setupSubtitleLabel() @@ -256,12 +268,12 @@ class CustomMediaPlayerViewController: UIViewController { playPauseButton.heightAnchor.constraint(equalToConstant: 50), backwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor), - backwardButton.trailingAnchor.constraint(equalTo: playPauseButton.leadingAnchor, constant: -30), + backwardButton.trailingAnchor.constraint(equalTo: playPauseButton.leadingAnchor, constant: -50), backwardButton.widthAnchor.constraint(equalToConstant: 40), backwardButton.heightAnchor.constraint(equalToConstant: 40), forwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor), - forwardButton.leadingAnchor.constraint(equalTo: playPauseButton.trailingAnchor, constant: 30), + forwardButton.leadingAnchor.constraint(equalTo: playPauseButton.trailingAnchor, constant: 50), forwardButton.widthAnchor.constraint(equalToConstant: 40), forwardButton.heightAnchor.constraint(equalToConstant: 40) ]) @@ -275,14 +287,25 @@ class CustomMediaPlayerViewController: UIViewController { updateSubtitleLabelAppearance() view.addSubview(subtitleLabel) subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + + subtitleBottomConstraint = subtitleLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -subtitleBottomPadding) + NSLayoutConstraint.activate([ subtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - subtitleLabel.bottomAnchor.constraint(equalTo: sliderHostingController?.view.bottomAnchor ?? view.safeAreaLayoutGuide.bottomAnchor), + subtitleBottomConstraint!, subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36), subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36) ]) } + func updateSubtitleLabelConstraints() { + subtitleBottomConstraint?.constant = -subtitleBottomPadding + view.setNeedsLayout() + UIView.animate(withDuration: 0.2) { + self.view.layoutIfNeeded() + } + } + func setupDismissButton() { dismissButton = UIButton(type: .system) dismissButton.setImage(UIImage(systemName: "xmark"), for: .normal) @@ -374,11 +397,7 @@ class CustomMediaPlayerViewController: UIViewController { func updateSubtitleLabelAppearance() { subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) subtitleLabel.textColor = subtitleUIColor() - if subtitleBackgroundEnabled { - subtitleLabel.backgroundColor = UIColor.black.withAlphaComponent(0.6) - } else { - subtitleLabel.backgroundColor = .clear - } + subtitleLabel.backgroundColor = subtitleBackgroundEnabled ? UIColor.black.withAlphaComponent(0.6) : .clear subtitleLabel.layer.cornerRadius = 5 subtitleLabel.clipsToBounds = true subtitleLabel.layer.shadowColor = UIColor.black.cgColor @@ -538,42 +557,120 @@ class CustomMediaPlayerViewController: UIViewController { if let subURL = subtitlesURL, !subURL.isEmpty { let foregroundActions = [ - UIAction(title: "White") { _ in self.subtitleForegroundColor = "white"; self.updateSubtitleLabelAppearance() }, - UIAction(title: "Yellow") { _ in self.subtitleForegroundColor = "yellow"; self.updateSubtitleLabelAppearance() }, - UIAction(title: "Green") { _ in self.subtitleForegroundColor = "green"; self.updateSubtitleLabelAppearance() }, - UIAction(title: "Blue") { _ in self.subtitleForegroundColor = "blue"; self.updateSubtitleLabelAppearance() }, - UIAction(title: "Red") { _ in self.subtitleForegroundColor = "red"; self.updateSubtitleLabelAppearance() }, - UIAction(title: "Purple") { _ in self.subtitleForegroundColor = "purple"; self.updateSubtitleLabelAppearance() } + UIAction(title: "White") { _ in + SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "white" } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "Yellow") { _ in + SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "yellow" } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "Green") { _ in + SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "green" } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "Blue") { _ in + SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "blue" } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "Red") { _ in + SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "red" } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "Purple") { _ in + SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "purple" } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + } ] let colorMenu = UIMenu(title: "Subtitle Color", children: foregroundActions) let fontSizeActions = [ - UIAction(title: "16") { _ in self.subtitleFontSize = 16; self.updateSubtitleLabelAppearance() }, - UIAction(title: "18") { _ in self.subtitleFontSize = 18; self.updateSubtitleLabelAppearance() }, - UIAction(title: "20") { _ in self.subtitleFontSize = 20; self.updateSubtitleLabelAppearance() }, - UIAction(title: "22") { _ in self.subtitleFontSize = 22; self.updateSubtitleLabelAppearance() }, - UIAction(title: "24") { _ in self.subtitleFontSize = 24; self.updateSubtitleLabelAppearance() }, + UIAction(title: "16") { _ in + SubtitleSettingsManager.shared.update { settings in settings.fontSize = 16 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "18") { _ in + SubtitleSettingsManager.shared.update { settings in settings.fontSize = 18 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "20") { _ in + SubtitleSettingsManager.shared.update { settings in settings.fontSize = 20 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "22") { _ in + SubtitleSettingsManager.shared.update { settings in settings.fontSize = 22 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "24") { _ in + SubtitleSettingsManager.shared.update { settings in settings.fontSize = 24 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, UIAction(title: "Custom") { _ in self.presentCustomFontAlert() } ] let fontSizeMenu = UIMenu(title: "Font Size", children: fontSizeActions) let shadowActions = [ - UIAction(title: "None") { _ in self.subtitleShadowRadius = 0; self.updateSubtitleLabelAppearance() }, - UIAction(title: "Low") { _ in self.subtitleShadowRadius = 1; self.updateSubtitleLabelAppearance() }, - UIAction(title: "Medium") { _ in self.subtitleShadowRadius = 3; self.updateSubtitleLabelAppearance() }, - UIAction(title: "High") { _ in self.subtitleShadowRadius = 6; self.updateSubtitleLabelAppearance() } + UIAction(title: "None") { _ in + SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 0 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "Low") { _ in + SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 1 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "Medium") { _ in + SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 3 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + }, + UIAction(title: "High") { _ in + SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 6 } + self.loadSubtitleSettings() + self.updateSubtitleLabelAppearance() + } ] let shadowMenu = UIMenu(title: "Shadow Intensity", children: shadowActions) let backgroundActions = [ UIAction(title: "Toggle") { _ in - self.subtitleBackgroundEnabled.toggle() + SubtitleSettingsManager.shared.update { settings in settings.backgroundEnabled.toggle() } + self.loadSubtitleSettings() self.updateSubtitleLabelAppearance() } ] let backgroundMenu = UIMenu(title: "Background", children: backgroundActions) - let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [colorMenu, fontSizeMenu, shadowMenu, backgroundMenu]) + let paddingActions = [ + UIAction(title: "10p") { _ in + SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 10 } + self.loadSubtitleSettings() + }, + UIAction(title: "20p") { _ in + SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 20 } + self.loadSubtitleSettings() + }, + UIAction(title: "30p") { _ in + SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 30 } + self.loadSubtitleSettings() + }, + UIAction(title: "Custom") { _ in self.presentCustomPaddingAlert() } + ] + let paddingMenu = UIMenu(title: "Bottom Padding", children: paddingActions) + + let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu]) menuElements = [subtitleOptionsMenu] } @@ -581,6 +678,26 @@ class CustomMediaPlayerViewController: UIViewController { return UIMenu(title: "", children: menuElements) } + func presentCustomPaddingAlert() { + let alert = UIAlertController(title: "Enter Custom Padding", message: nil, preferredStyle: .alert) + alert.addTextField { textField in + textField.placeholder = "Padding Value" + textField.keyboardType = .numberPad + textField.text = String(Int(self.subtitleBottomPadding)) + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in + if let text = alert.textFields?.first?.text, let intValue = Int(text) { + let newSize = CGFloat(intValue) + SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = newSize } + self.loadSubtitleSettings() + } + })) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.present(alert, animated: true, completion: nil) + } + } + func presentCustomFontAlert() { let alert = UIAlertController(title: "Enter Custom Font Size", message: nil, preferredStyle: .alert) alert.addTextField { textField in @@ -591,7 +708,8 @@ class CustomMediaPlayerViewController: UIViewController { alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in if let text = alert.textFields?.first?.text, let newSize = Double(text) { - self.subtitleFontSize = newSize + SubtitleSettingsManager.shared.update { settings in settings.fontSize = newSize } + self.loadSubtitleSettings() self.updateSubtitleLabelAppearance() } })) @@ -600,6 +718,15 @@ class CustomMediaPlayerViewController: UIViewController { } } + func loadSubtitleSettings() { + let settings = SubtitleSettingsManager.shared.settings + self.subtitleForegroundColor = settings.foregroundColor + self.subtitleFontSize = settings.fontSize + self.subtitleShadowRadius = settings.shadowRadius + self.subtitleBackgroundEnabled = settings.backgroundEnabled + self.subtitleBottomPadding = settings.bottomPadding + } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UserDefaults.standard.bool(forKey: "alwaysLandscape") { return .landscape diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/SubtitleSettingsManager.swift b/Sora/Utils/MediaPlayer/CustomPlayer/SubtitleSettingsManager.swift new file mode 100644 index 0000000..cb8f82a --- /dev/null +++ b/Sora/Utils/MediaPlayer/CustomPlayer/SubtitleSettingsManager.swift @@ -0,0 +1,43 @@ +// +// SubtitleSettingsManager.swift +// Sulfur +// +// Created by Francesco on 09/03/25. +// + +import UIKit + +struct SubtitleSettings: Codable { + var foregroundColor: String = "white" + var fontSize: Double = 20.0 + var shadowRadius: Double = 1.0 + var backgroundEnabled: Bool = true + var bottomPadding: CGFloat = 20.0 +} + +class SubtitleSettingsManager { + static let shared = SubtitleSettingsManager() + + private let userDefaultsKey = "SubtitleSettings" + + var settings: SubtitleSettings { + get { + if let data = UserDefaults.standard.data(forKey: userDefaultsKey), + let savedSettings = try? JSONDecoder().decode(SubtitleSettings.self, from: data) { + return savedSettings + } + return SubtitleSettings() + } + set { + if let data = try? JSONEncoder().encode(newValue) { + UserDefaults.standard.set(data, forKey: userDefaultsKey) + } + } + } + + func update(_ updateBlock: (inout SubtitleSettings) -> Void) { + var currentSettings = settings + updateBlock(¤tSettings) + settings = currentSettings + } +} diff --git a/Sora/Views/HomeView.swift b/Sora/Views/HomeView.swift index 7d17fff..56e51d4 100644 --- a/Sora/Views/HomeView.swift +++ b/Sora/Views/HomeView.swift @@ -13,6 +13,7 @@ struct HomeView: View { @State private var aniListItems: [AniListItem] = [] @State private var trendingItems: [AniListItem] = [] @State private var continueWatchingItems: [ContinueWatchingItem] = [] + @State private var isLoading: Bool = true private var currentDeviceSeasonAndYear: (season: String, year: Int) { let currentDate = Date() @@ -42,251 +43,73 @@ struct HomeView: View { var body: some View { NavigationView { - VStack { - ScrollView { + ScrollView { + VStack(alignment: .leading) { if !continueWatchingItems.isEmpty { - LazyVStack(alignment: .leading) { - Text("Continue Watching") - .font(.headline) - .padding(.horizontal, 8) - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(Array(continueWatchingItems.reversed())) { item in - Button(action: { - if UserDefaults.standard.string(forKey: "externalPlayer") == "Sora" { - let customMediaPlayer = CustomMediaPlayerViewController( - module: item.module, - urlString: item.streamUrl, - fullUrl: item.fullUrl, - title: item.mediaTitle, - episodeNumber: item.episodeNumber, - onWatchNext: { }, - subtitlesURL: item.subtitles, - episodeImageUrl: item.imageUrl - ) - customMediaPlayer.modalPresentationStyle = .fullScreen - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootVC = windowScene.windows.first?.rootViewController { - findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil) - } - } else { - let videoPlayerViewController = VideoPlayerViewController(module: item.module) - videoPlayerViewController.streamUrl = item.streamUrl - videoPlayerViewController.fullUrl = item.fullUrl - videoPlayerViewController.episodeImageUrl = item.imageUrl - videoPlayerViewController.episodeNumber = item.episodeNumber - videoPlayerViewController.mediaTitle = item.mediaTitle - videoPlayerViewController.subtitles = item.subtitles ?? "" - videoPlayerViewController.modalPresentationStyle = .fullScreen - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootVC = windowScene.windows.first?.rootViewController { - findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil) - } - } - }) { - VStack(alignment: .leading) { - ZStack { - KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 240, height: 135) - .shimmering() - } - .setProcessor(RoundCornerImageProcessor(cornerRadius: 10)) - .resizable() - .aspectRatio(16/9, contentMode: .fill) - .frame(width: 240, height: 135) - .cornerRadius(10) - .clipped() - .overlay( - KFImage(URL(string: item.module.metadata.iconUrl)) - .resizable() - .frame(width: 24, height: 24) - .cornerRadius(4) - .padding(4), - alignment: .topLeading - ) - } - .overlay( - ZStack { - Rectangle() - .fill(Color.black.opacity(0.3)) - .blur(radius: 3) - .frame(height: 30) - - ProgressView(value: item.progress) - .progressViewStyle(LinearProgressViewStyle(tint: .white)) - .padding(.horizontal, 8) - .scaleEffect(x: 1, y: 1.5, anchor: .center) - }, - alignment: .bottom - ) - - VStack(alignment: .leading) { - Text("Episode \(item.episodeNumber)") - .font(.caption) - .lineLimit(1) - .foregroundColor(.secondary) - - Text(item.mediaTitle) - .font(.caption) - .lineLimit(2) - .foregroundColor(.primary) - .multilineTextAlignment(.leading) - } - .padding(.horizontal, 8) - } - .frame(width: 250, height: 190) - } - .contextMenu { - Button(action: { markContinueWatchingItemAsWatched(item: item) }) { - Label("Mark as Watched", systemImage: "checkmark.circle") - } - Button(role: .destructive, action: { removeContinueWatchingItem(item: item) }) { - Label("Remove Item", systemImage: "trash") - } - } - } - } - .padding(.horizontal, 8) - } - .frame(height: 190) - } - } - VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .bottom, spacing: 5) { - Text("Seasonal") - .font(.headline) - Text("of \(currentDeviceSeasonAndYear.season) \(String(format: "%d", currentDeviceSeasonAndYear.year))") - .font(.subheadline) - .foregroundColor(.gray) - } - .padding(.horizontal, 8) - .padding(.top, 8) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - if aniListItems.isEmpty { - ForEach(0..<5, id: \.self) { _ in - HomeSkeletonCell() - } - } else { - ForEach(aniListItems, id: \.id) { item in - NavigationLink(destination: AniListDetailsView(animeID: item.id)) { - VStack { - KFImage(URL(string: item.coverImage.large)) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 130, height: 195) - .shimmering() - } - .setProcessor(RoundCornerImageProcessor(cornerRadius: 10)) - .resizable() - .scaledToFill() - .frame(width: 130, height: 195) - .cornerRadius(10) - .clipped() - - Text(item.title.romaji) - .font(.caption) - .frame(width: 130) - .lineLimit(1) - .multilineTextAlignment(.center) - .foregroundColor(.primary) - } - } - } - } - } - .padding(.horizontal, 8) - } - - HStack(alignment: .bottom, spacing: 5) { - Text("Trending") - .font(.headline) - Text("on \(trendingDateString)") - .font(.subheadline) - .foregroundColor(.gray) - } - .padding(.horizontal, 8) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - if trendingItems.isEmpty { - ForEach(0..<5, id: \.self) { _ in - HomeSkeletonCell() - } - } else { - ForEach(trendingItems, id: \.id) { item in - NavigationLink(destination: AniListDetailsView(animeID: item.id)) { - VStack { - KFImage(URL(string: item.coverImage.large)) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 130, height: 195) - .shimmering() - } - .setProcessor(RoundCornerImageProcessor(cornerRadius: 10)) - .resizable() - .scaledToFill() - .frame(width: 130, height: 195) - .cornerRadius(10) - .clipped() - - Text(item.title.romaji) - .font(.caption) - .frame(width: 130) - .lineLimit(1) - .multilineTextAlignment(.center) - .foregroundColor(.primary) - } - } - } - } - } - .padding(.horizontal, 8) - } - } - .padding(.bottom, 16) - } - .navigationTitle("Home") - } - .onAppear { - continueWatchingItems = ContinueWatchingManager.shared.fetchItems() - if tracingService == "TMDB" { - TMDBSeasonal.fetchTMDBSeasonal { items in - if let items = items { - aniListItems = items + ContinueWatchingSection(items: $continueWatchingItems) { item in + markContinueWatchingItemAsWatched(item: item) + } removeItem: { item in + removeContinueWatchingItem(item: item) } } - TMBDTrending.fetchTMDBTrending { items in - if let items = items { - trendingItems = items - } - } - } else { - AnilistServiceSeasonalAnime().fetchSeasonalAnime { items in - if let items = items { - aniListItems = items - } - } - AnilistServiceTrendingAnime().fetchTrendingAnime { items in - if let items = items { - trendingItems = items - } - } + SeasonalSection( + title: "Seasonal of \(currentDeviceSeasonAndYear.season) \(String(format: "%d", currentDeviceSeasonAndYear.year))", + items: aniListItems, + isLoading: isLoading + ) + + TrendingSection( + title: "Trending on \(trendingDateString)", + items: trendingItems, + isLoading: isLoading + ) } + .padding(.bottom, 16) + } + .navigationTitle("Home") + .onAppear { + fetchData() + } + .refreshable { + fetchData() } } .navigationViewStyle(StackNavigationViewStyle()) } + private func fetchData() { + isLoading = true + continueWatchingItems = ContinueWatchingManager.shared.fetchItems() + + let fetchSeasonal: (@escaping ([AniListItem]?) -> Void) -> Void + let fetchTrending: (@escaping ([AniListItem]?) -> Void) -> Void + + if tracingService == "TMDB" { + fetchSeasonal = TMDBSeasonal.fetchTMDBSeasonal + fetchTrending = TMBDTrending.fetchTMDBTrending + } else { + fetchSeasonal = AnilistServiceSeasonalAnime().fetchSeasonalAnime + fetchTrending = AnilistServiceTrendingAnime().fetchTrendingAnime + } + + fetchSeasonal { items in + aniListItems = items ?? [] + checkLoadingState() + } + + fetchTrending { items in + trendingItems = items ?? [] + checkLoadingState() + } + } + + private func checkLoadingState() { + if !aniListItems.isEmpty && !trendingItems.isEmpty { + isLoading = false + } + } + private func markContinueWatchingItemAsWatched(item: ContinueWatchingItem) { let key = "lastPlayedTime_\(item.fullUrl)" let totalKey = "totalTime_\(item.fullUrl)" @@ -294,16 +117,237 @@ struct HomeView: View { UserDefaults.standard.set(99999999.0, forKey: totalKey) ContinueWatchingManager.shared.remove(item: item) - if let index = continueWatchingItems.firstIndex(where: { $0.id == item.id }) { - continueWatchingItems.remove(at: index) - } + continueWatchingItems.removeAll { $0.id == item.id } } private func removeContinueWatchingItem(item: ContinueWatchingItem) { ContinueWatchingManager.shared.remove(item: item) - - if let index = continueWatchingItems.firstIndex(where: { $0.id == item.id }) { - continueWatchingItems.remove(at: index) + continueWatchingItems.removeAll { $0.id == item.id } + } +} + +struct ContinueWatchingSection: View { + @Binding var items: [ContinueWatchingItem] + var markAsWatched: (ContinueWatchingItem) -> Void + var removeItem: (ContinueWatchingItem) -> Void + + var body: some View { + LazyVStack(alignment: .leading) { + SectionHeader(title: "Continue Watching") + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(Array(items.reversed())) { item in + ContinueWatchingCell(item: item) { + markAsWatched(item) + } removeItem: { + removeItem(item) + } + } + } + .padding(.horizontal, 20) + } + .frame(height: 190) + } + } +} + +struct ContinueWatchingCell: View { + let item: ContinueWatchingItem + var markAsWatched: () -> Void + var removeItem: () -> Void + + var body: some View { + Button(action: { + if UserDefaults.standard.string(forKey: "externalPlayer") == "Sora" { + let customMediaPlayer = CustomMediaPlayerViewController( + module: item.module, + urlString: item.streamUrl, + fullUrl: item.fullUrl, + title: item.mediaTitle, + episodeNumber: item.episodeNumber, + onWatchNext: { }, + subtitlesURL: item.subtitles, + episodeImageUrl: item.imageUrl + ) + customMediaPlayer.modalPresentationStyle = .fullScreen + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil) + } + } else { + let videoPlayerViewController = VideoPlayerViewController(module: item.module) + videoPlayerViewController.streamUrl = item.streamUrl + videoPlayerViewController.fullUrl = item.fullUrl + videoPlayerViewController.episodeImageUrl = item.imageUrl + videoPlayerViewController.episodeNumber = item.episodeNumber + videoPlayerViewController.mediaTitle = item.mediaTitle + videoPlayerViewController.subtitles = item.subtitles ?? "" + videoPlayerViewController.modalPresentationStyle = .fullScreen + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil) + } + } + }) { + VStack(alignment: .leading) { + ZStack { + KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) + .placeholder { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + .frame(width: 240, height: 135) + .shimmering() + } + .setProcessor(RoundCornerImageProcessor(cornerRadius: 10)) + .resizable() + .aspectRatio(16/9, contentMode: .fill) + .frame(width: 240, height: 135) + .cornerRadius(10) + .clipped() + .overlay( + KFImage(URL(string: item.module.metadata.iconUrl)) + .resizable() + .frame(width: 24, height: 24) + .cornerRadius(4) + .padding(4), + alignment: .topLeading + ) + } + .overlay( + ZStack { + Rectangle() + .fill(Color.black.opacity(0.3)) + .blur(radius: 3) + .frame(height: 30) + + ProgressView(value: item.progress) + .progressViewStyle(LinearProgressViewStyle(tint: .white)) + .padding(.horizontal, 8) + .scaleEffect(x: 1, y: 1.5, anchor: .center) + }, + alignment: .bottom + ) + + VStack(alignment: .leading) { + Text("Episode \(item.episodeNumber)") + .font(.caption) + .lineLimit(1) + .foregroundColor(.secondary) + + Text(item.mediaTitle) + .font(.caption) + .lineLimit(2) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + } + } + .frame(width: 240, height: 170) + } + .contextMenu { + Button(action: { markAsWatched() }) { + Label("Mark as Watched", systemImage: "checkmark.circle") + } + Button(role: .destructive, action: { removeItem() }) { + Label("Remove Item", systemImage: "trash") + } + } + } +} + +struct SeasonalSection: View { + let title: String + let items: [AniListItem] + let isLoading: Bool + + var body: some View { + VStack(alignment: .leading) { + SectionHeader(title: title) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + if isLoading { + ForEach(0..<5, id: \.self) { _ in + HomeSkeletonCell() + } + } else { + ForEach(items, id: \.id) { item in + NavigationLink(destination: AniListDetailsView(animeID: item.id)) { + AnimeItemCell(item: item) + } + } + } + } + .padding(.horizontal, 20) + } + } + } +} + +struct TrendingSection: View { + let title: String + let items: [AniListItem] + let isLoading: Bool + + var body: some View { + VStack(alignment: .leading) { + SectionHeader(title: title) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + if isLoading { + ForEach(0..<5, id: \.self) { _ in + HomeSkeletonCell() + } + } else { + ForEach(items, id: \.id) { item in + NavigationLink(destination: AniListDetailsView(animeID: item.id)) { + AnimeItemCell(item: item) + } + } + } + } + .padding(.horizontal, 20) + } + } + } +} + +struct SectionHeader: View { + let title: String + + var body: some View { + Text(title) + .font(.headline) + .padding(.horizontal, 20) + .padding(.top, 8) + } +} + +struct AnimeItemCell: View { + let item: AniListItem + + var body: some View { + VStack { + KFImage(URL(string: item.coverImage.large)) + .placeholder { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + .frame(width: 130, height: 195) + .shimmering() + } + .setProcessor(RoundCornerImageProcessor(cornerRadius: 10)) + .resizable() + .scaledToFill() + .frame(width: 130, height: 195) + .cornerRadius(10) + .clipped() + + Text(item.title.romaji) + .font(.caption) + .frame(width: 130) + .lineLimit(1) + .multilineTextAlignment(.center) + .foregroundColor(.primary) } } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 1d4d498..d696058 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; }; 13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; }; 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */; }; + 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; }; 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; }; 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; }; 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; }; @@ -108,6 +109,7 @@ 13CBEFD92D5F7D1200D011EE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = ""; }; 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.swift; sourceTree = ""; }; + 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = ""; }; 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; @@ -416,6 +418,7 @@ 13EA2BD22D32D97400C1EBD7 /* Components */, 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */, 13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */, + 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */, ); path = CustomPlayer; sourceTree = ""; @@ -524,6 +527,7 @@ 133D7C932D2BE2640075467E /* Modules.swift in Sources */, 1334FF562D7872E9007E289F /* SettingsViewTrackingServices.swift in Sources */, 136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */, + 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, 1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */, 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,