replace custom swipe logic with swiftUIs native .swipeactions (#251)
Some checks failed
Build and Release / Build IPA (push) Has been cancelled
Build and Release / Build macOS App (push) Has been cancelled

* replace custom swipe logic with swiftUIs native .swipeactions

* use uservars

* Fixed formatting and also watched status method

* add back the horizontal slider

* removed silly git text

---------

Co-authored-by: cranci1 <100066266+cranci1@users.noreply.github.com>
This commit is contained in:
Mousica 2025-11-01 10:20:01 +02:00 committed by GitHub
parent db48f1eab9
commit 9271c3e122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -24,7 +24,6 @@ struct EpisodeCell: View {
let tmdbID: Int? let tmdbID: Int?
let seasonNumber: Int? let seasonNumber: Int?
//receives the set of filler episode numbers (from MediaInfoView)
let fillerEpisodes: Set<Int>? let fillerEpisodes: Set<Int>?
let isMultiSelectMode: Bool let isMultiSelectMode: Bool
@ -47,9 +46,10 @@ struct EpisodeCell: View {
@State private var isShowingActions: Bool = false @State private var isShowingActions: Bool = false
@State private var actionButtonWidth: CGFloat = 60 @State private var actionButtonWidth: CGFloat = 60
@State private var dragState: DragState = .inactive @State private var dragState: DragState = .inactive
@State private var dragStart: CGPoint?
@State private var retryAttempts: Int = 0 @State private var retryAttempts: Int = 0
private var malIDFromParent: Int? { malID } private var malIDFromParent: Int? { malID }
private let maxRetryAttempts: Int = 3 private let maxRetryAttempts: Int = 3
private let initialBackoffDelay: TimeInterval = 1.0 private let initialBackoffDelay: TimeInterval = 1.0
@ -57,8 +57,8 @@ struct EpisodeCell: View {
@EnvironmentObject var moduleManager: ModuleManager @EnvironmentObject var moduleManager: ModuleManager
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system @AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system
@AppStorage("remainingTimePercentage") private var remainingTimePercentage: Double = 90.0
// Filler state (derived from passed-in fillerEpisodes)
@State private var isFiller: Bool = false @State private var isFiller: Bool = false
init( init(
@ -82,7 +82,7 @@ struct EpisodeCell: View {
seasonNumber: Int? = nil, seasonNumber: Int? = nil,
fillerEpisodes: Set<Int>? = nil fillerEpisodes: Set<Int>? = nil
) { ) {
self.episodeIndex = episodeIndex self.episodeIndex = episodeIndex
self.episode = episode self.episode = episode
self.episodeID = episodeID self.episodeID = episodeID
@ -114,49 +114,44 @@ struct EpisodeCell: View {
} }
var body: some View { var body: some View {
ZStack { episodeCellContent
actionButtonsBackground .onAppear {
setupOnAppear()
episodeCellContent let epNum = episodeID + 1
} if let set = fillerEpisodes {
.onAppear { self.isFiller = set.contains(epNum)
setupOnAppear() }
// set filler state based on passed-in set (if available)
let epNum = episodeID + 1
if let set = fillerEpisodes {
self.isFiller = set.contains(epNum)
} }
} .onChange(of: progress) { _ in updateProgress() }
.onChange(of: progress) { _ in updateProgress() } .onChange(of: itemID) { _ in handleItemIDChange() }
.onChange(of: itemID) { _ in handleItemIDChange() } .onChange(of: tmdbID) { _ in
.onChange(of: tmdbID) { _ in isLoading = true
isLoading = true retryAttempts = 0
retryAttempts = 0 fetchEpisodeDetails()
fetchEpisodeDetails()
}
.onChange(of: fillerEpisodes) { newValue in
let epNum = episodeID + 1
if let set = newValue {
self.isFiller = set.contains(epNum)
} else {
self.isFiller = false
} }
} .onChange(of: fillerEpisodes) { newValue in
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in let epNum = episodeID + 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if let set = newValue {
self.isFiller = set.contains(epNum)
} else {
self.isFiller = false
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
updateDownloadStatus()
updateProgress()
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadStatusChanged"))) { _ in
updateDownloadStatus() updateDownloadStatus()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadCompleted"))) { _ in
updateDownloadStatus()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("episodeProgressChanged"))) { _ in
updateProgress() updateProgress()
} }
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadStatusChanged"))) { _ in
updateDownloadStatus()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadCompleted"))) { _ in
updateDownloadStatus()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("episodeProgressChanged"))) { _ in
updateProgress()
}
} }
} }
@ -171,17 +166,54 @@ private extension EpisodeCell {
} }
var episodeCellContent: some View { var episodeCellContent: some View {
HStack { ZStack {
episodeThumbnail HStack {
episodeInfo episodeThumbnail
Spacer() episodeInfo
if case .downloaded = downloadStatus { Spacer()
downloadedIndicator if case .downloaded = downloadStatus {
.padding(.trailing, 8) downloadedIndicator
.padding(.trailing, 8)
}
CircularProgressBar(progress: currentProgress)
.frame(width: 40, height: 40)
.padding(.trailing, 4)
} }
CircularProgressBar(progress: currentProgress) .contentShape(Rectangle())
.frame(width: 40, height: 40) .padding(.horizontal, 8)
.padding(.trailing, 4) .padding(.vertical, 8)
.frame(maxWidth: .infinity)
.background(cellBackground)
.clipShape(RoundedRectangle(cornerRadius: 15))
.offset(x: swipeOffset + dragState.translation.width)
.zIndex(1)
.scaleEffect(dragState.isActive ? 0.98 : 1.0)
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: swipeOffset)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: dragState.isActive)
.contextMenu { contextMenuContent }
.simultaneousGesture(
DragGesture(minimumDistance: 20, coordinateSpace: .local)
.onChanged { value in
let translation = value.translation
if abs(translation.width) > abs(translation.height) * 2.0 && abs(translation.width) > 30 {
handleDragChanged(value)
}
}
.onEnded { value in
let translation = value.translation
if abs(translation.width) > abs(translation.height) * 2.0 && abs(translation.width) > 30 {
handleDragEnded(value)
} else {
dragStart = nil
dragState = .inactive
}
}
)
.onTapGesture {
handleTap()
}
actionButtonsBackground
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.padding(.horizontal, 8) .padding(.horizontal, 8)
@ -189,21 +221,35 @@ private extension EpisodeCell {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(cellBackground) .background(cellBackground)
.clipShape(RoundedRectangle(cornerRadius: 15)) .clipShape(RoundedRectangle(cornerRadius: 15))
.offset(x: swipeOffset + dragState.translation.width)
.zIndex(1)
.scaleEffect(dragState.isActive ? 0.98 : 1.0)
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: swipeOffset)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: dragState.isActive)
.contextMenu { contextMenuContent } .contextMenu { contextMenuContent }
.simultaneousGesture( .swipeActions(edge: .trailing) {
DragGesture(coordinateSpace: .local)
.onChanged { value in Button(action: { downloadEpisode() }) {
handleDragChanged(value) Label("Download", systemImage: "arrow.down.circle")
}
.tint(.blue)
if progress <= remainingTimePercentage {
Button(action: { markAsWatched() }) {
Label("Watched", systemImage: "checkmark.circle")
} }
.onEnded { value in .tint(.green)
handleDragEnded(value) }
if progress != 0 {
Button(action: { resetProgress() }) {
Label("Reset", systemImage: "arrow.counterclockwise")
} }
) .tint(.orange)
}
if episodeIndex > 0 {
Button(action: { onMarkAllPrevious() }) {
Label("All Prev", systemImage: "checkmark.circle.fill")
}
.tint(.purple)
}
}
.onTapGesture { handleTap() } .onTapGesture { handleTap() }
} }
@ -285,7 +331,7 @@ private extension EpisodeCell {
var contextMenuContent: some View { var contextMenuContent: some View {
Group { Group {
if progress <= 0.9 { if progress <= remainingTimePercentage {
Button(action: markAsWatched) { Button(action: markAsWatched) {
Label("Mark Episode as Watched", systemImage: "checkmark.circle") Label("Mark Episode as Watched", systemImage: "checkmark.circle")
} }
@ -320,7 +366,7 @@ private extension EpisodeCell {
closeActionsAndPerform { downloadEpisode() } closeActionsAndPerform { downloadEpisode() }
} }
if progress <= 0.9 { if progress >= (remainingTimePercentage / 100.0) {
ActionButton( ActionButton(
icon: "checkmark.circle", icon: "checkmark.circle",
label: "Watched", label: "Watched",
@ -470,7 +516,7 @@ private extension EpisodeCell {
func calculateMaxSwipeDistance() -> CGFloat { func calculateMaxSwipeDistance() -> CGFloat {
var buttonCount = 1 var buttonCount = 1
if progress <= 0.9 { buttonCount += 1 } if progress >= (remainingTimePercentage / 100.0) { buttonCount += 1 }
if progress != 0 { buttonCount += 1 } if progress != 0 { buttonCount += 1 }
if episodeIndex > 0 { buttonCount += 1 } if episodeIndex > 0 { buttonCount += 1 }
@ -495,7 +541,9 @@ private extension EpisodeCell {
action() action()
} }
} }
}
private extension EpisodeCell {
func markAsWatched() { func markAsWatched() {
let defaults = UserDefaults.standard let defaults = UserDefaults.standard
let totalTime = 1000.0 let totalTime = 1000.0
@ -520,7 +568,6 @@ private extension EpisodeCell {
} }
} }
} }
func resetProgress() { func resetProgress() {
let userDefaults = UserDefaults.standard let userDefaults = UserDefaults.standard
@ -980,7 +1027,6 @@ private extension EpisodeCell {
} }
}.resume() }.resume()
} }
func handleFetchFailure(error: Error) { func handleFetchFailure(error: Error) {
Logger.shared.log("Episode details fetch error: \(error.localizedDescription)", type: "Error") Logger.shared.log("Episode details fetch error: \(error.localizedDescription)", type: "Error")