mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
replace custom swipe logic with swiftUIs native .swipeactions (#251)
* 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:
parent
db48f1eab9
commit
9271c3e122
1 changed files with 116 additions and 70 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue