From dc87e6a9d14148d00cb694836dc538983c79e179 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Sat, 15 Mar 2025 08:16:02 +0100 Subject: [PATCH 01/21] added skip 85 seconds button --- .../CustomPlayer/CustomPlayer.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 612039f..67d434f 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -52,6 +52,7 @@ class CustomMediaPlayerViewController: UIViewController { var watchNextButton: UIButton! var blackCoverView: UIView! var speedButton: UIButton! + var skip85Button: UIButton! var sliderHostingController: UIHostingController>? var sliderViewModel = SliderViewModel() @@ -125,6 +126,7 @@ class CustomMediaPlayerViewController: UIViewController { setupMenuButton() setupSpeedButton() setupWatchNextButton() + setupSkip85Button() addTimeObserver() startUpdateTimer() setupAudioSession() @@ -394,6 +396,28 @@ class CustomMediaPlayerViewController: UIViewController { NSLayoutConstraint.activate(watchNextButtonControlsConstraints) } + func setupSkip85Button() { + skip85Button = UIButton(type: .system) + skip85Button.setTitle("Skip 85s", for: .normal) + skip85Button.setImage(UIImage(systemName: "goforward"), for: .normal) + skip85Button.tintColor = .black + skip85Button.backgroundColor = .white + skip85Button.layer.cornerRadius = 25 + skip85Button.setTitleColor(.black, for: .normal) + skip85Button.alpha = 0.8 + skip85Button.addTarget(self, action: #selector(skip85Tapped), for: .touchUpInside) + + controlsContainerView.addSubview(skip85Button) + skip85Button.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + skip85Button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 85), + skip85Button.bottomAnchor.constraint(equalTo: watchNextButton.bottomAnchor), + skip85Button.heightAnchor.constraint(equalTo: watchNextButton.heightAnchor), + skip85Button.widthAnchor.constraint(equalTo: watchNextButton.widthAnchor) + ]) + } + func updateSubtitleLabelAppearance() { subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) subtitleLabel.textColor = subtitleUIColor() @@ -488,6 +512,8 @@ class CustomMediaPlayerViewController: UIViewController { UIView.animate(withDuration: 0.2) { 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) @@ -539,6 +565,11 @@ class CustomMediaPlayerViewController: UIViewController { } } + @objc func skip85Tapped() { + currentTimeVal = min(currentTimeVal + 85, duration) + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) + } + func speedChangerMenu() -> UIMenu { let speeds: [Double] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] let playbackSpeedActions = speeds.map { speed in From d09989cec26a5e1fe19ddd52f97296cfe1dfb05a Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Sat, 15 Mar 2025 14:37:23 +0100 Subject: [PATCH 02/21] added skip 85 seconds button: fix for ipads --- .../CustomPlayer/CustomPlayer.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 67d434f..b783d4d 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -410,13 +410,23 @@ class CustomMediaPlayerViewController: UIViewController { controlsContainerView.addSubview(skip85Button) skip85Button.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - skip85Button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 85), - skip85Button.bottomAnchor.constraint(equalTo: watchNextButton.bottomAnchor), - skip85Button.heightAnchor.constraint(equalTo: watchNextButton.heightAnchor), - skip85Button.widthAnchor.constraint(equalTo: watchNextButton.widthAnchor) - ]) + if UIDevice.current.userInterfaceIdiom == .pad { + NSLayoutConstraint.activate([ + skip85Button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30), + skip85Button.bottomAnchor.constraint(equalTo: watchNextButton.bottomAnchor), + skip85Button.heightAnchor.constraint(equalTo: watchNextButton.heightAnchor), + skip85Button.widthAnchor.constraint(equalTo: watchNextButton.widthAnchor) + ]) + } else { + NSLayoutConstraint.activate([ + skip85Button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), + skip85Button.bottomAnchor.constraint(equalTo: watchNextButton.bottomAnchor), + skip85Button.heightAnchor.constraint(equalTo: watchNextButton.heightAnchor), + skip85Button.widthAnchor.constraint(equalTo: watchNextButton.widthAnchor) + ]) + } } + func updateSubtitleLabelAppearance() { subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) From bbebf148373d1ee23d17b8581db461d0c66fd8d0 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Sat, 15 Mar 2025 22:02:28 +0100 Subject: [PATCH 03/21] added setting to change anime per row in landscape and portrait modes + fixed stretched images --- Sora/Utils/SkeletonCells/SkeletonCell.swift | 12 +++-- Sora/Views/LibraryView/LibraryView.swift | 40 ++++++++++++++--- Sora/Views/SearchView.swift | 44 ++++++++++++++++--- .../SettingsViewGeneral.swift | 42 ++++++++++++++++++ 4 files changed, 121 insertions(+), 17 deletions(-) diff --git a/Sora/Utils/SkeletonCells/SkeletonCell.swift b/Sora/Utils/SkeletonCells/SkeletonCell.swift index adf330a..deb6c67 100644 --- a/Sora/Utils/SkeletonCells/SkeletonCell.swift +++ b/Sora/Utils/SkeletonCells/SkeletonCell.swift @@ -8,17 +8,19 @@ import SwiftUI struct HomeSkeletonCell: View { + let cellWidth: CGFloat + var body: some View { VStack { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: 130, height: 195) + .frame(width: cellWidth, height: cellWidth * 1.5) // Maintains 2:3 aspect ratio .cornerRadius(10) .shimmering() RoundedRectangle(cornerRadius: 5) .fill(Color.gray.opacity(0.3)) - .frame(width: 130, height: 20) + .frame(width: cellWidth, height: 20) .padding(.top, 4) .shimmering() } @@ -26,15 +28,17 @@ struct HomeSkeletonCell: View { } struct SearchSkeletonCell: View { + let cellWidth: CGFloat + var body: some View { VStack(alignment: .leading, spacing: 8) { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) + .frame(width: cellWidth, height: cellWidth * 1.5) // Maintains 2:3 aspect ratio .shimmering() RoundedRectangle(cornerRadius: 5) .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 20) + .frame(width: cellWidth, height: 20) .shimmering() } } diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 7bd8130..4d2a104 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -12,7 +12,13 @@ struct LibraryView: View { @EnvironmentObject private var libraryManager: LibraryManager @EnvironmentObject private var moduleManager: ModuleManager + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + + @Environment(\.verticalSizeClass) var verticalSizeClass + @State private var continueWatchingItems: [ContinueWatchingItem] = [] + @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape private let columns = [ GridItem(.adaptive(minimum: 150), spacing: 12) @@ -21,6 +27,8 @@ struct LibraryView: View { var body: some View { NavigationView { ScrollView { + let columnsCount = determineColumns() + VStack(alignment: .leading, spacing: 12) { Text("Continue Watching") .font(.title2) @@ -67,22 +75,23 @@ struct LibraryView: View { .padding() .frame(maxWidth: .infinity) } else { - LazyVGrid(columns: columns, spacing: 12) { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { ForEach(libraryManager.bookmarks) { item in if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) { NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) { - VStack { + VStack(alignment: .leading) { ZStack { KFImage(URL(string: item.imageUrl)) .placeholder { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) + .aspectRatio(2/3, contentMode: .fit) .shimmering() } .resizable() .aspectRatio(2/3, contentMode: .fill) - .frame(width: 150, height: 225) + // Allow the image to expand to fill the available width of its grid cell. + .frame(maxWidth: .infinity) .cornerRadius(10) .clipped() .overlay( @@ -94,11 +103,10 @@ struct LibraryView: View { alignment: .topLeading ) } - Text(item.title) .font(.subheadline) .foregroundColor(.primary) - .lineLimit(2) + .lineLimit(1) .multilineTextAlignment(.leading) } } @@ -106,6 +114,12 @@ struct LibraryView: View { } } .padding(.horizontal, 20) + .onAppear { + updateOrientation() + } + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + updateOrientation() + } } } .padding(.vertical, 20) @@ -135,6 +149,20 @@ struct LibraryView: View { ContinueWatchingManager.shared.remove(item: item) continueWatchingItems.removeAll { $0.id == item.id } } + + private func updateOrientation() { + DispatchQueue.main.async { + isLandscape = UIDevice.current.orientation.isLandscape + } + } + + private func determineColumns() -> Int { + if UIDevice.current.userInterfaceIdiom == .pad { + return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait + } else { + return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait + } + } } struct ContinueWatchingSection: View { diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index 914b585..de0bb1b 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -17,14 +17,19 @@ struct SearchItem: Identifiable { struct SearchView: View { @AppStorage("selectedModuleId") private var selectedModuleId: String? + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + @StateObject private var jsController = JSController() @EnvironmentObject var moduleManager: ModuleManager + @Environment(\.verticalSizeClass) var verticalSizeClass @State private var searchItems: [SearchItem] = [] @State private var selectedSearchItem: SearchItem? @State private var isSearching = false @State private var searchText = "" @State private var hasNoResults = false + @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape private var selectedModule: ScrapingModule? { guard let id = selectedModuleId else { return nil } @@ -38,10 +43,12 @@ struct SearchView: View { "Please wait...", "Almost there..." ] - + var body: some View { NavigationView { ScrollView { + let columnsCount = determineColumns() + VStack(spacing: 0) { HStack { SearchBar(text: $searchText, onSearchButtonClicked: performSearch) @@ -79,9 +86,13 @@ struct SearchView: View { if !searchText.isEmpty { if isSearching { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 16) { - ForEach(0..<2, id: \.self) { _ in - SearchSkeletonCell() + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) // Spacing between items + let availableWidth = UIScreen.main.bounds.width - totalSpacing + let cellWidth = availableWidth / CGFloat(columnsCount) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) { + ForEach(0.. Int { + if UIDevice.current.userInterfaceIdiom == .pad { + return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait + } else { + return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait + } + } } struct SearchBar: View { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index c9bbfbd..65f7bfe 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -14,6 +14,9 @@ struct SettingsViewGeneral: View { @AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false @AppStorage("multiThreads") private var multiThreadsEnabled: Bool = false @AppStorage("metadataProviders") private var metadataProviders: String = "AniList" + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + private let metadataProvidersList = ["AniList"] @EnvironmentObject var settings: Settings @@ -76,6 +79,45 @@ struct SettingsViewGeneral: View { // .tint(.accentColor) //} + Section(header: Text("Media Grid Layout"), footer: Text("Adjust the number of media items per row in portrait and landscape modes.")) { + HStack { + Spacer() + if UIDevice.current.userInterfaceIdiom == .pad { + Picker("Portrait Columns", selection: $mediaColumnsPortrait) { + ForEach(1..<6) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } else { + Picker("Portrait Columns", selection: $mediaColumnsPortrait) { + ForEach(1..<5) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } + } + HStack { + Spacer() + if UIDevice.current.userInterfaceIdiom == .pad { + Picker("Landscape Columns", selection: $mediaColumnsLandscape) { + ForEach(2..<9) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } else { + Picker("Landscape Columns", selection: $mediaColumnsLandscape) { + ForEach(2..<6) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } + } + } + Section(header: Text("Modules"), footer: Text("Note that the modules will be replaced only if there is a different version string inside the JSON file.")) { Toggle("Refresh Modules on Launch", isOn: $refreshModulesOnLaunch) .tint(.accentColor) From 6d6bc33dfe9636b0659448919fafca976afdf6e5 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Sat, 15 Mar 2025 22:04:49 +0100 Subject: [PATCH 04/21] added setting to change anime per row in landscape and portrait modes + fixed stretched images 2 --- Sora/Views/LibraryView/LibraryView.swift | 10 +++++++--- Sora/Views/MediaInfoView/MediaInfoView.swift | 5 +++-- Sora/Views/SearchView.swift | 10 ++++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 4d2a104..396300c 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -76,6 +76,10 @@ struct LibraryView: View { .frame(maxWidth: .infinity) } else { LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) // Spacing between items + let availableWidth = UIScreen.main.bounds.width - totalSpacing + let cellWidth = availableWidth / CGFloat(columnsCount) + ForEach(libraryManager.bookmarks) { item in if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) { NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) { @@ -89,9 +93,9 @@ struct LibraryView: View { .shimmering() } .resizable() - .aspectRatio(2/3, contentMode: .fill) - // Allow the image to expand to fill the available width of its grid cell. - .frame(maxWidth: .infinity) + .aspectRatio(contentMode: .fill) + .frame(height: cellWidth * 3 / 2) + .frame(maxWidth: cellWidth) .cornerRadius(10) .clipped() .overlay( diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 8b47f57..2ad0986 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -65,9 +65,10 @@ struct MediaInfoView: View { .shimmering() } .resizable() - .aspectRatio(2/3, contentMode: .fit) - .cornerRadius(10) + .aspectRatio(contentMode: .fill) .frame(width: 150, height: 225) + .clipped() + .cornerRadius(10) VStack(alignment: .leading, spacing: 4) { Text(title) diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index de0bb1b..9680cca 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -112,15 +112,21 @@ struct SearchView: View { .frame(maxWidth: .infinity) .padding(.top) } else { + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) // Spacing between items + let availableWidth = UIScreen.main.bounds.width - totalSpacing + let cellWidth = availableWidth / CGFloat(columnsCount) + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) { ForEach(searchItems) { item in NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule!)) { VStack { KFImage(URL(string: item.imageUrl)) .resizable() - .aspectRatio(2/3, contentMode: .fit) + .aspectRatio(contentMode: .fill) + .frame(height: cellWidth * 3 / 2) + .frame(maxWidth: cellWidth) .cornerRadius(10) - .frame(maxWidth: .infinity) + .clipped() Text(item.title) .font(.subheadline) .foregroundColor(Color.primary) From a1bbc51c118c5622a24bc016bec338d3d96488cd Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Sat, 15 Mar 2025 22:35:26 +0100 Subject: [PATCH 05/21] added setting to change anime per row in landscape and portrait modes + fixed stretched images 3 --- Sora/Views/SearchView.swift | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index 9680cca..85802ba 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -43,6 +43,26 @@ struct SearchView: View { "Please wait...", "Almost there..." ] + + private var columnsCount: Int { + if UIDevice.current.userInterfaceIdiom == .pad { + let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height + return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait + } else { + return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait + } + } + + private var cellWidth: CGFloat { + let keyWindow = UIApplication.shared.connectedScenes + .compactMap { ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow }) } + .first + let safeAreaInsets = keyWindow?.safeAreaInsets ?? .zero + let safeWidth = UIScreen.main.bounds.width - safeAreaInsets.left - safeAreaInsets.right + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) + let availableWidth = safeWidth - totalSpacing + return availableWidth / CGFloat(columnsCount) + } var body: some View { NavigationView { @@ -86,10 +106,6 @@ struct SearchView: View { if !searchText.isEmpty { if isSearching { - let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) // Spacing between items - let availableWidth = UIScreen.main.bounds.width - totalSpacing - let cellWidth = availableWidth / CGFloat(columnsCount) - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) { ForEach(0.. Date: Sat, 15 Mar 2025 23:07:43 +0100 Subject: [PATCH 06/21] added setting to change anime per row in landscape and portrait modes + fixed stretched images 4 --- Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift | 1 + Sora/Utils/SkeletonCells/SkeletonCell.swift | 4 ++-- Sora/Views/LibraryView/LibraryView.swift | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index b783d4d..9db9b58 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -800,3 +800,4 @@ class CustomMediaPlayerViewController: UIViewController { // yes? Like the plural of the famous american rapper ye? -IBHRAD // low taper fade the meme is massive -cranci // cranci still doesnt have a job -seiike +// guys watch Clannad already - ibro diff --git a/Sora/Utils/SkeletonCells/SkeletonCell.swift b/Sora/Utils/SkeletonCells/SkeletonCell.swift index deb6c67..342b2bf 100644 --- a/Sora/Utils/SkeletonCells/SkeletonCell.swift +++ b/Sora/Utils/SkeletonCells/SkeletonCell.swift @@ -14,7 +14,7 @@ struct HomeSkeletonCell: View { VStack { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: cellWidth, height: cellWidth * 1.5) // Maintains 2:3 aspect ratio + .frame(width: cellWidth, height: cellWidth * 1.5) .cornerRadius(10) .shimmering() @@ -34,7 +34,7 @@ struct SearchSkeletonCell: View { VStack(alignment: .leading, spacing: 8) { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: cellWidth, height: cellWidth * 1.5) // Maintains 2:3 aspect ratio + .frame(width: cellWidth, height: cellWidth * 1.5) .shimmering() RoundedRectangle(cornerRadius: 5) .fill(Color.gray.opacity(0.3)) diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 396300c..1e5612f 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -76,7 +76,7 @@ struct LibraryView: View { .frame(maxWidth: .infinity) } else { LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { - let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) // Spacing between items + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) let availableWidth = UIScreen.main.bounds.width - totalSpacing let cellWidth = availableWidth / CGFloat(columnsCount) From 517ccb8983c874319e7cb952a2b4730612130ca1 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:55:38 +0100 Subject: [PATCH 07/21] bookmark logic fixed "Mark all previous" counting in the logger and removing a series from bookmarks when you open them from the home tab it no longer kicks you out --- Sora/Views/LibraryView/LibraryManager.swift | 8 +++++ Sora/Views/LibraryView/LibraryView.swift | 34 ++++++++++++++++++-- Sora/Views/MediaInfoView/MediaInfoView.swift | 2 +- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Sora/Views/LibraryView/LibraryManager.swift b/Sora/Views/LibraryView/LibraryManager.swift index 19712fb..b65d6ae 100644 --- a/Sora/Views/LibraryView/LibraryManager.swift +++ b/Sora/Views/LibraryView/LibraryManager.swift @@ -35,6 +35,14 @@ class LibraryManager: ObservableObject { loadBookmarks() } + func removeBookmark(item: LibraryItem) { + if let index = bookmarks.firstIndex(where: { $0.id == item.id }) { + bookmarks.remove(at: index) + Logger.shared.log("Removed series \(item.id) from bookmarks.",type: "Debug") + saveBookmarks() + } + } + private func loadBookmarks() { guard let data = UserDefaults.standard.data(forKey: bookmarksKey) else { Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Error") diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 1e5612f..a73b676 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -20,6 +20,10 @@ struct LibraryView: View { @State private var continueWatchingItems: [ContinueWatchingItem] = [] @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape + // New state variables to handle bookmark navigation explicitly + @State private var selectedBookmark: LibraryItem? = nil + @State private var isDetailActive: Bool = false + private let columns = [ GridItem(.adaptive(minimum: 150), spacing: 12) ] @@ -82,7 +86,10 @@ struct LibraryView: View { ForEach(libraryManager.bookmarks) { item in if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) { - NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) { + Button(action: { + selectedBookmark = item + isDetailActive = true + }) { VStack(alignment: .leading) { ZStack { KFImage(URL(string: item.imageUrl)) @@ -114,6 +121,13 @@ struct LibraryView: View { .multilineTextAlignment(.leading) } } + .contextMenu { + Button(role: .destructive, action: { + libraryManager.removeBookmark(item: item) + }) { + Label("Remove from Bookmarks", systemImage: "trash") + } + } } } } @@ -127,6 +141,22 @@ struct LibraryView: View { } } .padding(.vertical, 20) + NavigationLink( + destination: Group { + if let bookmark = selectedBookmark, + let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) { + MediaInfoView(title: bookmark.title, + imageUrl: bookmark.imageUrl, + href: bookmark.href, + module: module) + } else { + Text("No Data Available") + } + }, + isActive: $isDetailActive + ) { + EmptyView() + } } .navigationTitle("Library") .onAppear { @@ -179,7 +209,7 @@ struct ContinueWatchingSection: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(Array(items.reversed())) { item in - ContinueWatchingCell(item: item,markAsWatched: { + ContinueWatchingCell(item: item, markAsWatched: { markAsWatched(item) }, removeItem: { removeItem(item) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 2ad0986..a6d33d1 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -237,7 +237,7 @@ struct MediaInfoView: View { UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(href)") } refreshTrigger.toggle() - Logger.shared.log("Marked \(ep.number) episodes watched within anime \"\(title)\".", type: "General") + Logger.shared.log("Marked \(ep.number - 1) episodes watched within anime \"\(title)\".", type: "General") } ) .id(refreshTrigger) From de08c149184c1a63fe0d1479df0ac3a6132cf5ad Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:20:09 +0100 Subject: [PATCH 08/21] skip increments logic --- .../CustomPlayer/CustomPlayer.swift | 70 +++++++++++++++---- .../SettingsSubViews/SettingsViewPlayer.swift | 20 +++++- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 9db9b58..54e6401 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -207,12 +207,24 @@ class CustomMediaPlayerViewController: UIViewController { blackCoverView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) - backwardButton = UIImageView(image: UIImage(systemName: "gobackward.10")) + backwardButton = UIImageView(image: UIImage(systemName: "gobackward")) backwardButton.tintColor = .white backwardButton.contentMode = .scaleAspectFit backwardButton.isUserInteractionEnabled = true + + // 1) Tap gesture → normal skip let backwardTap = UITapGestureRecognizer(target: self, action: #selector(seekBackward)) + backwardTap.numberOfTapsRequired = 1 backwardButton.addGestureRecognizer(backwardTap) + + // 2) Long-press gesture → hold skip + let backwardLongPress = UILongPressGestureRecognizer(target: self, action: #selector(seekBackwardLongPress(_:))) + backwardLongPress.minimumPressDuration = 0.5 // Adjust as needed + backwardButton.addGestureRecognizer(backwardLongPress) + + // Make sure the tap doesn’t fire if the long-press is recognized + backwardTap.require(toFail: backwardLongPress) + controlsContainerView.addSubview(backwardButton) backwardButton.translatesAutoresizingMaskIntoConstraints = false @@ -225,14 +237,23 @@ class CustomMediaPlayerViewController: UIViewController { controlsContainerView.addSubview(playPauseButton) playPauseButton.translatesAutoresizingMaskIntoConstraints = false - forwardButton = UIImageView(image: UIImage(systemName: "goforward.10")) - forwardButton.tintColor = .white - forwardButton.contentMode = .scaleAspectFit - forwardButton.isUserInteractionEnabled = true - let forwardTap = UITapGestureRecognizer(target: self, action: #selector(seekForward)) - forwardButton.addGestureRecognizer(forwardTap) - controlsContainerView.addSubview(forwardButton) - forwardButton.translatesAutoresizingMaskIntoConstraints = false + forwardButton = UIImageView(image: UIImage(systemName: "goforward")) + forwardButton.tintColor = .white + forwardButton.contentMode = .scaleAspectFit + forwardButton.isUserInteractionEnabled = true + + let forwardTap = UITapGestureRecognizer(target: self, action: #selector(seekForward)) + forwardTap.numberOfTapsRequired = 1 + forwardButton.addGestureRecognizer(forwardTap) + + let forwardLongPress = UILongPressGestureRecognizer(target: self, action: #selector(seekForwardLongPress(_:))) + forwardLongPress.minimumPressDuration = 0.5 + forwardButton.addGestureRecognizer(forwardLongPress) + + forwardTap.require(toFail: forwardLongPress) + + controlsContainerView.addSubview(forwardButton) + forwardButton.translatesAutoresizingMaskIntoConstraints = false let sliderView = MusicProgressSlider( value: Binding(get: { self.sliderViewModel.sliderValue }, @@ -538,13 +559,36 @@ class CustomMediaPlayerViewController: UIViewController { } } - @objc func seekBackward() { - currentTimeVal = max(currentTimeVal - 10, 0) - player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) + @objc func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) { + // Only do the skip when the gesture first begins + if gesture.state == .began { + let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold") + let finalSkip = holdValue > 0 ? holdValue : 30 // fallback to 30 if not set + currentTimeVal = max(currentTimeVal - finalSkip, 0) + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) + } + } + + @objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) { + if gesture.state == .began { + let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold") + let finalSkip = holdValue > 0 ? holdValue : 30 + currentTimeVal = min(currentTimeVal + finalSkip, duration) + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) + } } + @objc func seekBackward() { + let skipValue = UserDefaults.standard.double(forKey: "skipIncrement") + let finalSkip = skipValue > 0 ? skipValue : 10 + currentTimeVal = max(currentTimeVal - finalSkip, 0) + player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) + } + @objc func seekForward() { - currentTimeVal = min(currentTimeVal + 10, duration) + let skipValue = UserDefaults.standard.double(forKey: "skipIncrement") + let finalSkip = skipValue > 0 ? skipValue : 10 + currentTimeVal = min(currentTimeVal + finalSkip, duration) player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 7eadefe..c37ecd2 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -13,12 +13,14 @@ struct SettingsViewPlayer: View { @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 + @AppStorage("skipIncrementHold") private var skipIncrementHold: Double = 30.0 private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"] var body: some View { Form { - Section(header: Text("Media Player"), footer: Text("Some features are limited to the Sora and Default player, such as ForceLandscape and holdSpeed")) { + Section(header: Text("Media Player"), footer: Text("Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.")) { HStack { Text("Media Player") Spacer() @@ -56,7 +58,21 @@ struct SettingsViewPlayer: View { } } } - + Section(header: Text("Skip Settings")) { + // Normal skip + HStack { + Text("Tap Skip:") + Spacer() + Stepper("\(Int(skipIncrement))s", value: $skipIncrement, in: 5...300, step: 5) + } + + // Long-press skip + HStack { + Text("Long press Skip:") + Spacer() + Stepper("\(Int(skipIncrementHold))s", value: $skipIncrementHold, in: 5...300, step: 5) + } + } SubtitleSettingsSection() } .navigationTitle("Player") From 91087854bb82eabd6f2375d6f1f16507686931b9 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Mon, 17 Mar 2025 20:55:34 +0100 Subject: [PATCH 09/21] here you go mario --- Sora/Views/LibraryView/LibraryView.swift | 25 +----------------------- Sulfur.xcodeproj/project.pbxproj | 8 ++++---- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index a73b676..87087c0 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -20,10 +20,6 @@ struct LibraryView: View { @State private var continueWatchingItems: [ContinueWatchingItem] = [] @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape - // New state variables to handle bookmark navigation explicitly - @State private var selectedBookmark: LibraryItem? = nil - @State private var isDetailActive: Bool = false - private let columns = [ GridItem(.adaptive(minimum: 150), spacing: 12) ] @@ -86,10 +82,7 @@ struct LibraryView: View { ForEach(libraryManager.bookmarks) { item in if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) { - Button(action: { - selectedBookmark = item - isDetailActive = true - }) { + NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) { VStack(alignment: .leading) { ZStack { KFImage(URL(string: item.imageUrl)) @@ -141,22 +134,6 @@ struct LibraryView: View { } } .padding(.vertical, 20) - NavigationLink( - destination: Group { - if let bookmark = selectedBookmark, - let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) { - MediaInfoView(title: bookmark.title, - imageUrl: bookmark.imageUrl, - href: bookmark.href, - module: module) - } else { - Text("No Data Available") - } - }, - isActive: $isDetailActive - ) { - EmptyView() - } } .navigationTitle("Library") .onAppear { diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index d5494e1..7ec2d98 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -58,9 +58,9 @@ 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 */; }; - 13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */; }; 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; + 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -115,9 +115,9 @@ 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 = ""; }; 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; - 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; + 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -437,7 +437,7 @@ isa = PBXGroup; children = ( 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */, - 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */, + 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */, ); path = Components; sourceTree = ""; @@ -532,7 +532,6 @@ 1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */, - 13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */, 1334FF542D787217007E289F /* TMDBRequest.swift in Sources */, 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */, 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */, @@ -556,6 +555,7 @@ 1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */, 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */, 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */, + 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */, 133D7C942D2BE2640075467E /* JSController.swift in Sources */, 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, From 706daafd5ab1d3a4618986d7187394c214d1e975 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Tue, 18 Mar 2025 01:30:45 +0100 Subject: [PATCH 10/21] fire ass button animations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this bs took way too much time logic later 🙏 --- .../CustomPlayer/CustomPlayer.swift | 151 +++++++++++------- 1 file changed, 93 insertions(+), 58 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 54e6401..5edc666 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -34,6 +34,7 @@ class CustomMediaPlayerViewController: UIViewController { var duration: Double = 0.0 var isVideoLoaded = false var showWatchNextButton = true + var isWatchNextVisible: Bool = false var subtitleForegroundColor: String = "white" var subtitleBackgroundEnabled: Bool = true @@ -116,17 +117,15 @@ class CustomMediaPlayerViewController: UIViewController { super.viewDidLoad() view.backgroundColor = .black - // Load persistent subtitle settings on launch loadSubtitleSettings() - setupPlayerViewController() setupControls() setupSubtitleLabel() setupDismissButton() setupMenuButton() setupSpeedButton() - setupWatchNextButton() setupSkip85Button() + setupWatchNextButton() addTimeObserver() startUpdateTimer() setupAudioSession() @@ -136,6 +135,14 @@ class CustomMediaPlayerViewController: UIViewController { if let url = subtitlesURL, !url.isEmpty { subtitlesLoader.load(from: url) } + + DispatchQueue.main.async { + self.isControlsVisible = true + NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) + NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) + self.watchNextButton.alpha = 1.0 + self.view.layoutIfNeeded() + } } override func viewWillDisappear(_ animated: Bool) { @@ -387,34 +394,36 @@ class CustomMediaPlayerViewController: UIViewController { func setupWatchNextButton() { watchNextButton = UIButton(type: .system) - watchNextButton.setTitle("Watch Next", for: .normal) + watchNextButton.setTitle("Play Next", for: .normal) watchNextButton.setImage(UIImage(systemName: "forward.fill"), 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 // Initially invisible but not hidden watchNextButton.isHidden = true - watchNextButton.alpha = 0.8 - + view.addSubview(watchNextButton) watchNextButton.translatesAutoresizingMaskIntoConstraints = false - + + // Normal position (when controls are hidden) - above progress bar watchNextButtonNormalConstraints = [ watchNextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - watchNextButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -40), + watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -10), watchNextButton.heightAnchor.constraint(equalToConstant: 50), watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) ] - + + // Controls visible position (same height as Skip 85s) watchNextButtonControlsConstraints = [ - watchNextButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor), - watchNextButton.bottomAnchor.constraint(equalTo: speedButton.bottomAnchor, constant: -5), + watchNextButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor), // Move left of speed + watchNextButton.bottomAnchor.constraint(equalTo: skip85Button.bottomAnchor), // Tie to Skip 85s button watchNextButton.heightAnchor.constraint(equalToConstant: 50), watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) ] - - NSLayoutConstraint.activate(watchNextButtonControlsConstraints) + + NSLayoutConstraint.activate(watchNextButtonNormalConstraints) // Default position } func setupSkip85Button() { @@ -427,25 +436,16 @@ class CustomMediaPlayerViewController: UIViewController { skip85Button.setTitleColor(.black, for: .normal) skip85Button.alpha = 0.8 skip85Button.addTarget(self, action: #selector(skip85Tapped), for: .touchUpInside) - - controlsContainerView.addSubview(skip85Button) + + view.addSubview(skip85Button) skip85Button.translatesAutoresizingMaskIntoConstraints = false - - if UIDevice.current.userInterfaceIdiom == .pad { - NSLayoutConstraint.activate([ - skip85Button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30), - skip85Button.bottomAnchor.constraint(equalTo: watchNextButton.bottomAnchor), - skip85Button.heightAnchor.constraint(equalTo: watchNextButton.heightAnchor), - skip85Button.widthAnchor.constraint(equalTo: watchNextButton.widthAnchor) - ]) - } else { - NSLayoutConstraint.activate([ - skip85Button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), - skip85Button.bottomAnchor.constraint(equalTo: watchNextButton.bottomAnchor), - skip85Button.heightAnchor.constraint(equalTo: watchNextButton.heightAnchor), - skip85Button.widthAnchor.constraint(equalTo: watchNextButton.widthAnchor) - ]) - } + + NSLayoutConstraint.activate([ + skip85Button.leadingAnchor.constraint(equalTo: sliderHostingController!.view.leadingAnchor), // Align with progress bar start + skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -3), + skip85Button.heightAnchor.constraint(equalToConstant: 50), + skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) + ]) } @@ -478,42 +478,75 @@ class CustomMediaPlayerViewController: UIViewController { timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in guard let self = self, let currentItem = self.player.currentItem, currentItem.duration.seconds.isFinite else { return } + + let currentDuration = currentItem.duration.seconds + if currentDuration.isNaN || currentDuration <= 0 { return } // Prevent invalid durations + self.currentTimeVal = time.seconds - self.duration = currentItem.duration.seconds - + self.duration = currentDuration + + // Ensure progress bar values remain within range if !self.isSliderEditing { - self.sliderViewModel.sliderValue = self.currentTimeVal + self.sliderViewModel.sliderValue = max(0, min(self.currentTimeVal, self.duration)) } - + UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)") UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)") - + + // Subtitle Handling if let currentCue = self.subtitlesLoader.cues.first(where: { self.currentTimeVal >= $0.startTime && self.currentTimeVal <= $0.endTime }) { self.subtitleLabel.text = currentCue.text.strippedHTML } else { self.subtitleLabel.text = "" } - - if (self.duration - self.currentTimeVal) <= (self.duration * 0.10) - && self.currentTimeVal != self.duration - && self.showWatchNextButton - && self.duration != 0 { - - if UserDefaults.standard.bool(forKey: "hideNextButton") { - DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { - self.watchNextButton.isHidden = true - } - } else { + + // --- WATCH NEXT BUTTON LOGIC --- + let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10) + && self.currentTimeVal != self.duration + && self.showWatchNextButton + && self.duration != 0 + + if isNearEnd { + if !self.isWatchNextVisible { + self.isWatchNextVisible = true self.watchNextButton.isHidden = false + + if self.isControlsVisible { + self.watchNextButton.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + self.watchNextButton.alpha = 0.0 + + UIView.animate(withDuration: 0.7, delay: 0, options: .curveEaseInOut, animations: { + NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) + NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) + self.view.layoutIfNeeded() + self.watchNextButton.alpha = 0.8 + self.watchNextButton.transform = .identity + }) + } else { + self.watchNextButton.alpha = 0.0 + UIView.animate(withDuration: 0.7, delay: 0, options: .curveEaseInOut, animations: { + NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) + NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) + self.view.layoutIfNeeded() + self.watchNextButton.alpha = 0.8 + }) + } } } else { - self.watchNextButton.isHidden = true + // Hide the button if playback goes below 90% + self.isWatchNextVisible = false + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: { + self.watchNextButton.alpha = 0.0 + }, completion: { _ in + self.watchNextButton.isHidden = true + }) } - + + // --- Update Slider in UI --- DispatchQueue.main.async { self.sliderHostingController?.rootView = MusicProgressSlider( - value: Binding(get: { self.sliderViewModel.sliderValue }, - set: { self.sliderViewModel.sliderValue = $0 }), + value: Binding(get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) }, + set: { self.sliderViewModel.sliderValue = max(0, min($0, self.duration)) }), inRange: 0...(self.duration > 0 ? self.duration : 1.0), activeFillColor: .white, fillColor: .white.opacity(0.5), @@ -522,7 +555,8 @@ class CustomMediaPlayerViewController: UIViewController { onEditingChanged: { editing in self.isSliderEditing = editing if !editing { - self.player.seek(to: CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600)) + let seekTime = CMTime(seconds: self.sliderViewModel.sliderValue, preferredTimescale: 600) + self.player.seek(to: seekTime) } } ) @@ -539,24 +573,25 @@ class CustomMediaPlayerViewController: UIViewController { @objc func toggleControls() { isControlsVisible.toggle() - - UIView.animate(withDuration: 0.2) { + + UIView.animate(withDuration: 0.2, animations: { self.controlsContainerView.alpha = self.isControlsVisible ? 1 : 0 - self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0 - + if self.isControlsVisible { + // Move Play Next beside playback controls AND align it with Skip 85s button NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) self.watchNextButton.alpha = 1.0 } else { + // Move Play Next back above the progress bar NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) self.watchNextButton.alpha = 0.8 } - + self.view.layoutIfNeeded() - } + }) } @objc func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) { From 37f5be596093f9a0a59ba3daa33faeb487b769a0 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:24:04 +0100 Subject: [PATCH 11/21] fixed cusotm player more --- .../CustomPlayer/CustomPlayer.swift | 164 +++++++++++------- Sora/Views/MediaInfoView/MediaInfoView.swift | 7 - 2 files changed, 97 insertions(+), 74 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 5edc666..6e79932 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -28,6 +28,8 @@ class CustomMediaPlayerViewController: UIViewController { var timeObserverToken: Any? var inactivityTimer: Timer? var updateTimer: Timer? + var originalRate: Float = 1.0 + var holdGesture: UILongPressGestureRecognizer? var isPlaying = true var currentTimeVal: Double = 0.0 @@ -117,6 +119,8 @@ class CustomMediaPlayerViewController: UIViewController { super.viewDidLoad() view.backgroundColor = .black + setupHoldGesture() + setInitialPlayerRate() loadSubtitleSettings() setupPlayerViewController() setupControls() @@ -135,7 +139,7 @@ class CustomMediaPlayerViewController: UIViewController { if let url = subtitlesURL, !url.isEmpty { subtitlesLoader.load(from: url) } - + DispatchQueue.main.async { self.isControlsVisible = true NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) @@ -147,6 +151,9 @@ class CustomMediaPlayerViewController: UIViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + if let playbackSpeed = player?.rate { + UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed") + } player.pause() updateTimer?.invalidate() inactivityTimer?.invalidate() @@ -218,20 +225,16 @@ class CustomMediaPlayerViewController: UIViewController { backwardButton.tintColor = .white backwardButton.contentMode = .scaleAspectFit backwardButton.isUserInteractionEnabled = true - - // 1) Tap gesture → normal skip + let backwardTap = UITapGestureRecognizer(target: self, action: #selector(seekBackward)) backwardTap.numberOfTapsRequired = 1 backwardButton.addGestureRecognizer(backwardTap) - - // 2) Long-press gesture → hold skip + let backwardLongPress = UILongPressGestureRecognizer(target: self, action: #selector(seekBackwardLongPress(_:))) - backwardLongPress.minimumPressDuration = 0.5 // Adjust as needed + backwardLongPress.minimumPressDuration = 0.5 backwardButton.addGestureRecognizer(backwardLongPress) - - // Make sure the tap doesn’t fire if the long-press is recognized backwardTap.require(toFail: backwardLongPress) - + controlsContainerView.addSubview(backwardButton) backwardButton.translatesAutoresizingMaskIntoConstraints = false @@ -245,22 +248,22 @@ class CustomMediaPlayerViewController: UIViewController { playPauseButton.translatesAutoresizingMaskIntoConstraints = false forwardButton = UIImageView(image: UIImage(systemName: "goforward")) - forwardButton.tintColor = .white - forwardButton.contentMode = .scaleAspectFit - forwardButton.isUserInteractionEnabled = true - - let forwardTap = UITapGestureRecognizer(target: self, action: #selector(seekForward)) - forwardTap.numberOfTapsRequired = 1 - forwardButton.addGestureRecognizer(forwardTap) - - let forwardLongPress = UILongPressGestureRecognizer(target: self, action: #selector(seekForwardLongPress(_:))) - forwardLongPress.minimumPressDuration = 0.5 - forwardButton.addGestureRecognizer(forwardLongPress) - - forwardTap.require(toFail: forwardLongPress) - - controlsContainerView.addSubview(forwardButton) - forwardButton.translatesAutoresizingMaskIntoConstraints = false + forwardButton.tintColor = .white + forwardButton.contentMode = .scaleAspectFit + forwardButton.isUserInteractionEnabled = true + + let forwardTap = UITapGestureRecognizer(target: self, action: #selector(seekForward)) + forwardTap.numberOfTapsRequired = 1 + forwardButton.addGestureRecognizer(forwardTap) + + let forwardLongPress = UILongPressGestureRecognizer(target: self, action: #selector(seekForwardLongPress(_:))) + forwardLongPress.minimumPressDuration = 0.5 + forwardButton.addGestureRecognizer(forwardLongPress) + + forwardTap.require(toFail: forwardLongPress) + + controlsContainerView.addSubview(forwardButton) + forwardButton.translatesAutoresizingMaskIntoConstraints = false let sliderView = MusicProgressSlider( value: Binding(get: { self.sliderViewModel.sliderValue }, @@ -285,8 +288,8 @@ class CustomMediaPlayerViewController: UIViewController { controlsContainerView.addSubview(sliderHostView) NSLayoutConstraint.activate([ - sliderHostView.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 26), - sliderHostView.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -26), + sliderHostView.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 18), + sliderHostView.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -18), sliderHostView.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -20), sliderHostView.heightAnchor.constraint(equalToConstant: 30) ]) @@ -401,29 +404,27 @@ class CustomMediaPlayerViewController: UIViewController { watchNextButton.layer.cornerRadius = 25 watchNextButton.setTitleColor(.black, for: .normal) watchNextButton.addTarget(self, action: #selector(watchNextTapped), for: .touchUpInside) - watchNextButton.alpha = 0.0 // Initially invisible but not hidden + watchNextButton.alpha = 0.0 watchNextButton.isHidden = true - + view.addSubview(watchNextButton) watchNextButton.translatesAutoresizingMaskIntoConstraints = false - - // Normal position (when controls are hidden) - above progress bar + watchNextButtonNormalConstraints = [ watchNextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -10), + watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.centerYAnchor), watchNextButton.heightAnchor.constraint(equalToConstant: 50), watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) ] - - // Controls visible position (same height as Skip 85s) + watchNextButtonControlsConstraints = [ - watchNextButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor), // Move left of speed - watchNextButton.bottomAnchor.constraint(equalTo: skip85Button.bottomAnchor), // Tie to Skip 85s button + watchNextButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor), + watchNextButton.bottomAnchor.constraint(equalTo: skip85Button.bottomAnchor), watchNextButton.heightAnchor.constraint(equalToConstant: 50), watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) ] - - NSLayoutConstraint.activate(watchNextButtonNormalConstraints) // Default position + + NSLayoutConstraint.activate(watchNextButtonNormalConstraints) } func setupSkip85Button() { @@ -441,13 +442,13 @@ class CustomMediaPlayerViewController: UIViewController { skip85Button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - skip85Button.leadingAnchor.constraint(equalTo: sliderHostingController!.view.leadingAnchor), // Align with progress bar start + skip85Button.leadingAnchor.constraint(equalTo: sliderHostingController!.view.leadingAnchor), skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -3), skip85Button.heightAnchor.constraint(equalToConstant: 50), skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) ]) } - + func updateSubtitleLabelAppearance() { subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) @@ -478,39 +479,36 @@ class CustomMediaPlayerViewController: UIViewController { timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in guard let self = self, let currentItem = self.player.currentItem, currentItem.duration.seconds.isFinite else { return } - + let currentDuration = currentItem.duration.seconds - if currentDuration.isNaN || currentDuration <= 0 { return } // Prevent invalid durations - + if currentDuration.isNaN || currentDuration <= 0 { return } + self.currentTimeVal = time.seconds self.duration = currentDuration - - // Ensure progress bar values remain within range + if !self.isSliderEditing { self.sliderViewModel.sliderValue = max(0, min(self.currentTimeVal, self.duration)) } - + UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)") UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)") - - // Subtitle Handling + if let currentCue = self.subtitlesLoader.cues.first(where: { self.currentTimeVal >= $0.startTime && self.currentTimeVal <= $0.endTime }) { self.subtitleLabel.text = currentCue.text.strippedHTML } else { self.subtitleLabel.text = "" } - - // --- WATCH NEXT BUTTON LOGIC --- + let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10) - && self.currentTimeVal != self.duration - && self.showWatchNextButton - && self.duration != 0 - + && self.currentTimeVal != self.duration + && self.showWatchNextButton + && self.duration != 0 + if isNearEnd { if !self.isWatchNextVisible { self.isWatchNextVisible = true self.watchNextButton.isHidden = false - + if self.isControlsVisible { self.watchNextButton.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) self.watchNextButton.alpha = 0.0 @@ -533,7 +531,6 @@ class CustomMediaPlayerViewController: UIViewController { } } } else { - // Hide the button if playback goes below 90% self.isWatchNextVisible = false UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: { self.watchNextButton.alpha = 0.0 @@ -541,8 +538,7 @@ class CustomMediaPlayerViewController: UIViewController { self.watchNextButton.isHidden = true }) } - - // --- Update Slider in UI --- + DispatchQueue.main.async { self.sliderHostingController?.rootView = MusicProgressSlider( value: Binding(get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) }, @@ -573,37 +569,34 @@ class CustomMediaPlayerViewController: UIViewController { @objc func toggleControls() { isControlsVisible.toggle() - + UIView.animate(withDuration: 0.2, animations: { self.controlsContainerView.alpha = self.isControlsVisible ? 1 : 0 self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0 - + if self.isControlsVisible { - // Move Play Next beside playback controls AND align it with Skip 85s button NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) self.watchNextButton.alpha = 1.0 } else { - // Move Play Next back above the progress bar NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) self.watchNextButton.alpha = 0.8 } - + self.view.layoutIfNeeded() }) } @objc func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) { - // Only do the skip when the gesture first begins if gesture.state == .began { let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold") - let finalSkip = holdValue > 0 ? holdValue : 30 // fallback to 30 if not set + let finalSkip = holdValue > 0 ? holdValue : 30 currentTimeVal = max(currentTimeVal - finalSkip, 0) player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) } } - + @objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) { if gesture.state == .began { let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold") @@ -619,7 +612,7 @@ class CustomMediaPlayerViewController: UIViewController { currentTimeVal = max(currentTimeVal - finalSkip, 0) player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) } - + @objc func seekForward() { let skipValue = UserDefaults.standard.double(forKey: "skipIncrement") let finalSkip = skipValue > 0 ? skipValue : 10 @@ -874,6 +867,43 @@ class CustomMediaPlayerViewController: UIViewController { Logger.shared.log("Failed to set up AVAudioSession: \(error)") } } + + private func setupHoldGesture() { + holdGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldGesture(_:))) + holdGesture?.minimumPressDuration = 0.5 + if let holdGesture = holdGesture { + view.addGestureRecognizer(holdGesture) + } + } + + @objc private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) { + switch gesture.state { + case .began: + beginHoldSpeed() + case .ended, .cancelled: + endHoldSpeed() + default: + break + } + } + + private func beginHoldSpeed() { + guard let player = player else { return } + originalRate = player.rate + let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer") + player.rate = holdSpeed > 0 ? holdSpeed : 2.0 + } + + private func endHoldSpeed() { + player?.rate = originalRate + } + + private func setInitialPlayerRate() { + if UserDefaults.standard.bool(forKey: "rememberPlaySpeed") { + let lastPlayedSpeed = UserDefaults.standard.float(forKey: "lastPlaybackSpeed") + player?.rate = lastPlayedSpeed > 0 ? lastPlayedSpeed : 1.0 + } + } } // yes? Like the plural of the famous american rapper ye? -IBHRAD diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index a6d33d1..98a19af 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -512,13 +512,6 @@ struct MediaInfoView: View { func playStream(url: String, fullURL: String, subtitles: String? = nil) { DispatchQueue.main.async { - guard let streamURL = URL(string: url) else { - Logger.shared.log("Invalid stream URL: \(url)", type: "Error") - handleStreamFailure() - return - } - let subtitleFileURL = subtitles != nil ? URL(string: subtitles!) : nil - let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora" var scheme: String? From bb65945ac1a6076de512a2523ca004fd5682aba9 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:28:01 +0100 Subject: [PATCH 12/21] update progresses instaly --- Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index b284a47..282fadd 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -80,11 +80,15 @@ struct EpisodeCell: View { } } .onAppear { + updateProgress() + if UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil || UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata") { fetchEpisodeDetails() } - currentProgress = progress + } + .onChange(of: progress) { newProgress in + updateProgress() } .onTapGesture { onTap(episodeImageUrl) From 53e1b08956588df71b9131508ab14c3bc1dbf431 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:29:55 +0100 Subject: [PATCH 13/21] forgot about continue watching should work? --- Sora/Views/LibraryView/LibraryView.swift | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 87087c0..877f38b 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -205,6 +205,8 @@ struct ContinueWatchingCell: View { var markAsWatched: () -> Void var removeItem: () -> Void + @State private var currentProgress: Double = 0.0 + var body: some View { Button(action: { if UserDefaults.standard.string(forKey: "externalPlayer") == "Default" { @@ -271,7 +273,7 @@ struct ContinueWatchingCell: View { .blur(radius: 3) .frame(height: 30) - ProgressView(value: item.progress) + ProgressView(value: currentProgress) .progressViewStyle(LinearProgressViewStyle(tint: .white)) .padding(.horizontal, 8) .scaleEffect(x: 1, y: 1.5, anchor: .center) @@ -302,5 +304,22 @@ struct ContinueWatchingCell: View { Label("Remove Item", systemImage: "trash") } } + .onAppear { + updateProgress() + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + updateProgress() + } + } + + private func updateProgress() { + let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(item.fullUrl)") + let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(item.fullUrl)") + + if totalTime > 0 { + currentProgress = lastPlayedTime / totalTime + } else { + currentProgress = item.progress + } } } From 9b8a389def80a4fb9555982a727df27395704a77 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:47:57 +0100 Subject: [PATCH 14/21] added srt support maybe --- Sora/Utils/Extensions/String.swift | 4 + .../SubtitleSettingsManager.swift | 0 .../Helpers/VTTSubtitlesLoader.swift | 168 ++++++++++++++++++ .../CustomPlayer/VTTSubtitlesLoader.swift | 87 --------- Sora/Views/LibraryView/LibraryManager.swift | 2 +- Sulfur.xcodeproj/project.pbxproj | 12 +- 6 files changed, 183 insertions(+), 90 deletions(-) rename Sora/Utils/MediaPlayer/CustomPlayer/{ => Helpers}/SubtitleSettingsManager.swift (100%) create mode 100644 Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift delete mode 100644 Sora/Utils/MediaPlayer/CustomPlayer/VTTSubtitlesLoader.swift diff --git a/Sora/Utils/Extensions/String.swift b/Sora/Utils/Extensions/String.swift index 660329a..10b3b5a 100644 --- a/Sora/Utils/Extensions/String.swift +++ b/Sora/Utils/Extensions/String.swift @@ -17,4 +17,8 @@ extension String { let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) return attributedString?.string ?? self } + + var trimmed: String { + return self.trimmingCharacters(in: .whitespacesAndNewlines) + } } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/SubtitleSettingsManager.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift similarity index 100% rename from Sora/Utils/MediaPlayer/CustomPlayer/SubtitleSettingsManager.swift rename to Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift new file mode 100644 index 0000000..37b98d4 --- /dev/null +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift @@ -0,0 +1,168 @@ +// +// VTTSubtitlesLoader.swift +// Sora +// +// Created by Francesco on 15/02/25. +// + +import Combine +import Foundation + +struct SubtitleCue: Identifiable { + let id = UUID() + let startTime: Double + let endTime: Double + let text: String +} + +class VTTSubtitlesLoader: ObservableObject { + @Published var cues: [SubtitleCue] = [] + + enum SubtitleFormat { + case vtt + case srt + case unknown + } + + func load(from urlString: String) { + guard let url = URL(string: urlString) else { return } + + let format = determineSubtitleFormat(from: url) + + URLSession.custom.dataTask(with: url) { data, _, error in + guard let data = data, + let content = String(data: data, encoding: .utf8), + error == nil else { return } + + DispatchQueue.main.async { + switch format { + case .vtt: + self.cues = self.parseVTT(content: content) + case .srt: + self.cues = self.parseSRT(content: content) + case .unknown: + if content.trimmed.hasPrefix("WEBVTT") { + self.cues = self.parseVTT(content: content) + } else { + self.cues = self.parseSRT(content: content) + } + } + } + }.resume() + } + + private func determineSubtitleFormat(from url: URL) -> SubtitleFormat { + let fileExtension = url.pathExtension.lowercased() + switch fileExtension { + case "vtt", "webvtt": + return .vtt + case "srt": + return .srt + default: + return .unknown + } + } + + private func parseVTT(content: String) -> [SubtitleCue] { + var cues: [SubtitleCue] = [] + let lines = content.components(separatedBy: .newlines) + var index = 0 + + while index < lines.count { + let line = lines[index].trimmingCharacters(in: .whitespaces) + if line.isEmpty || line == "WEBVTT" { + index += 1 + continue + } + + if !line.contains("-->") { + index += 1 + if index >= lines.count { break } + } + + let timeLine = lines[index] + let times = timeLine.components(separatedBy: "-->") + if times.count < 2 { + index += 1 + continue + } + + let startTime = parseTimecode(times[0].trimmingCharacters(in: .whitespaces)) + let adjustedStartTime = max(startTime - 0.5, 0) + let endTime = parseTimecode(times[1].trimmingCharacters(in: .whitespaces)) + let adjusteEndTime = max(endTime - 0.5, 0) + index += 1 + var cueText = "" + while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).isEmpty { + cueText += lines[index] + "\n" + index += 1 + } + cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjusteEndTime, text: cueText.trimmingCharacters(in: .whitespacesAndNewlines))) + } + return cues + } + + private func parseSRT(content: String) -> [SubtitleCue] { + var cues: [SubtitleCue] = [] + let normalizedContent = content.replacingOccurrences(of: "\r\n", with: "\n") + .replacingOccurrences(of: "\r", with: "\n") + let blocks = normalizedContent.components(separatedBy: "\n\n") + + for block in blocks { + let lines = block.components(separatedBy: "\n").filter { !$0.isEmpty } + guard lines.count >= 2 else { continue } + + let timeLine = lines[1] + let times = timeLine.components(separatedBy: "-->") + + guard times.count >= 2 else { continue } + + let startTime = parseSRTTimecode(times[0].trimmingCharacters(in: .whitespaces)) + let adjustedStartTime = max(startTime - 0.5, 0) + let endTime = parseSRTTimecode(times[1].trimmingCharacters(in: .whitespaces)) + let adjustedEndTime = max(endTime - 0.5, 0) + + var textLines = [String]() + if lines.count > 2 { + textLines = Array(lines[2...]) + } + let text = textLines.joined(separator: "\n") + + cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjustedEndTime, text: text)) + } + + return cues + } + + private func parseTimecode(_ timeString: String) -> Double { + let parts = timeString.components(separatedBy: ":") + var seconds = 0.0 + if parts.count == 3, + let h = Double(parts[0]), + let m = Double(parts[1]), + let s = Double(parts[2].replacingOccurrences(of: ",", with: ".")) { + seconds = h * 3600 + m * 60 + s + } else if parts.count == 2, + let m = Double(parts[0]), + let s = Double(parts[1].replacingOccurrences(of: ",", with: ".")) { + seconds = m * 60 + s + } + return seconds + } + + private func parseSRTTimecode(_ timeString: String) -> Double { + let parts = timeString.components(separatedBy: ":") + guard parts.count == 3 else { return 0 } + + let secondsParts = parts[2].components(separatedBy: ",") + guard secondsParts.count == 2, + let hours = Double(parts[0]), + let minutes = Double(parts[1]), + let seconds = Double(secondsParts[0]), + let milliseconds = Double(secondsParts[1]) else { + return 0 + } + + return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000 + } +} diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/VTTSubtitlesLoader.swift b/Sora/Utils/MediaPlayer/CustomPlayer/VTTSubtitlesLoader.swift deleted file mode 100644 index c58214a..0000000 --- a/Sora/Utils/MediaPlayer/CustomPlayer/VTTSubtitlesLoader.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// VTTSubtitlesLoader.swift -// Sora -// -// Created by Francesco on 15/02/25. -// - -import Combine -import Foundation - -struct SubtitleCue: Identifiable { - let id = UUID() - let startTime: Double - let endTime: Double - let text: String -} - -class VTTSubtitlesLoader: ObservableObject { - @Published var cues: [SubtitleCue] = [] - - func load(from urlString: String) { - guard let url = URL(string: urlString) else { return } - URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, - let vttContent = String(data: data, encoding: .utf8), - error == nil else { return } - DispatchQueue.main.async { - self.cues = self.parseVTT(content: vttContent) - } - }.resume() - } - - private func parseVTT(content: String) -> [SubtitleCue] { - var cues: [SubtitleCue] = [] - let lines = content.components(separatedBy: .newlines) - var index = 0 - - while index < lines.count { - let line = lines[index].trimmingCharacters(in: .whitespaces) - if line.isEmpty || line == "WEBVTT" { - index += 1 - continue - } - - if !line.contains("-->") { - index += 1 - if index >= lines.count { break } - } - - let timeLine = lines[index] - let times = timeLine.components(separatedBy: "-->") - if times.count < 2 { - index += 1 - continue - } - - let startTime = parseTimecode(times[0].trimmingCharacters(in: .whitespaces)) - let adjustedStartTime = max(startTime - 0.5, 0) - let endTime = parseTimecode(times[1].trimmingCharacters(in: .whitespaces)) - let adjusteEndTime = max(endTime - 0.5, 0) - index += 1 - var cueText = "" - while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).isEmpty { - cueText += lines[index] + "\n" - index += 1 - } - cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjusteEndTime, text: cueText.trimmingCharacters(in: .whitespacesAndNewlines))) - } - return cues - } - - private func parseTimecode(_ timeString: String) -> Double { - let parts = timeString.components(separatedBy: ":") - var seconds = 0.0 - if parts.count == 3, - let h = Double(parts[0]), - let m = Double(parts[1]), - let s = Double(parts[2].replacingOccurrences(of: ",", with: ".")) { - seconds = h * 3600 + m * 60 + s - } else if parts.count == 2, - let m = Double(parts[0]), - let s = Double(parts[1].replacingOccurrences(of: ",", with: ".")) { - seconds = m * 60 + s - } - return seconds - } -} diff --git a/Sora/Views/LibraryView/LibraryManager.swift b/Sora/Views/LibraryView/LibraryManager.swift index b65d6ae..285524e 100644 --- a/Sora/Views/LibraryView/LibraryManager.swift +++ b/Sora/Views/LibraryView/LibraryManager.swift @@ -45,7 +45,7 @@ class LibraryManager: ObservableObject { private func loadBookmarks() { guard let data = UserDefaults.standard.data(forKey: bookmarksKey) else { - Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Error") + Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Debug") return } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 7ec2d98..738eb75 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -361,6 +361,15 @@ path = DetailsView; sourceTree = ""; }; + 1384DCDF2D89BE870094797A /* Helpers */ = { + isa = PBXGroup; + children = ( + 13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */, + 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = { isa = PBXGroup; children = ( @@ -425,10 +434,9 @@ 13EA2BD02D32D97400C1EBD7 /* CustomPlayer */ = { isa = PBXGroup; children = ( + 1384DCDF2D89BE870094797A /* Helpers */, 13EA2BD22D32D97400C1EBD7 /* Components */, 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */, - 13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */, - 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */, ); path = CustomPlayer; sourceTree = ""; From ff57939a3952ac6134e292474f9344a49b22c3fd Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:15:48 +0100 Subject: [PATCH 15/21] =?UTF-8?q?quality=20picker=20and=20more=20improveme?= =?UTF-8?q?nts=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomPlayer/CustomPlayer.swift | 239 +++++++++++++++++- 1 file changed, 232 insertions(+), 7 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 6e79932..4966b1c 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -56,6 +56,12 @@ class CustomMediaPlayerViewController: UIViewController { var blackCoverView: UIView! var speedButton: UIButton! var skip85Button: UIButton! + var qualityButton: UIButton! + + var isHLSStream: Bool = false + var qualities: [(String, String)] = [] + var currentQualityURL: URL? + var baseM3U8URL: URL? var sliderHostingController: UIHostingController>? var sliderViewModel = SliderViewModel() @@ -128,12 +134,17 @@ class CustomMediaPlayerViewController: UIViewController { setupDismissButton() setupMenuButton() setupSpeedButton() + setupQualityButton() setupSkip85Button() setupWatchNextButton() addTimeObserver() startUpdateTimer() setupAudioSession() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.checkForHLSStream() + } + player.play() if let url = subtitlesURL, !url.isEmpty { @@ -149,8 +160,16 @@ class CustomMediaPlayerViewController: UIViewController { } } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidChange), name: .AVPlayerItemNewAccessLogEntry, object: nil) + } + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemNewAccessLogEntry, object: nil) + if let playbackSpeed = player?.rate { UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed") } @@ -179,6 +198,15 @@ class CustomMediaPlayerViewController: UIViewController { } } + @objc private func playerItemDidChange() { + DispatchQueue.main.async { [weak self] in + if let self = self, self.qualityButton.isHidden && self.isHLSStream { + self.qualityButton.isHidden = false + self.qualityButton.menu = self.qualitySelectionMenu() + } + } + } + func setupPlayerViewController() { playerViewController = AVPlayerViewController() playerViewController.player = player @@ -387,12 +415,24 @@ class CustomMediaPlayerViewController: UIViewController { controlsContainerView.addSubview(speedButton) speedButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - speedButton.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -50), - speedButton.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor), - speedButton.widthAnchor.constraint(equalToConstant: 40), - speedButton.heightAnchor.constraint(equalToConstant: 40) - ]) + + guard let sliderView = sliderHostingController?.view else { return } + + if menuButton.isHidden { + NSLayoutConstraint.activate([ + speedButton.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -50), + speedButton.trailingAnchor.constraint(equalTo: sliderView.trailingAnchor), + speedButton.widthAnchor.constraint(equalToConstant: 40), + speedButton.heightAnchor.constraint(equalToConstant: 40) + ]) + } else { + NSLayoutConstraint.activate([ + speedButton.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -50), + speedButton.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor), + speedButton.widthAnchor.constraint(equalToConstant: 40), + speedButton.heightAnchor.constraint(equalToConstant: 40) + ]) + } } func setupWatchNextButton() { @@ -418,7 +458,7 @@ class CustomMediaPlayerViewController: UIViewController { ] watchNextButtonControlsConstraints = [ - watchNextButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor), + watchNextButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor), watchNextButton.bottomAnchor.constraint(equalTo: skip85Button.bottomAnchor), watchNextButton.heightAnchor.constraint(equalToConstant: 50), watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) @@ -449,6 +489,24 @@ class CustomMediaPlayerViewController: UIViewController { ]) } + private func setupQualityButton() { + qualityButton = UIButton(type: .system) + qualityButton.setImage(UIImage(systemName: "4k.tv"), for: .normal) + qualityButton.tintColor = .white + qualityButton.showsMenuAsPrimaryAction = true + qualityButton.menu = qualitySelectionMenu() + qualityButton.isHidden = true + + controlsContainerView.addSubview(qualityButton) + qualityButton.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + qualityButton.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -50), + qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor), + qualityButton.widthAnchor.constraint(equalToConstant: 40), + qualityButton.heightAnchor.constraint(equalToConstant: 40) + ]) + } func updateSubtitleLabelAppearance() { subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) @@ -665,6 +723,173 @@ class CustomMediaPlayerViewController: UIViewController { return UIMenu(title: "Playback Speed", children: playbackSpeedActions) } + private func parseM3U8(url: URL, completion: @escaping () -> Void) { + var request = URLRequest(url: url) + request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") + request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") + + URLSession.shared.dataTask(with: request) { [weak self] data, response, error in + guard let self = self, let data = data, let content = String(data: data, encoding: .utf8) else { + print("Failed to load m3u8 file") + DispatchQueue.main.async { + self?.qualities = [] + completion() + } + return + } + + let lines = content.components(separatedBy: .newlines) + var qualities: [(String, String)] = [] + + qualities.append(("Auto (Recommended)", url.absoluteString)) + + func getQualityName(for height: Int) -> String { + switch height { + case 1080...: return "\(height)p (FHD)" + case 720..<1080: return "\(height)p (HD)" + case 480..<720: return "\(height)p (SD)" + default: return "\(height)p" + } + } + + for (index, line) in lines.enumerated() { + if line.contains("#EXT-X-STREAM-INF"), index + 1 < lines.count { + if let resolutionRange = line.range(of: "RESOLUTION="), + let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",") ?? line[resolutionRange.upperBound...].range(of: "\n") { + + let resolutionPart = String(line[resolutionRange.upperBound.. secondHeight + } + + if let auto = autoQuality { + sortedQualities.insert(auto, at: 0) + } + + self.qualities = sortedQualities + completion() + } + }.resume() + } + + private func switchToQuality(urlString: String) { + guard let url = URL(string: urlString), currentQualityURL?.absoluteString != urlString else { return } + + let currentTime = player.currentTime() + let wasPlaying = player.rate > 0 + + var request = URLRequest(url: url) + request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") + request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") + + let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) + let playerItem = AVPlayerItem(asset: asset) + + player.replaceCurrentItem(with: playerItem) + + player.seek(to: currentTime) + if wasPlaying { + player.play() + } + + currentQualityURL = url + + UserDefaults.standard.set(urlString, forKey: "lastSelectedQuality") + qualityButton.menu = qualitySelectionMenu() + + if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 { + DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", subtitle: "", duration: 0.5, icon: UIImage(systemName: "eye")) + } + } + + private func qualitySelectionMenu() -> UIMenu { + var menuItems: [UIMenuElement] = [] + + if isHLSStream { + if qualities.isEmpty { + let loadingAction = UIAction(title: "Loading qualities...", attributes: .disabled) { _ in } + menuItems.append(loadingAction) + } else { + var menuTitle = "Video Quality" + if let currentURL = currentQualityURL?.absoluteString, + let selectedQuality = qualities.first(where: { $0.1 == currentURL })?.0 { + menuTitle = "Quality: \(selectedQuality)" + } + + for (name, urlString) in qualities { + let isCurrentQuality = currentQualityURL?.absoluteString == urlString + + let action = UIAction( + title: name, + state: isCurrentQuality ? .on : .off, + handler: { [weak self] _ in + self?.switchToQuality(urlString: urlString) + } + ) + menuItems.append(action) + } + + return UIMenu(title: menuTitle, children: menuItems) + } + } else { + let unavailableAction = UIAction(title: "Quality selection unavailable", attributes: .disabled) { _ in } + menuItems.append(unavailableAction) + } + + return UIMenu(title: "Video Quality", children: menuItems) + } + + private func checkForHLSStream() { + guard let url = URL(string: streamURL) else { return } + + if url.absoluteString.contains(".m3u8") { + isHLSStream = true + baseM3U8URL = url + currentQualityURL = url + + parseM3U8(url: url) { [weak self] in + guard let self = self else { return } + + if let lastSelectedQuality = UserDefaults.standard.string(forKey: "lastSelectedQuality"), + self.qualities.contains(where: { $0.1 == lastSelectedQuality }) { + self.switchToQuality(urlString: lastSelectedQuality) + } + + self.qualityButton.isHidden = false + self.qualityButton.menu = self.qualitySelectionMenu() + } + } else { + isHLSStream = false + qualityButton.isHidden = true + } + } + func buildOptionsMenu() -> UIMenu { var menuElements: [UIMenuElement] = [] From 9621fba368e97d8f47a66674b2cdc35204cb8f30 Mon Sep 17 00:00:00 2001 From: cranci <100066266+cranci1@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:24:18 +0100 Subject: [PATCH 16/21] Update SettingsViewGeneral.swift --- .../SettingsView/SettingsSubViews/SettingsViewGeneral.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index 65f7bfe..ed0f83e 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -81,7 +81,6 @@ struct SettingsViewGeneral: View { Section(header: Text("Media Grid Layout"), footer: Text("Adjust the number of media items per row in portrait and landscape modes.")) { HStack { - Spacer() if UIDevice.current.userInterfaceIdiom == .pad { Picker("Portrait Columns", selection: $mediaColumnsPortrait) { ForEach(1..<6) { i in @@ -99,7 +98,6 @@ struct SettingsViewGeneral: View { } } HStack { - Spacer() if UIDevice.current.userInterfaceIdiom == .pad { Picker("Landscape Columns", selection: $mediaColumnsLandscape) { ForEach(2..<9) { i in From c54d4d799af3b5ff5527563fe2494be0294874a0 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Tue, 18 Mar 2025 17:22:58 +0100 Subject: [PATCH 17/21] Implemented season seperator for tv shows --- Sora/Views/MediaInfoView/MediaInfoView.swift | 167 +++++++++++++++---- 1 file changed, 130 insertions(+), 37 deletions(-) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 98a19af..5ce7cb2 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -38,6 +38,7 @@ struct MediaInfoView: View { @State private var selectedEpisodeNumber: Int = 0 @State private var selectedEpisodeImage: String = "" + @State private var selectedSeason: Int = 0 @AppStorage("externalPlayer") private var externalPlayer: String = "Default" @AppStorage("episodeChunkSize") private var episodeChunkSize: Int = 100 @@ -48,6 +49,17 @@ struct MediaInfoView: View { @State private var selectedRange: Range = 0..<100 + private var isGroupedBySeasons: Bool { + var lastEpisodeNumber = 0 + for episode in episodeLinks { + if episode.number == 1 && lastEpisodeNumber > 1 { + return true + } + lastEpisodeNumber = episode.number + } + return false + } + var body: some View { Group { if isLoading { @@ -190,7 +202,7 @@ struct MediaInfoView: View { Spacer() - if episodeLinks.count > episodeChunkSize { + if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { Menu { ForEach(generateRanges(), id: \.self) { range in Button(action: { @@ -204,44 +216,105 @@ struct MediaInfoView: View { .font(.system(size: 14)) .foregroundColor(.accentColor) } - } - } - - ForEach(episodeLinks.indices.filter { selectedRange.contains($0) }, id: \.self) { i in - let ep = episodeLinks[i] - let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") - let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") - let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 - - EpisodeCell( - episodeIndex: i, - episode: ep.href, - episodeID: ep.number - 1, - progress: progress, - itemID: itemID ?? 0, - onTap: { imageUrl in - if !isFetchingEpisode { - selectedEpisodeNumber = ep.number - selectedEpisodeImage = imageUrl - fetchStream(href: ep.href) - AnalyticsManager.shared.sendEvent( - event: "watch", - additionalData: ["title": title, "episode": ep.number] - ) + } else if isGroupedBySeasons { + let seasons = groupedEpisodes() + if !seasons.isEmpty { + Menu { + ForEach(0.. 0 ? lastPlayedTime / totalTime : 0 + + EpisodeCell( + episodeIndex: i, + episode: ep.href, + episodeID: ep.number - 1, + progress: progress, + itemID: itemID ?? 0, + onTap: { imageUrl in + if !isFetchingEpisode { + selectedEpisodeNumber = ep.number + selectedEpisodeImage = imageUrl + fetchStream(href: ep.href) + AnalyticsManager.shared.sendEvent( + event: "watch", + additionalData: ["title": title, "episode": ep.number] + ) + } + }, + onMarkAllPrevious: { + for idx in 0.. 0 ? lastPlayedTime / totalTime : 0 + + EpisodeCell( + episodeIndex: i, + episode: ep.href, + episodeID: ep.number - 1, + progress: progress, + itemID: itemID ?? 0, + onTap: { imageUrl in + if !isFetchingEpisode { + selectedEpisodeNumber = ep.number + selectedEpisodeImage = imageUrl + fetchStream(href: ep.href) + AnalyticsManager.shared.sendEvent( + event: "watch", + additionalData: ["title": title, "episode": ep.number] + ) + } + }, + onMarkAllPrevious: { + for idx in 0.. [[EpisodeLink]] { + guard !episodeLinks.isEmpty else { return [] } + var groups: [[EpisodeLink]] = [] + var currentGroup: [EpisodeLink] = [] + + for ep in episodeLinks { + if let last = currentGroup.last, ep.number <= last.number { + groups.append(currentGroup) + currentGroup = [ep] + } else { + currentGroup.append(ep) + } + } + + if !currentGroup.isEmpty { + groups.append(currentGroup) + } + return groups + } + func fetchDetails() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { Task { From 418435957e89c7917eb84de43fa6a33cc38b51dd Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Tue, 18 Mar 2025 21:36:26 +0100 Subject: [PATCH 18/21] Implemented season seperator for tv shows 2 --- Sora/Views/MediaInfoView/MediaInfoView.swift | 42 +++++++------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 5ce7cb2..02a8499 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -50,14 +50,7 @@ struct MediaInfoView: View { @State private var selectedRange: Range = 0..<100 private var isGroupedBySeasons: Bool { - var lastEpisodeNumber = 0 - for episode in episodeLinks { - if episode.number == 1 && lastEpisodeNumber > 1 { - return true - } - lastEpisodeNumber = episode.number - } - return false + return groupedEpisodes().count > 1 } var body: some View { @@ -205,9 +198,7 @@ struct MediaInfoView: View { if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { Menu { ForEach(generateRanges(), id: \.self) { range in - Button(action: { - selectedRange = range - }) { + Button(action: { selectedRange = range }) { Text("\(range.lowerBound + 1)-\(range.upperBound)") } } @@ -218,12 +209,10 @@ struct MediaInfoView: View { } } else if isGroupedBySeasons { let seasons = groupedEpisodes() - if !seasons.isEmpty { + if seasons.count > 1 { Menu { ForEach(0.. 0 ? lastPlayedTime / totalTime : 0 EpisodeCell( - episodeIndex: i, + episodeIndex: selectedSeason, episode: ep.href, episodeID: ep.number - 1, progress: progress, @@ -263,13 +250,13 @@ struct MediaInfoView: View { } }, onMarkAllPrevious: { - for idx in 0.. [[EpisodeLink]] { guard !episodeLinks.isEmpty else { return [] } var groups: [[EpisodeLink]] = [] - var currentGroup: [EpisodeLink] = [] + var currentGroup: [EpisodeLink] = [episodeLinks[0]] - for ep in episodeLinks { - if let last = currentGroup.last, ep.number <= last.number { + for ep in episodeLinks.dropFirst() { + if let last = currentGroup.last, ep.number < last.number { groups.append(currentGroup) currentGroup = [ep] } else { @@ -458,11 +445,10 @@ struct MediaInfoView: View { } } - if !currentGroup.isEmpty { - groups.append(currentGroup) - } + groups.append(currentGroup) return groups } + func fetchDetails() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { From b6a9185fa0ec42bc0cdf53d954f7d642b0ad0f96 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Tue, 18 Mar 2025 22:46:48 +0100 Subject: [PATCH 19/21] THE goat of buttons --- .../CustomPlayer/CustomPlayer.swift | 151 ++++++++++++------ 1 file changed, 103 insertions(+), 48 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 4966b1c..b9d00aa 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -36,7 +36,13 @@ class CustomMediaPlayerViewController: UIViewController { var duration: Double = 0.0 var isVideoLoaded = false var showWatchNextButton = true + + var watchNextButtonTimer: Timer? + var isWatchNextRepositioned: Bool = false var isWatchNextVisible: Bool = false + var lastDuration: Double = 0.0 + var watchNextButtonAppearedAt: Double? + var subtitleForegroundColor: String = "white" var subtitleBackgroundEnabled: Bool = true @@ -535,7 +541,8 @@ class CustomMediaPlayerViewController: UIViewController { func addTimeObserver() { let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in - guard let self = self, let currentItem = self.player.currentItem, + guard let self = self, + let currentItem = self.player.currentItem, currentItem.duration.seconds.isFinite else { return } let currentDuration = currentItem.duration.seconds @@ -557,46 +564,7 @@ class CustomMediaPlayerViewController: UIViewController { self.subtitleLabel.text = "" } - let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10) - && self.currentTimeVal != self.duration - && self.showWatchNextButton - && self.duration != 0 - - if isNearEnd { - if !self.isWatchNextVisible { - self.isWatchNextVisible = true - self.watchNextButton.isHidden = false - - if self.isControlsVisible { - self.watchNextButton.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - self.watchNextButton.alpha = 0.0 - - UIView.animate(withDuration: 0.7, delay: 0, options: .curveEaseInOut, animations: { - NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) - NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - self.view.layoutIfNeeded() - self.watchNextButton.alpha = 0.8 - self.watchNextButton.transform = .identity - }) - } else { - self.watchNextButton.alpha = 0.0 - UIView.animate(withDuration: 0.7, delay: 0, options: .curveEaseInOut, animations: { - NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) - NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) - self.view.layoutIfNeeded() - self.watchNextButton.alpha = 0.8 - }) - } - } - } else { - self.isWatchNextVisible = false - UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: { - self.watchNextButton.alpha = 0.0 - }, completion: { _ in - self.watchNextButton.isHidden = true - }) - } - + // ORIGINAL PROGRESS BAR CODE: DispatchQueue.main.async { self.sliderHostingController?.rootView = MusicProgressSlider( value: Binding(get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) }, @@ -615,8 +583,82 @@ class CustomMediaPlayerViewController: UIViewController { } ) } + + // Watch Next Button Logic: + let hideNext = UserDefaults.standard.bool(forKey: "hideNextButton") + let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10) + && self.currentTimeVal != self.duration + && self.showWatchNextButton + && self.duration != 0 + + if isNearEnd { + // First appearance: show the button in its normal position. + if !self.isWatchNextVisible { + self.isWatchNextVisible = true + self.watchNextButtonAppearedAt = self.currentTimeVal + + // Choose constraints based on current controls visibility. + if self.isControlsVisible { + NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) + NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) + } else { + NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) + NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) + } + // Soft fade-in. + 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) + } + + // When 5 seconds have elapsed from when the button first appeared: + if let appearedAt = self.watchNextButtonAppearedAt, + (self.currentTimeVal - appearedAt) >= 5, + !self.isWatchNextRepositioned { + // Fade out the button first (even if controls are visible). + UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { + self.watchNextButton.alpha = 0.0 + }, completion: { _ in + self.watchNextButton.isHidden = true + // Then lock it to the controls-attached constraints. + NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) + NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) + self.isWatchNextRepositioned = true + }) + } + } else { + // Not near end: reset the watch-next button state. + 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 + }) + } } } + + + + func repositionWatchNextButton() { + self.isWatchNextRepositioned = true + // Update constraints so the button is now attached next to the playback controls. + UIView.animate(withDuration: 0.3, animations: { + NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) + NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) + self.view.layoutIfNeeded() + self.watchNextButton.alpha = 0.0 + }, completion: { _ in + self.watchNextButton.isHidden = true + }) + self.watchNextButtonTimer?.invalidate() + self.watchNextButtonTimer = nil + } + func startUpdateTimer() { updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in @@ -627,21 +669,34 @@ class CustomMediaPlayerViewController: UIViewController { @objc func toggleControls() { isControlsVisible.toggle() - - UIView.animate(withDuration: 0.2, animations: { + UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { self.controlsContainerView.alpha = self.isControlsVisible ? 1 : 0 self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0 if self.isControlsVisible { + // Always use the controls-attached constraints. NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - self.watchNextButton.alpha = 1.0 + if self.isWatchNextRepositioned || self.isWatchNextVisible { + self.watchNextButton.isHidden = false + UIView.animate(withDuration: 0.5, animations: { + self.watchNextButton.alpha = 0.8 + }) + } } else { - NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) - NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) - self.watchNextButton.alpha = 0.8 + // When controls are hidden: + 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() }) } From b510f8a19ba86e7cb8f3ccf94c1922eee235d6ec Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Tue, 18 Mar 2025 22:52:28 +0100 Subject: [PATCH 20/21] =?UTF-8?q?settings=20toggle=20now=20actually=20does?= =?UTF-8?q?=20some=20=F0=9F=99=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomPlayer/CustomPlayer.swift | 71 ++++++++++++------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index b9d00aa..fabbb15 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -584,7 +584,7 @@ class CustomMediaPlayerViewController: UIViewController { ) } - // Watch Next Button Logic: + // --- Watch Next Button Logic --- let hideNext = UserDefaults.standard.bool(forKey: "hideNextButton") let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10) && self.currentTimeVal != self.duration @@ -592,7 +592,7 @@ class CustomMediaPlayerViewController: UIViewController { && self.duration != 0 if isNearEnd { - // First appearance: show the button in its normal position. + // First appearance: show the button in its "normal" position. if !self.isWatchNextVisible { self.isWatchNextVisible = true self.watchNextButtonAppearedAt = self.currentTimeVal @@ -613,19 +613,34 @@ class CustomMediaPlayerViewController: UIViewController { }, completion: nil) } - // When 5 seconds have elapsed from when the button first appeared: - if let appearedAt = self.watchNextButtonAppearedAt, - (self.currentTimeVal - appearedAt) >= 5, - !self.isWatchNextRepositioned { - // Fade out the button first (even if controls are visible). - UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { - self.watchNextButton.alpha = 0.0 - }, completion: { _ in - self.watchNextButton.isHidden = true - // Then lock it to the controls-attached constraints. + if hideNext { + // When the toggle is on: after 5 seconds, fade out then lock. + if let appearedAt = self.watchNextButtonAppearedAt, + (self.currentTimeVal - appearedAt) >= 5, + !self.isWatchNextRepositioned { + // First fade out. + UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { + self.watchNextButton.alpha = 0.0 + }, completion: { _ in + self.watchNextButton.isHidden = true + // Then lock it to the controls-attached constraints. + NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) + NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) + self.isWatchNextRepositioned = true + }) + } + } else { + // When the toggle is off: always keep the button visible and simply update constraints. + if self.isControlsVisible { NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - self.isWatchNextRepositioned = true + } else { + NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) + NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) + } + self.watchNextButton.isHidden = false + UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { + self.watchNextButton.alpha = 0.8 }) } } else { @@ -642,7 +657,6 @@ class CustomMediaPlayerViewController: UIViewController { } } - func repositionWatchNextButton() { self.isWatchNextRepositioned = true @@ -669,32 +683,37 @@ class CustomMediaPlayerViewController: UIViewController { @objc func toggleControls() { isControlsVisible.toggle() + // Read the toggle value. + let hideNext = UserDefaults.standard.bool(forKey: "hideNextButton") UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { self.controlsContainerView.alpha = self.isControlsVisible ? 1 : 0 self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0 if self.isControlsVisible { - // Always use the controls-attached constraints. + // When controls are shown, always use the controls-attached constraints. NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - if self.isWatchNextRepositioned || self.isWatchNextVisible { - self.watchNextButton.isHidden = false - UIView.animate(withDuration: 0.5, animations: { - self.watchNextButton.alpha = 0.8 - }) - } + self.watchNextButton.isHidden = false + UIView.animate(withDuration: 0.5, animations: { + self.watchNextButton.alpha = 0.8 + }) } else { // When controls are hidden: - if !self.isWatchNextRepositioned && self.isWatchNextVisible { - NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) - NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) - } - if self.isWatchNextRepositioned { + if hideNext && self.isWatchNextRepositioned { + // If the toggle is on and the button is locked, fade it out. UIView.animate(withDuration: 0.5, animations: { self.watchNextButton.alpha = 0.0 }, completion: { _ in self.watchNextButton.isHidden = true }) + } else { + // Otherwise, update to the normal (hidden-controls) constraints and keep visible. + NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) + NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) + self.watchNextButton.isHidden = false + UIView.animate(withDuration: 0.5, animations: { + self.watchNextButton.alpha = 0.8 + }) } } self.view.layoutIfNeeded() From d4c50910b3bc83ccdbdaca044d3c6d5b7d3281fe Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Tue, 18 Mar 2025 23:08:29 +0100 Subject: [PATCH 21/21] im only human after alll --- .../CustomPlayer/CustomPlayer.swift | 71 +++++++------------ 1 file changed, 26 insertions(+), 45 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index fabbb15..b9d00aa 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -584,7 +584,7 @@ class CustomMediaPlayerViewController: UIViewController { ) } - // --- Watch Next Button Logic --- + // Watch Next Button Logic: let hideNext = UserDefaults.standard.bool(forKey: "hideNextButton") let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10) && self.currentTimeVal != self.duration @@ -592,7 +592,7 @@ class CustomMediaPlayerViewController: UIViewController { && self.duration != 0 if isNearEnd { - // First appearance: show the button in its "normal" position. + // First appearance: show the button in its normal position. if !self.isWatchNextVisible { self.isWatchNextVisible = true self.watchNextButtonAppearedAt = self.currentTimeVal @@ -613,34 +613,19 @@ class CustomMediaPlayerViewController: UIViewController { }, completion: nil) } - if hideNext { - // When the toggle is on: after 5 seconds, fade out then lock. - if let appearedAt = self.watchNextButtonAppearedAt, - (self.currentTimeVal - appearedAt) >= 5, - !self.isWatchNextRepositioned { - // First fade out. - UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { - self.watchNextButton.alpha = 0.0 - }, completion: { _ in - self.watchNextButton.isHidden = true - // Then lock it to the controls-attached constraints. - NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) - NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - self.isWatchNextRepositioned = true - }) - } - } else { - // When the toggle is off: always keep the button visible and simply update constraints. - if self.isControlsVisible { + // When 5 seconds have elapsed from when the button first appeared: + if let appearedAt = self.watchNextButtonAppearedAt, + (self.currentTimeVal - appearedAt) >= 5, + !self.isWatchNextRepositioned { + // Fade out the button first (even if controls are visible). + UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { + self.watchNextButton.alpha = 0.0 + }, completion: { _ in + self.watchNextButton.isHidden = true + // Then lock it to the controls-attached constraints. NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - } else { - NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) - NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) - } - self.watchNextButton.isHidden = false - UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { - self.watchNextButton.alpha = 0.8 + self.isWatchNextRepositioned = true }) } } else { @@ -657,6 +642,7 @@ class CustomMediaPlayerViewController: UIViewController { } } + func repositionWatchNextButton() { self.isWatchNextRepositioned = true @@ -683,37 +669,32 @@ class CustomMediaPlayerViewController: UIViewController { @objc func toggleControls() { isControlsVisible.toggle() - // Read the toggle value. - let hideNext = UserDefaults.standard.bool(forKey: "hideNextButton") UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { self.controlsContainerView.alpha = self.isControlsVisible ? 1 : 0 self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0 if self.isControlsVisible { - // When controls are shown, always use the controls-attached constraints. + // Always use the controls-attached constraints. NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) - self.watchNextButton.isHidden = false - UIView.animate(withDuration: 0.5, animations: { - self.watchNextButton.alpha = 0.8 - }) + if self.isWatchNextRepositioned || self.isWatchNextVisible { + self.watchNextButton.isHidden = false + UIView.animate(withDuration: 0.5, animations: { + self.watchNextButton.alpha = 0.8 + }) + } } else { // When controls are hidden: - if hideNext && self.isWatchNextRepositioned { - // If the toggle is on and the button is locked, fade it out. + 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 }) - } else { - // Otherwise, update to the normal (hidden-controls) constraints and keep visible. - NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) - NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) - self.watchNextButton.isHidden = false - UIView.animate(withDuration: 0.5, animations: { - self.watchNextButton.alpha = 0.8 - }) } } self.view.layoutIfNeeded()