@@ -9,6 +9,87 @@ import SwiftUI
9
9
import SwiftData
10
10
import Foundation
11
11
12
+ protocol FileListDelegate : AnyObject {
13
+ @MainActor
14
+ func deleteFiles( fileIDs: [ Int ] , onSuccess: ( ( ) -> Void ) ? ) async -> Bool
15
+ @MainActor
16
+ func renameFile( fileID: Int , newName: String , onSuccess: ( ( ) -> Void ) ? ) async -> Bool
17
+ }
18
+
19
+ @MainActor
20
+ class FileListManager : ObservableObject , FileListDelegate {
21
+ @Published var files : [ DFFile ] = [ ]
22
+ var server : Binding < DjangoFilesSession ? >
23
+
24
+ init ( server: Binding < DjangoFilesSession ? > ) {
25
+ self . server = server
26
+ }
27
+
28
+ func deleteFiles( fileIDs: [ Int ] , onSuccess: ( ( ) -> Void ) ? ) async -> Bool {
29
+ guard let serverInstance = server. wrappedValue,
30
+ let url = URL ( string: serverInstance. url) else {
31
+ return false
32
+ }
33
+
34
+ let api = DFAPI ( url: url, token: serverInstance. token)
35
+ let status = await api. deleteFiles ( fileIDs: fileIDs, selectedServer: serverInstance)
36
+ if status {
37
+ withAnimation {
38
+ files. removeAll { file in
39
+ fileIDs. contains ( file. id)
40
+ }
41
+ onSuccess ? ( )
42
+ }
43
+ }
44
+ return status
45
+ }
46
+
47
+ func renameFile( fileID: Int , newName: String , onSuccess: ( ( ) -> Void ) ? ) async -> Bool {
48
+ guard let serverInstance = server. wrappedValue,
49
+ let url = URL ( string: serverInstance. url) else {
50
+ return false
51
+ }
52
+
53
+ let api = DFAPI ( url: url, token: serverInstance. token)
54
+ let status = await api. renameFile ( fileID: fileID, name: newName, selectedServer: serverInstance)
55
+ if status {
56
+ withAnimation {
57
+ if let index = files. firstIndex ( where: { $0. id == fileID } ) {
58
+ var updatedFiles = files
59
+
60
+ // Update the name
61
+ updatedFiles [ index] . name = newName
62
+
63
+ // Update URLs that contain the filename
64
+ let file = updatedFiles [ index]
65
+
66
+ // Update raw URL
67
+ if let oldRawURL = URL ( string: file. raw) {
68
+ let newRawURL = oldRawURL. deletingLastPathComponent ( ) . appendingPathComponent ( newName)
69
+ updatedFiles [ index] . raw = newRawURL. absoluteString
70
+ }
71
+
72
+ // Update thumb URL
73
+ if let oldThumbURL = URL ( string: file. thumb) {
74
+ let newThumbURL = oldThumbURL. deletingLastPathComponent ( ) . appendingPathComponent ( newName)
75
+ updatedFiles [ index] . thumb = newThumbURL. absoluteString
76
+ }
77
+
78
+ // Update main URL
79
+ if let oldURL = URL ( string: file. url) {
80
+ let newURL = oldURL. deletingLastPathComponent ( ) . appendingPathComponent ( newName)
81
+ updatedFiles [ index] . url = newURL. absoluteString
82
+ }
83
+
84
+ // Reassign the entire array to trigger a view update
85
+ files = updatedFiles
86
+ }
87
+ onSuccess ? ( )
88
+ }
89
+ }
90
+ return status
91
+ }
92
+ }
12
93
13
94
struct CustomLabel : LabelStyle {
14
95
var spacing : Double = 0.0
@@ -22,7 +103,7 @@ struct CustomLabel: LabelStyle {
22
103
}
23
104
24
105
struct FileRowView : View {
25
- let file : DFFile
106
+ @ Binding var file : DFFile
26
107
@State var isPrivate : Bool
27
108
@State var hasPassword : Bool
28
109
@State var hasExpiration : Bool
@@ -44,7 +125,6 @@ struct FileRowView: View {
44
125
return components? . url ?? serverURL
45
126
}
46
127
47
-
48
128
var body : some View {
49
129
HStack ( alignment: . center) {
50
130
VStack ( spacing: 0 ) {
@@ -131,8 +211,8 @@ struct FileListView: View {
131
211
let albumName : String ?
132
212
133
213
@Environment ( \. dismiss) private var dismiss
214
+ @StateObject private var fileListManager : FileListManager
134
215
135
- @State private var files : [ DFFile ] = [ ]
136
216
@State private var currentPage = 1
137
217
@State private var hasNextPage : Bool = false
138
218
@State private var isLoading : Bool = true
@@ -163,6 +243,19 @@ struct FileListView: View {
163
243
164
244
@State private var showFileInfo : Bool = false
165
245
246
+ init ( server: Binding < DjangoFilesSession ? > , albumID: Int ? , navigationPath: Binding < NavigationPath > , albumName: String ? ) {
247
+ self . server = server
248
+ self . albumID = albumID
249
+ self . navigationPath = navigationPath
250
+ self . albumName = albumName
251
+ _fileListManager = StateObject ( wrappedValue: FileListManager ( server: server) )
252
+ }
253
+
254
+ private var files : [ DFFile ] {
255
+ get { fileListManager. files }
256
+ nonmutating set { fileListManager. files = newValue }
257
+ }
258
+
166
259
private func getTitle( server: Binding < DjangoFilesSession ? > , albumName: String ? ) -> String {
167
260
if server. wrappedValue != nil && albumName == nil {
168
261
return " Files ( \( String ( describing: URL ( string: server. wrappedValue? . url ?? " host " ) !. host ?? " unknown " ) ) ) "
@@ -202,46 +295,58 @@ struct FileListView: View {
202
295
. listRowSeparator ( . hidden)
203
296
}
204
297
205
- ForEach ( files, id: \. id ) { file in
298
+ ForEach ( files. indices , id: \. self ) { index in
206
299
Button {
207
- selectedFile = file
300
+ selectedFile = files [ index ]
208
301
showingPreview = true
209
302
} label: {
210
- if file. mime. starts ( with: " image/ " ) {
211
- FileRowView ( file: file, isPrivate: file. private, hasPassword: ( file. password != " " ) , hasExpiration: ( file. expr != " " ) , serverURL: URL ( string: server. wrappedValue!. url) !)
212
- . contextMenu {
213
- fileContextMenu ( for: file, isPreviewing: false , isPrivate: file. private, expirationText: $expirationText, passwordText: $passwordText, fileNameText: $fileNameText)
214
- } preview: {
215
- CachedAsyncImage ( url: thumbnailURL ( file: file) ) { image in
216
- image
217
- . resizable ( )
218
- . scaledToFill ( )
219
- } placeholder: {
220
- ProgressView ( )
221
- }
222
- . frame ( width: 512 , height: 512 )
223
- . cornerRadius ( 8 )
303
+ if files [ index] . mime. starts ( with: " image/ " ) {
304
+ FileRowView (
305
+ file: $fileListManager. files [ index] ,
306
+ isPrivate: files [ index] . private,
307
+ hasPassword: ( files [ index] . password != " " ) ,
308
+ hasExpiration: ( files [ index] . expr != " " ) ,
309
+ serverURL: URL ( string: server. wrappedValue!. url) !
310
+ )
311
+ . contextMenu {
312
+ fileContextMenu ( for: files [ index] , isPreviewing: false , isPrivate: files [ index] . private, expirationText: $expirationText, passwordText: $passwordText, fileNameText: $fileNameText)
313
+ } preview: {
314
+ CachedAsyncImage ( url: thumbnailURL ( file: files [ index] ) ) { image in
315
+ image
316
+ . resizable ( )
317
+ . scaledToFill ( )
318
+ } placeholder: {
319
+ ProgressView ( )
224
320
}
321
+ . frame ( width: 512 , height: 512 )
322
+ . cornerRadius ( 8 )
323
+ }
225
324
} else {
226
- FileRowView ( file: file, isPrivate: file. private, hasPassword: ( file. password != " " ) , hasExpiration: ( file. expr != " " ) , serverURL: URL ( string: server. wrappedValue!. url) !)
227
- . contextMenu {
228
- fileContextMenu ( for: file, isPreviewing: false , isPrivate: file. private, expirationText: $expirationText, passwordText: $passwordText, fileNameText: $fileNameText)
229
- }
325
+ FileRowView (
326
+ file: $fileListManager. files [ index] ,
327
+ isPrivate: files [ index] . private,
328
+ hasPassword: ( files [ index] . password != " " ) ,
329
+ hasExpiration: ( files [ index] . expr != " " ) ,
330
+ serverURL: URL ( string: server. wrappedValue!. url) !
331
+ )
332
+ . contextMenu {
333
+ fileContextMenu ( for: files [ index] , isPreviewing: false , isPrivate: files [ index] . private, expirationText: $expirationText, passwordText: $passwordText, fileNameText: $fileNameText)
334
+ }
230
335
}
231
336
}
232
- . id ( file . id)
337
+ . id ( files [ index ] . id)
233
338
. swipeActions ( edge: . trailing, allowsFullSwipe: true ) {
234
339
Button ( ) {
235
- fileIDsToDelete = [ file . id]
236
- fileNameToDelete = file . name
340
+ fileIDsToDelete = [ files [ index ] . id]
341
+ fileNameToDelete = files [ index ] . name
237
342
showingDeleteConfirmation = true
238
343
} label: {
239
344
Label ( " Delete " , systemImage: " trash " )
240
345
}
241
346
. tint ( . red)
242
347
}
243
348
244
- if hasNextPage && files. suffix ( 5 ) . contains ( where: { $0. id == file . id } ) {
349
+ if hasNextPage && files. suffix ( 5 ) . contains ( where: { $0. id == files [ index ] . id } ) {
245
350
Color . clear
246
351
. frame ( height: 20 )
247
352
. onAppear {
@@ -260,11 +365,13 @@ struct FileListView: View {
260
365
}
261
366
}
262
367
. fullScreenCover ( isPresented: $showingPreview) {
263
- if let file = selectedFile {
368
+ if let index = files . firstIndex ( where : { $0 . id == selectedFile? . id } ) {
264
369
FilePreviewView (
265
- file: file,
370
+ file: $fileListManager. files [ index] ,
371
+ server: server,
266
372
showingPreview: $showingPreview,
267
- showFileInfo: $showFileInfo
373
+ showFileInfo: $showFileInfo,
374
+ fileListDelegate: fileListManager
268
375
)
269
376
}
270
377
}
@@ -329,10 +436,6 @@ struct FileListView: View {
329
436
Button ( " Delete " , role: . destructive) {
330
437
Task {
331
438
await deleteFiles ( fileIDs: fileIDsToDelete)
332
- // Return to the file list if we're in a detail view
333
- if navigationPath. wrappedValue. count > 0 {
334
- navigationPath. wrappedValue. removeLast ( )
335
- }
336
439
}
337
440
}
338
441
Button ( " Cancel " , role: . cancel) {
@@ -610,22 +713,8 @@ struct FileListView: View {
610
713
}
611
714
612
715
@MainActor
613
- private func deleteFiles( fileIDs: [ Int ] ) async {
614
- guard let serverInstance = server. wrappedValue,
615
- let url = URL ( string: serverInstance. url) else {
616
- return
617
- }
618
-
619
- let api = DFAPI ( url: url, token: serverInstance. token)
620
- await api. deleteFiles ( fileIDs: fileIDs, selectedServer: serverInstance)
621
-
622
- // Remove the deleted files from the local array
623
- withAnimation {
624
- files. removeAll { file in
625
- fileIDs. contains ( file. id)
626
- }
627
- }
628
-
716
+ private func deleteFiles( fileIDs: [ Int ] , onSuccess: ( ( ) -> Void ) ? = nil ) async -> Bool {
717
+ return await fileListManager. deleteFiles ( fileIDs: fileIDs, onSuccess: onSuccess)
629
718
}
630
719
631
720
@MainActor
@@ -663,12 +752,10 @@ struct FileListView: View {
663
752
let url = URL ( string: serverInstance. url) else {
664
753
return
665
754
}
666
-
667
755
let api = DFAPI ( url: url, token: serverInstance. token)
668
756
// Toggle the private status (if currently private, make it public and vice versa)
669
757
let _ = await api. editFiles ( fileIDs: [ file. id] , changes: [ " private " : !file. private] , selectedServer: serverInstance)
670
-
671
- await refreshFiles ( )
758
+ await refreshFiles ( ) // TODO: update local data instead of refresh
672
759
}
673
760
674
761
@MainActor
@@ -680,7 +767,7 @@ struct FileListView: View {
680
767
681
768
let api = DFAPI ( url: url, token: serverInstance. token)
682
769
let _ = await api. editFiles ( fileIDs: [ file. id] , changes: [ " expr " : expr ?? " " ] , selectedServer: serverInstance)
683
- await refreshFiles ( )
770
+ await refreshFiles ( ) // TODO: update local data instead of refresh
684
771
}
685
772
686
773
@MainActor
@@ -691,19 +778,12 @@ struct FileListView: View {
691
778
}
692
779
let api = DFAPI ( url: url, token: serverInstance. token)
693
780
let _ = await api. editFiles ( fileIDs: [ file. id] , changes: [ " password " : password ?? " " ] , selectedServer: serverInstance)
694
- await refreshFiles ( )
781
+ await refreshFiles ( ) // TODO: update local data instead of refresh
695
782
}
696
783
697
784
@MainActor
698
785
private func renameFile( file: DFFile , name: String ) async {
699
- guard let serverInstance = server. wrappedValue,
700
- let url = URL ( string: serverInstance. url) else {
701
- return
702
- }
703
- let api = DFAPI ( url: url, token: serverInstance. token)
704
- if await api. renameFile ( fileID: file. id, name: name, selectedServer: serverInstance) {
705
- await refreshFiles ( )
706
- }
786
+ let _ = await fileListManager. renameFile ( fileID: file. id, newName: name, onSuccess: nil )
707
787
}
708
788
709
789
}
0 commit comments