From e63c27d64907ffacae2846b9b7b336c88ec2a421 Mon Sep 17 00:00:00 2001 From: 50/50 <80717571+50n50@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:17:46 +0200 Subject: [PATCH] Episodecell fixes + iPad scaling (not perfect) (#163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed episodecells getting stuck sliding * Enabled device scaling for ipad not good enough yet, not applied everywhere cuz idk where to apply exactly 💯 --- .../ViewModifiers/DeviceScaleModifier.swift | 7 +- .../EpisodeCell/EpisodeCell.swift | 179 +++++++++++++----- Sora/Views/MediaInfoView/MediaInfoView.swift | 1 - 3 files changed, 132 insertions(+), 55 deletions(-) diff --git a/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift b/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift index 97b6969..1667d6f 100644 --- a/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift +++ b/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift @@ -6,7 +6,7 @@ // import SwiftUI -/* + struct DeviceScaleModifier: ViewModifier { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -28,13 +28,14 @@ struct DeviceScaleModifier: ViewModifier { .position(x: geo.size.width / 2, y: geo.size.height / 2) } } -}*/ +} +/* struct DeviceScaleModifier: ViewModifier { func body(content: Content) -> some View { content // does nothing for now } -} +}*/ extension View { diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index 63788b4..0707d64 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -47,6 +47,7 @@ struct EpisodeCell: View { @State private var swipeOffset: CGFloat = 0 @State private var isShowingActions: Bool = false @State private var actionButtonWidth: CGFloat = 60 + @State private var dragState: DragState = .inactive @State private var retryAttempts: Int = 0 private let maxRetryAttempts: Int = 3 @@ -58,6 +59,39 @@ struct EpisodeCell: View { @Environment(\.colorScheme) private var colorScheme @AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system + enum DragState { + case inactive + case pressing + case dragging(translation: CGSize) + + var translation: CGSize { + switch self { + case .inactive, .pressing: + return .zero + case .dragging(let translation): + return translation + } + } + + var isActive: Bool { + switch self { + case .inactive: + return false + case .pressing, .dragging: + return true + } + } + + var isDragging: Bool { + switch self { + case .dragging: + return true + default: + return false + } + } + } + private var downloadStatusString: String { switch downloadStatus { case .notDownloaded: @@ -152,67 +186,26 @@ struct EpisodeCell: View { ) ) .clipShape(RoundedRectangle(cornerRadius: 15)) - .offset(x: swipeOffset) + .offset(x: swipeOffset + dragState.translation.width) .zIndex(1) - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: swipeOffset) + .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() + DragGesture(coordinateSpace: .local) .onChanged { value in - let horizontalTranslation = value.translation.width - let verticalTranslation = value.translation.height - - let isDefinitelyHorizontalSwipe = abs(horizontalTranslation) > 10 && abs(horizontalTranslation) > abs(verticalTranslation) * 1.5 - - if isShowingActions || isDefinitelyHorizontalSwipe { - if horizontalTranslation < 0 { - let maxSwipe = calculateMaxSwipeDistance() - swipeOffset = max(horizontalTranslation, -maxSwipe) - } else if isShowingActions { - let maxSwipe = calculateMaxSwipeDistance() - swipeOffset = max(horizontalTranslation - maxSwipe, -maxSwipe) - } - } + handleDragChanged(value) } .onEnded { value in - let horizontalTranslation = value.translation.width - let verticalTranslation = value.translation.height - - let wasHandlingGesture = abs(horizontalTranslation) > 10 && abs(horizontalTranslation) > abs(verticalTranslation) * 1.5 - - if isShowingActions || wasHandlingGesture { - let maxSwipe = calculateMaxSwipeDistance() - let threshold = maxSwipe * 0.2 - - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - if horizontalTranslation < -threshold && !isShowingActions { - swipeOffset = -maxSwipe - isShowingActions = true - } else if horizontalTranslation > threshold && isShowingActions { - swipeOffset = 0 - isShowingActions = false - } else { - swipeOffset = isShowingActions ? -maxSwipe : 0 - } - } - } + handleDragEnded(value) } ) } .onTapGesture { - if isShowingActions { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - swipeOffset = 0 - isShowingActions = false - } - } else if isMultiSelectMode { - onSelectionChanged?(!isSelected) - } else { - let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl - onTap(imageUrl) - } + handleTap() } .onAppear { updateProgress() @@ -975,13 +968,97 @@ struct EpisodeCell: View { .padding(.horizontal, 8) } + private func handleDragChanged(_ value: DragGesture.Value) { + let translation = value.translation + let velocity = value.velocity + + let isHorizontalGesture = abs(translation.width) > abs(translation.height) + let hasSignificantHorizontalMovement = abs(translation.width) > 10 + + if isHorizontalGesture && hasSignificantHorizontalMovement { + dragState = .dragging(translation: .zero) + + let proposedOffset = swipeOffset + translation.width + let maxSwipe = calculateMaxSwipeDistance() + + if translation.width < 0 { + let newOffset = max(proposedOffset, -maxSwipe) + if proposedOffset < -maxSwipe { + let resistance = abs(proposedOffset + maxSwipe) * 0.15 + swipeOffset = -maxSwipe - resistance + } else { + swipeOffset = newOffset + } + } else if isShowingActions { + swipeOffset = max(proposedOffset, -maxSwipe) + } + } else if !hasSignificantHorizontalMovement { + dragState = .inactive + } + } + + private func handleDragEnded(_ value: DragGesture.Value) { + let translation = value.translation + let velocity = value.velocity + + dragState = .inactive + + let isHorizontalGesture = abs(translation.width) > abs(translation.height) + let hasSignificantHorizontalMovement = abs(translation.width) > 10 + + if isHorizontalGesture && hasSignificantHorizontalMovement { + let maxSwipe = calculateMaxSwipeDistance() + let threshold = maxSwipe * 0.3 + let velocityThreshold: CGFloat = 500 + + withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { + if translation.width < -threshold || velocity.width < -velocityThreshold { + swipeOffset = -maxSwipe + isShowingActions = true + } else if translation.width > threshold || velocity.width > velocityThreshold { + swipeOffset = 0 + isShowingActions = false + } else { + swipeOffset = isShowingActions ? -maxSwipe : 0 + } + } + } else { + withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { + swipeOffset = isShowingActions ? -calculateMaxSwipeDistance() : 0 + } + } + } + + private func handleTap() { + if isShowingActions { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + swipeOffset = 0 + isShowingActions = false + } + } else if isMultiSelectMode { + onSelectionChanged?(!isSelected) + } else { + let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl + onTap(imageUrl) + } + } + + private func closeActionsIfNeeded() { + if isShowingActions { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + swipeOffset = 0 + isShowingActions = false + } + } + } + private func closeActionsAndPerform(action: @escaping () -> Void) { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { swipeOffset = 0 isShowingActions = false } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { action() } } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 7b4ee62..5dea2e1 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -248,7 +248,6 @@ struct MediaInfoView: View { .shadow(color: (colorScheme == .dark ? Color.black : Color.white).opacity(1), radius: 10, x: 0, y: 10) ) } - .deviceScaled() } } .onAppear {