Skip to content

Commit bae5852

Browse files
committed
delete and rename hooked up for live updates, list and preview
1 parent 9acce0e commit bae5852

File tree

3 files changed

+427
-106
lines changed

3 files changed

+427
-106
lines changed

Django Files/API/Files.swift

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,27 @@
88
import Foundation
99

1010
public struct DFFile: Codable, Hashable, Equatable {
11-
public let id: Int
12-
public let user: Int
13-
public let size: Int
14-
public let mime: String
15-
public let name: String
16-
public let userName: String
17-
public let userUsername: String
18-
public let info: String
19-
public let expr: String
20-
public let view: Int
21-
public let maxv: Int
22-
public let password: String
23-
public let `private`: Bool
24-
public let avatar: Bool
25-
public let url: String
26-
public let thumb: String
27-
public let raw: String
28-
public let date: String
29-
public let albums: [Int]
30-
public let exif: [String: AnyCodable]?
31-
public let meta: [String: AnyCodable]?
11+
public var id: Int
12+
public var user: Int
13+
public var size: Int
14+
public var mime: String
15+
public var name: String
16+
public var userName: String
17+
public var userUsername: String
18+
public var info: String
19+
public var expr: String
20+
public var view: Int
21+
public var maxv: Int
22+
public var password: String
23+
public var `private`: Bool
24+
public var avatar: Bool
25+
public var url: String
26+
public var thumb: String
27+
public var raw: String
28+
public var date: String
29+
public var albums: [Int]
30+
public var exif: [String: AnyCodable]?
31+
public var meta: [String: AnyCodable]?
3232

3333
// Skip nested JSON structures
3434
enum CodingKeys: String, CodingKey {
@@ -279,7 +279,7 @@ extension DFAPI {
279279
return nil
280280
}
281281

282-
public func deleteFiles(fileIDs: [Int], selectedServer: DjangoFilesSession? = nil) async {
282+
public func deleteFiles(fileIDs: [Int], selectedServer: DjangoFilesSession? = nil) async -> Bool {
283283
do {
284284
let fileIDsData = try JSONSerialization.data(withJSONObject: ["ids": fileIDs])
285285
let _ = try await makeAPIRequest(
@@ -289,8 +289,10 @@ extension DFAPI {
289289
method: .delete,
290290
selectedServer: selectedServer
291291
)
292+
return true
292293
} catch {
293294
print("File Delete Failed \(error)")
295+
return false
294296
}
295297
}
296298

Django Files/Views/FileList.swift

Lines changed: 143 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,87 @@ import SwiftUI
99
import SwiftData
1010
import Foundation
1111

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+
}
1293

1394
struct CustomLabel: LabelStyle {
1495
var spacing: Double = 0.0
@@ -22,7 +103,7 @@ struct CustomLabel: LabelStyle {
22103
}
23104

24105
struct FileRowView: View {
25-
let file: DFFile
106+
@Binding var file: DFFile
26107
@State var isPrivate: Bool
27108
@State var hasPassword: Bool
28109
@State var hasExpiration: Bool
@@ -44,7 +125,6 @@ struct FileRowView: View {
44125
return components?.url ?? serverURL
45126
}
46127

47-
48128
var body: some View {
49129
HStack(alignment: .center) {
50130
VStack(spacing: 0) {
@@ -131,8 +211,8 @@ struct FileListView: View {
131211
let albumName: String?
132212

133213
@Environment(\.dismiss) private var dismiss
214+
@StateObject private var fileListManager: FileListManager
134215

135-
@State private var files: [DFFile] = []
136216
@State private var currentPage = 1
137217
@State private var hasNextPage: Bool = false
138218
@State private var isLoading: Bool = true
@@ -163,6 +243,19 @@ struct FileListView: View {
163243

164244
@State private var showFileInfo: Bool = false
165245

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+
166259
private func getTitle(server: Binding<DjangoFilesSession?>, albumName: String?) -> String {
167260
if server.wrappedValue != nil && albumName == nil {
168261
return "Files (\(String(describing: URL(string: server.wrappedValue?.url ?? "host")!.host ?? "unknown")))"
@@ -202,46 +295,58 @@ struct FileListView: View {
202295
.listRowSeparator(.hidden)
203296
}
204297

205-
ForEach(files, id: \.id) { file in
298+
ForEach(files.indices, id: \.self) { index in
206299
Button {
207-
selectedFile = file
300+
selectedFile = files[index]
208301
showingPreview = true
209302
} 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()
224320
}
321+
.frame(width: 512, height: 512)
322+
.cornerRadius(8)
323+
}
225324
} 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+
}
230335
}
231336
}
232-
.id(file.id)
337+
.id(files[index].id)
233338
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
234339
Button() {
235-
fileIDsToDelete = [file.id]
236-
fileNameToDelete = file.name
340+
fileIDsToDelete = [files[index].id]
341+
fileNameToDelete = files[index].name
237342
showingDeleteConfirmation = true
238343
} label: {
239344
Label("Delete", systemImage: "trash")
240345
}
241346
.tint(.red)
242347
}
243348

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 }) {
245350
Color.clear
246351
.frame(height: 20)
247352
.onAppear {
@@ -260,11 +365,13 @@ struct FileListView: View {
260365
}
261366
}
262367
.fullScreenCover(isPresented: $showingPreview) {
263-
if let file = selectedFile {
368+
if let index = files.firstIndex(where: { $0.id == selectedFile?.id }) {
264369
FilePreviewView(
265-
file: file,
370+
file: $fileListManager.files[index],
371+
server: server,
266372
showingPreview: $showingPreview,
267-
showFileInfo: $showFileInfo
373+
showFileInfo: $showFileInfo,
374+
fileListDelegate: fileListManager
268375
)
269376
}
270377
}
@@ -329,10 +436,6 @@ struct FileListView: View {
329436
Button("Delete", role: .destructive) {
330437
Task {
331438
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-
}
336439
}
337440
}
338441
Button("Cancel", role: .cancel) {
@@ -610,22 +713,8 @@ struct FileListView: View {
610713
}
611714

612715
@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)
629718
}
630719

631720
@MainActor
@@ -663,12 +752,10 @@ struct FileListView: View {
663752
let url = URL(string: serverInstance.url) else {
664753
return
665754
}
666-
667755
let api = DFAPI(url: url, token: serverInstance.token)
668756
// Toggle the private status (if currently private, make it public and vice versa)
669757
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
672759
}
673760

674761
@MainActor
@@ -680,7 +767,7 @@ struct FileListView: View {
680767

681768
let api = DFAPI(url: url, token: serverInstance.token)
682769
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
684771
}
685772

686773
@MainActor
@@ -691,19 +778,12 @@ struct FileListView: View {
691778
}
692779
let api = DFAPI(url: url, token: serverInstance.token)
693780
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
695782
}
696783

697784
@MainActor
698785
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)
707787
}
708788

709789
}

0 commit comments

Comments
 (0)