@@ -3,41 +3,50 @@ import Core
33import SwiftUI
44
55extension FileManagerView . FileManagerListing {
6- struct ElementRow : View {
6+ struct ElementRowGrid : View {
77 let element : ExtendedElement
8- let type : FileManagerSettings . DisplayType
98
10- let onAction : ( ) -> Void
9+ let onSelect : ( ) -> Void
10+ let onTap : ( ) -> Void
1111
1212 var body : some View {
13- Group {
14- switch type {
15- case . grid:
16- VStack ( alignment: . leading, spacing: 12 ) {
17- HStack {
18- Icon ( for: element)
19- Spacer ( )
20- Action ( onTap: onAction)
21- }
22- Title ( for: element)
23- }
24- case . list:
25- HStack ( spacing: 12 ) {
26- Icon ( for: element)
27- Title ( for: element)
28- Spacer ( )
29- Action ( onTap: onAction)
30- }
13+ VStack ( alignment: . leading, spacing: 12 ) {
14+ HStack {
15+ Icon ( for: element)
16+ Spacer ( )
17+ Action ( onTap: onSelect)
3118 }
19+ Title ( for: element)
3220 }
3321 . padding ( 12 )
3422 . background ( Color . groupedBackground)
3523 . cornerRadius ( 12 )
24+ . onTapGesture { onTap ( ) }
25+ }
26+ }
27+
28+ struct ElementRowList : View {
29+ let element : ExtendedElement
30+
31+ let onSelect : ( ) -> Void
32+ let onDelete : ( ) -> Void
33+ let onTap : ( ) -> Void
34+
35+ var body : some View {
36+ HStack ( spacing: 12 ) {
37+ Icon ( for: element)
38+ Title ( for: element)
39+ Spacer ( )
40+ Action ( onTap: onSelect)
41+ }
42+ . padding ( 12 )
43+ . background ( Color . groupedBackground)
44+ . modifier ( SwipeToDeleteModifier ( onDelete: onDelete, onTap: onTap) )
3645 }
3746 }
3847}
3948
40- fileprivate extension FileManagerView . FileManagerListing . ElementRow {
49+ fileprivate extension FileManagerView . FileManagerListing {
4150 struct Icon : View {
4251 let element : ExtendedElement
4352
@@ -46,10 +55,11 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow {
4655 case . directory:
4756 return . init( " Folder " )
4857 case . file:
49- if let item = try ? ArchiveItem . Kind ( filename: element. name) {
58+ do {
59+ let item = try ArchiveItem . Kind ( filename: element. name)
5060 return item. icon
51- } else {
52- return . init ( " File " )
61+ } catch {
62+ return Image ( " File " )
5363 }
5464 }
5565 }
@@ -107,3 +117,113 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow {
107117 }
108118 }
109119}
120+
121+ fileprivate extension FileManagerView . FileManagerListing {
122+ struct SwipeToDeleteModifier : ViewModifier {
123+ @State private var offset : CGFloat = 0
124+ @GestureState private var isDragging : Bool = false
125+
126+ let onDelete : ( ) -> Void
127+ let onTap : ( ) -> Void
128+
129+ private var iconSize : Double { 24 }
130+ private var iconPadding : Double { 16 }
131+
132+ private var deleteThreshold : CGFloat { - ( iconSize + iconPadding * 2 ) }
133+ private var fullDeleteThreshold : CGFloat { - 120 }
134+
135+ private var delay : Double { 0.5 }
136+ private var animation : Animation { . easeOut( duration: delay) }
137+
138+ private var maxRadius : CGFloat { 12 }
139+ private var radius : CGFloat {
140+ let progress = min ( 1 , abs ( offset) / abs( deleteThreshold) )
141+ return maxRadius * ( 1 - progress)
142+ }
143+
144+ func body( content: Content ) -> some View {
145+ ZStack {
146+ Rectangle ( )
147+ . foregroundColor ( . red. opacity ( 0.1 ) )
148+ . cornerRadius ( 12 )
149+ . overlay (
150+ Image ( " Delete " )
151+ . resizable ( )
152+ . renderingMode ( . template)
153+ . foregroundColor ( . red)
154+ . frame ( width: iconSize, height: iconSize)
155+ . padding ( 16 )
156+ . contentShape ( Rectangle ( ) )
157+ . onTapGesture {
158+ withAnimation ( animation) {
159+ offset = 0
160+ }
161+ onDelete ( )
162+ } ,
163+ alignment: . trailing
164+ )
165+
166+ content
167+ . clipShape (
168+ . rect(
169+ topLeadingRadius: maxRadius,
170+ bottomLeadingRadius: maxRadius,
171+ bottomTrailingRadius: radius,
172+ topTrailingRadius: radius
173+ )
174+ )
175+ . offset ( x: offset)
176+ . simultaneousGesture (
177+ DragGesture (
178+ minimumDistance: 50 ,
179+ coordinateSpace: . local
180+ )
181+ . updating ( $isDragging) { _, state, _ in
182+ state = true
183+ }
184+ . onChanged { value in
185+ let translation = value. translation. width
186+ if translation <= 0 {
187+ offset = translation
188+ }
189+ }
190+ . onEnded { value in
191+ let translation = value. translation. width
192+
193+ if translation <= fullDeleteThreshold {
194+ withAnimation ( animation) {
195+ offset = - UIScreen. main. bounds. width
196+ }
197+
198+ Task { @MainActor in
199+ try await Task . sleep ( seconds: delay)
200+ onDelete ( )
201+
202+ withAnimation ( animation) {
203+ offset = 0
204+ }
205+ }
206+ } else if translation <= deleteThreshold {
207+ withAnimation ( animation) {
208+ offset = deleteThreshold
209+ }
210+ } else {
211+ withAnimation ( animation) {
212+ offset = 0
213+ }
214+ }
215+ }
216+ )
217+ . onTapGesture {
218+ if offset != 0 {
219+ withAnimation ( animation) {
220+ offset = 0
221+ }
222+ } else {
223+ onTap ( )
224+ }
225+ }
226+ }
227+ }
228+ }
229+ }
0 commit comments