mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 08:32:00 +00:00
fix scrolling logic for download pages (#258)
* fix scrolling in download series option menu homepage * fix ai doodoo code, use .swipeactions * fix
This commit is contained in:
parent
0361eb9aed
commit
97b81186ae
1 changed files with 59 additions and 171 deletions
|
|
@ -37,27 +37,28 @@ struct DownloadView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(spacing: 0) {
|
Group {
|
||||||
Spacer()
|
|
||||||
.frame(height: 20)
|
|
||||||
CustomDownloadHeader(
|
|
||||||
selectedTab: $selectedTab,
|
|
||||||
searchText: $searchText,
|
|
||||||
isSearchActive: $isSearchActive,
|
|
||||||
sortOption: $sortOption,
|
|
||||||
showSortMenu: selectedTab == 1 && !jsController.savedAssets.isEmpty
|
|
||||||
)
|
|
||||||
|
|
||||||
if selectedTab == 0 {
|
if selectedTab == 0 {
|
||||||
activeDownloadsView
|
activeDownloadsView
|
||||||
.transition(.opacity)
|
|
||||||
} else {
|
} else {
|
||||||
downloadedContentView
|
downloadedContentView
|
||||||
.transition(.opacity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.2), value: selectedTab)
|
.safeAreaInset(edge: .top) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
CustomDownloadHeader(
|
||||||
|
selectedTab: $selectedTab,
|
||||||
|
searchText: $searchText,
|
||||||
|
isSearchActive: $isSearchActive,
|
||||||
|
sortOption: $sortOption,
|
||||||
|
showSortMenu: selectedTab == 1 && !jsController.savedAssets.isEmpty
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
.background(.ultraThinMaterial)
|
||||||
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: selectedTab)
|
||||||
.alert(NSLocalizedString("Delete Download", comment: ""), isPresented: $showDeleteAlert) {
|
.alert(NSLocalizedString("Delete Download", comment: ""), isPresented: $showDeleteAlert) {
|
||||||
Button(NSLocalizedString("Delete", comment: ""), role: .destructive) {
|
Button(NSLocalizedString("Delete", comment: ""), role: .destructive) {
|
||||||
if let asset = assetToDelete {
|
if let asset = assetToDelete {
|
||||||
|
|
@ -117,7 +118,7 @@ struct DownloadView: View {
|
||||||
totalEpisodes: filteredAndSortedAssets.count,
|
totalEpisodes: filteredAndSortedAssets.count,
|
||||||
totalSize: filteredAndSortedAssets.reduce(0) { $0 + $1.fileSize }
|
totalSize: filteredAndSortedAssets.reduce(0) { $0 + $1.fileSize }
|
||||||
)
|
)
|
||||||
|
|
||||||
DownloadedSection(
|
DownloadedSection(
|
||||||
groups: groupedAssets,
|
groups: groupedAssets,
|
||||||
onDelete: { asset in
|
onDelete: { asset in
|
||||||
|
|
@ -130,6 +131,12 @@ struct DownloadView: View {
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
.scrollViewBottomPadding()
|
.scrollViewBottomPadding()
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
UIScrollView.appearance().bounces = false
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
UIScrollView.appearance().bounces = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1043,6 +1050,7 @@ struct EnhancedShowEpisodesView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
heroImageSection
|
||||||
mainScrollView
|
mainScrollView
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
.ignoresSafeArea(.container, edges: .top)
|
.ignoresSafeArea(.container, edges: .top)
|
||||||
|
|
@ -1050,12 +1058,12 @@ struct EnhancedShowEpisodesView: View {
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
let window = windowScene.windows.first,
|
let window = windowScene.windows.first,
|
||||||
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
||||||
navigationController.interactivePopGestureRecognizer?.isEnabled = true
|
navigationController.interactivePopGestureRecognizer?.isEnabled = true
|
||||||
navigationController.interactivePopGestureRecognizer?.delegate = nil
|
navigationController.interactivePopGestureRecognizer?.delegate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
|
@ -1091,10 +1099,7 @@ struct EnhancedShowEpisodesView: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainScrollView: some View {
|
private var mainScrollView: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
ZStack(alignment: .top) {
|
contentContainer
|
||||||
heroImageSection
|
|
||||||
contentContainer
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
UIScrollView.appearance().bounces = false
|
UIScrollView.appearance().bounces = false
|
||||||
|
|
@ -1103,22 +1108,24 @@ struct EnhancedShowEpisodesView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var heroImageSection: some View {
|
private var heroImageSection: some View {
|
||||||
Group {
|
if let posterURL = group.posterURL {
|
||||||
if let posterURL = group.posterURL {
|
LazyImage(url: posterURL) { state in
|
||||||
LazyImage(url: posterURL) { @MainActor state in
|
if let uiImage = state.imageContainer?.image {
|
||||||
if let uiImage = state.imageContainer?.image {
|
Image(uiImage: uiImage)
|
||||||
Image(uiImage: uiImage)
|
.resizable()
|
||||||
.resizable()
|
.aspectRatio(contentMode: .fill)
|
||||||
.aspectRatio(contentMode: .fill)
|
} else {
|
||||||
.frame(width: UIScreen.main.bounds.width, height: 700)
|
placeholderGradient
|
||||||
.clipped()
|
|
||||||
} else {
|
|
||||||
placeholderGradient
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
placeholderGradient
|
|
||||||
}
|
}
|
||||||
|
.ignoresSafeArea(.all)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: 400)
|
||||||
|
.clipped()
|
||||||
|
} else {
|
||||||
|
placeholderGradient
|
||||||
|
.ignoresSafeArea(.all)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: 400)
|
||||||
|
.clipped()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1135,8 +1142,6 @@ struct EnhancedShowEpisodesView: View {
|
||||||
endPoint: .bottomTrailing
|
endPoint: .bottomTrailing
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.frame(width: UIScreen.main.bounds.width, height: 700)
|
|
||||||
.clipped()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -1318,55 +1323,15 @@ struct EnhancedEpisodeRow: View {
|
||||||
let showDivider: Bool
|
let showDivider: Bool
|
||||||
let onPlay: (DownloadedAsset) -> Void
|
let onPlay: (DownloadedAsset) -> Void
|
||||||
let onDelete: (DownloadedAsset) -> Void
|
let onDelete: (DownloadedAsset) -> Void
|
||||||
@State private var swipeOffset: CGFloat = 0
|
|
||||||
@State private var isShowingActions: Bool = false
|
|
||||||
@State private var dragState = DragState.inactive
|
|
||||||
|
|
||||||
struct DragState {
|
|
||||||
var translation: CGSize
|
|
||||||
var isActive: Bool
|
|
||||||
|
|
||||||
static var inactive: DragState {
|
|
||||||
DragState(translation: .zero, isActive: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
private var fillerBadgeOpacity: Double { colorScheme == .dark ? 0.18 : 0.12 }
|
private var fillerBadgeOpacity: Double { colorScheme == .dark ? 0.18 : 0.12 }
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
|
||||||
actionButtonsBackground
|
|
||||||
episodeCellContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var actionButtonsBackground: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Button(action: {
|
|
||||||
onDelete(asset)
|
|
||||||
}) {
|
|
||||||
VStack(spacing: 4) {
|
|
||||||
Image(systemName: "trash.fill")
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
Text("Delete")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
.frame(width: 60)
|
|
||||||
}
|
|
||||||
.frame(height: 76)
|
|
||||||
}
|
|
||||||
.zIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var episodeCellContent: some View {
|
|
||||||
HStack {
|
HStack {
|
||||||
// Thumbnail
|
// Thumbnail
|
||||||
Group {
|
Group {
|
||||||
if let backdropURL = asset.metadata?.backdropURL ?? asset.metadata?.posterURL {
|
if let backdropURL = asset.metadata?.backdropURL ?? asset.metadata?.posterURL {
|
||||||
LazyImage(url: backdropURL) { @MainActor state in
|
LazyImage(url: backdropURL) { state in
|
||||||
if let uiImage = state.imageContainer?.image {
|
if let uiImage = state.imageContainer?.image {
|
||||||
Image(uiImage: uiImage)
|
Image(uiImage: uiImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
|
|
@ -1391,7 +1356,7 @@ struct EnhancedEpisodeRow: View {
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 56)
|
.frame(width: 100, height: 56)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Text("Episode \(asset.metadata?.episode ?? 0)")
|
Text("Episode \(asset.metadata?.episode ?? 0)")
|
||||||
|
|
@ -1416,9 +1381,9 @@ struct EnhancedEpisodeRow: View {
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
CircularProgressBar(progress: 0.0)
|
CircularProgressBar(progress: 0.0)
|
||||||
.frame(width: 40, height: 40)
|
.frame(width: 40, height: 40)
|
||||||
.padding(.trailing, 4)
|
.padding(.trailing, 4)
|
||||||
|
|
@ -1429,23 +1394,18 @@ struct EnhancedEpisodeRow: View {
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(cellBackground)
|
.background(cellBackground)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 15))
|
.clipShape(RoundedRectangle(cornerRadius: 15))
|
||||||
.offset(x: swipeOffset + dragState.translation.width)
|
.swipeActions(edge: .trailing) {
|
||||||
.zIndex(1)
|
Button(role: .destructive, action: {
|
||||||
.scaleEffect(dragState.isActive ? 0.98 : 1.0)
|
onDelete(asset)
|
||||||
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: swipeOffset)
|
}) {
|
||||||
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: dragState.isActive)
|
Label("Delete", systemImage: "trash.fill")
|
||||||
.simultaneousGesture(
|
}
|
||||||
DragGesture(coordinateSpace: .local)
|
}
|
||||||
.onChanged { value in
|
.onTapGesture {
|
||||||
handleDragChanged(value)
|
onPlay(asset)
|
||||||
}
|
}
|
||||||
.onEnded { value in
|
|
||||||
handleDragEnded(value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.onTapGesture { handleTap() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cellBackground: some View {
|
private var cellBackground: some View {
|
||||||
RoundedRectangle(cornerRadius: 15)
|
RoundedRectangle(cornerRadius: 15)
|
||||||
.fill(Color(UIColor.systemBackground))
|
.fill(Color(UIColor.systemBackground))
|
||||||
|
|
@ -1468,78 +1428,6 @@ struct EnhancedEpisodeRow: View {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = .inactive
|
|
||||||
|
|
||||||
let proposedOffset = swipeOffset + translation.width
|
|
||||||
let maxSwipe: CGFloat = 60 // Only one button
|
|
||||||
|
|
||||||
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 = min(max(proposedOffset, -maxSwipe), maxSwipe * 0.2)
|
|
||||||
}
|
|
||||||
} 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: CGFloat = 60
|
|
||||||
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 ? -60 : 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleTap() {
|
|
||||||
if isShowingActions {
|
|
||||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
|
||||||
swipeOffset = 0
|
|
||||||
isShowingActions = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onPlay(asset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchableStyleModifier: ViewModifier {
|
struct SearchableStyleModifier: ViewModifier {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue