diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index 8f5d4a7..21306e6 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -2021,12 +2021,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele let remainingPercentage = (self.duration - self.currentTimeVal) / self.duration let remainingTimePercentage = UserDefaults.standard.object(forKey: "remainingTimePercentage") != nil ? UserDefaults.standard.double(forKey: "remainingTimePercentage") : 90.0 let threshold = (100.0 - remainingTimePercentage) / 100.0 - + if remainingPercentage <= threshold { if self.aniListID != 0 && !self.aniListUpdatedSuccessfully && !self.aniListUpdateImpossible { self.tryAniListUpdate() } - + if let tmdbId = self.tmdbID, tmdbId > 0, !self.traktUpdateSent { self.sendTraktUpdate(tmdbId: tmdbId) } diff --git a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift index b26c09d..1bddaa3 100644 --- a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift @@ -324,12 +324,12 @@ class VideoPlayerViewController: UIViewController { let remainingPercentage = (duration - currentTime) / duration let remainingTimePercentage = UserDefaults.standard.object(forKey: "remainingTimePercentage") != nil ? UserDefaults.standard.double(forKey: "remainingTimePercentage") : 90.0 let threshold = (100.0 - remainingTimePercentage) / 100.0 - + if remainingPercentage <= threshold { if self.aniListID != 0 && !self.aniListUpdateSent { self.sendAniListUpdate() } - + if let tmdbId = self.tmdbID, tmdbId > 0, !self.traktUpdateSent { self.sendTraktUpdate(tmdbId: tmdbId) } diff --git a/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift b/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift index c58cc87..3f8bc6b 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift @@ -25,8 +25,8 @@ struct CircularProgressBar: View { .animation(.linear, value: progress) let remainingTimePercentage = UserDefaults.standard.object(forKey: "remainingTimePercentage") != nil ? UserDefaults.standard.double(forKey: "remainingTimePercentage") : 90.0 - let threshold = (100.0 - remainingTimePercentage) / 100.0 - + let threshold = remainingTimePercentage / 100.0 + if progress >= threshold { Image(systemName: "checkmark") .font(.system(size: 12)) diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index 153178d..15d78af 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -223,7 +223,7 @@ private extension EpisodeCell { } .tint(.blue) - if progress <= remainingTimePercentage { + if progress >= remainingTimePercentage / 100.0 { Button(action: { markAsWatched() }) { Label("Watched", systemImage: "checkmark.circle") } @@ -325,7 +325,7 @@ private extension EpisodeCell { var contextMenuContent: some View { Group { - if progress <= remainingTimePercentage { + if progress >= remainingTimePercentage / 100.0 { Button(action: markAsWatched) { Label("Mark Episode as Watched", systemImage: "checkmark.circle") } diff --git a/Sora/Views/SearchView/SearchView.swift b/Sora/Views/SearchView/SearchView.swift index 9e4b38b..cbba56b 100644 --- a/Sora/Views/SearchView/SearchView.swift +++ b/Sora/Views/SearchView/SearchView.swift @@ -40,6 +40,7 @@ struct SearchView: View { @State private var saveDebounceTimer: Timer? @State private var searchDebounceTimer: Timer? @State private var isActive: Bool = false + @State private var currentSearchTask: Task? init(searchQuery: Binding) { self._searchQuery = searchQuery @@ -244,40 +245,60 @@ struct SearchView: View { hasNoResults = false return } - + isSearchFieldFocused = false - + + currentSearchTask?.cancel() + currentSearchTask = nil + isSearching = true hasNoResults = false searchItems = [] - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - Task { - do { - let jsContent = try moduleManager.getModuleContent(module) - jsController.loadScript(jsContent) - if module.metadata.asyncJS == true { - jsController.fetchJsSearchResults(keyword: searchQuery, module: module) { items in - DispatchQueue.main.async { - searchItems = items - hasNoResults = items.isEmpty - isSearching = false - } - } - } else { - jsController.fetchSearchResults(keyword: searchQuery, module: module) { items in - DispatchQueue.main.async { - searchItems = items - hasNoResults = items.isEmpty - isSearching = false - } + + currentSearchTask = Task { + do { + try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds + guard !Task.isCancelled else { return } + + let jsContent = try moduleManager.getModuleContent(module) + jsController.loadScript(jsContent) + + guard !Task.isCancelled else { return } + + if module.metadata.asyncJS == true { + jsController.fetchJsSearchResults(keyword: searchQuery, module: module) { items in + guard !Task.isCancelled else { return } + DispatchQueue.main.async { + let uniqueItems = items.reduce(into: [String: SearchItem]()) { dict, item in + dict[item.href] = item + }.values + searchItems = Array(uniqueItems) + hasNoResults = uniqueItems.isEmpty + isSearching = false + currentSearchTask = nil } } - } catch { + } else { + jsController.fetchSearchResults(keyword: searchQuery, module: module) { items in + guard !Task.isCancelled else { return } + DispatchQueue.main.async { + let uniqueItems = items.reduce(into: [String: SearchItem]()) { dict, item in + dict[item.href] = item + }.values + searchItems = Array(uniqueItems) + hasNoResults = uniqueItems.isEmpty + isSearching = false + currentSearchTask = nil + } + } + } + } catch { + if !Task.isCancelled { Logger.shared.log("Error loading module: \(error)", type: "Error") DispatchQueue.main.async { isSearching = false hasNoResults = true + currentSearchTask = nil } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 7c3f741..1e3df07 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -156,7 +156,7 @@ fileprivate struct SettingsStepperRow: View { let step: Double var formatter: (Double) -> String = { "\(Int($0))" } var showDivider: Bool = true - + init(icon: String, title: String, value: Binding, range: ClosedRange, step: Double, formatter: @escaping (Double) -> String = { "\(Int($0))" }, showDivider: Bool = true) { self.icon = icon self.title = title @@ -166,24 +166,77 @@ fileprivate struct SettingsStepperRow: View { self.formatter = formatter self.showDivider = showDivider } - + var body: some View { VStack(spacing: 0) { HStack { Image(systemName: icon) .frame(width: 24, height: 24) .foregroundStyle(.primary) - + Text(title) .foregroundStyle(.primary) - + Spacer() - + Stepper(formatter(value), value: $value, in: range, step: step) } .padding(.horizontal, 16) .padding(.vertical, 12) - + + if showDivider { + Divider() + .padding(.horizontal, 16) + } + } + } +} + +fileprivate struct SettingsTextFieldRow: View { + let icon: String + let title: String + @Binding var value: Double + let range: ClosedRange + var showDivider: Bool = true + + init(icon: String, title: String, value: Binding, range: ClosedRange, showDivider: Bool = true) { + self.icon = icon + self.title = title + self._value = value + self.range = range + self.showDivider = showDivider + } + + var body: some View { + VStack(spacing: 0) { + HStack { + Image(systemName: icon) + .frame(width: 24, height: 24) + .foregroundStyle(.primary) + + Text(title) + .foregroundStyle(.primary) + + Spacer() + + TextField("", value: $value, format: .number) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(width: 60) + .onChange(of: value) { newValue in + if newValue < range.lowerBound { + value = range.lowerBound + } else if newValue > range.upperBound { + value = range.upperBound + } + } + + Text("%") + .foregroundStyle(.gray) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + if showDivider { Divider() .padding(.horizontal, 16) @@ -247,12 +300,11 @@ struct SettingsViewPlayer: View { showDivider: true ) - SettingsPickerRow( + SettingsTextFieldRow( icon: "timer", title: NSLocalizedString("Completion Percentage", comment: ""), - options: [60.0, 70.0, 80.0, 90.0, 95.0, 100.0], - optionToString: { "\(Int($0))%" }, - selection: $remainingTimePercentage, + value: $remainingTimePercentage, + range: 0...100, showDivider: false ) }