Skip to content

Commit a6c5033

Browse files
authored
gif support and preview deep linking (#40)
* gif support * preview deep link functionality * better preview state handling * refactor * cleanup * password protected file support * enhance preview swiping and page loading * fix server switch bug * fix preloading, center progress view on preview * improved video framing, preview overlay hiding * Loading fixes * minor tweaks and warning fixes * fix swift6 context error
1 parent f946269 commit a6c5033

15 files changed

+1038
-463
lines changed

Django Files/API/Files.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,17 @@ extension DFAPI {
262262
return nil
263263
}
264264

265-
public func getFileDetails(fileID: Int, selectedServer: DjangoFilesSession? = nil) async -> DFFile? {
265+
public func getFileDetails(fileID: Int, password: String? = nil, selectedServer: DjangoFilesSession? = nil) async -> DFFile? {
266266
do {
267+
var parameters: [String: String] = [:]
268+
if let password = password {
269+
parameters["password"] = password
270+
}
271+
267272
let responseBody = try await makeAPIRequest(
268273
body: Data(),
269274
path: getAPIPath(.file) + "\(fileID)",
270-
parameters: [:],
275+
parameters: parameters,
271276
method: .get,
272277
selectedServer: selectedServer
273278
)

Django Files/Django_FilesApp.swift

Lines changed: 60 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import FirebaseCore
1111
import FirebaseAnalytics
1212
import FirebaseCrashlytics
1313

14+
class PreviewStateManager: ObservableObject {
15+
@Published var deepLinkFile: DFFile?
16+
@Published var showingDeepLinkPreview = false
17+
@Published var deepLinkTargetFileID: Int? = nil
18+
@Published var deepLinkFilePassword: String? = nil
19+
}
20+
1421
class AppDelegate: NSObject, UIApplicationDelegate {
1522
func application(_ application: UIApplication,
1623
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
17-
// Skip Firebase initialization if disabled via launch arguments
1824
let shouldDisableFirebase = ProcessInfo.processInfo.arguments.contains("--DisableFirebase")
1925
if !shouldDisableFirebase {
2026
FirebaseApp.configure()
@@ -36,6 +42,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
3642
@main
3743
struct Django_FilesApp: App {
3844
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
45+
@StateObject private var previewStateManager = PreviewStateManager()
46+
@State private var showFileInfo = false
3947
var sharedModelContainer: ModelContainer = {
4048
let schema = Schema([
4149
DjangoFilesSession.self,
@@ -112,26 +120,72 @@ struct Django_FilesApp: App {
112120
TabViewWindow(sessionManager: sessionManager, selectedTab: $selectedTab)
113121
}
114122
}
123+
.environmentObject(previewStateManager)
115124
.onOpenURL { url in
116-
handleDeepLink(url)
125+
DeepLinks.shared.handleDeepLink(
126+
url,
127+
context: sharedModelContainer.mainContext,
128+
sessionManager: sessionManager,
129+
previewStateManager: previewStateManager,
130+
selectedTab: $selectedTab,
131+
hasExistingSessions: $hasExistingSessions,
132+
showingServerConfirmation: $showingServerConfirmation,
133+
pendingAuthURL: $pendingAuthURL,
134+
pendingAuthSignature: $pendingAuthSignature
135+
)
117136
}
118137
.sheet(isPresented: $showingServerConfirmation) {
119138
ServerConfirmationView(
120139
serverURL: $pendingAuthURL,
121140
signature: $pendingAuthSignature,
122141
onConfirm: { setAsDefault in
123142
Task {
124-
await handleServerConfirmation(confirmed: true, setAsDefault: setAsDefault)
143+
await DeepLinks.shared.handleServerConfirmation(
144+
confirmed: true,
145+
setAsDefault: setAsDefault,
146+
pendingAuthURL: $pendingAuthURL,
147+
pendingAuthSignature: $pendingAuthSignature,
148+
context: sharedModelContainer.mainContext,
149+
sessionManager: sessionManager,
150+
hasExistingSessions: $hasExistingSessions,
151+
selectedTab: $selectedTab
152+
)
125153
}
126154
},
127155
onCancel: {
128156
Task {
129-
await handleServerConfirmation(confirmed: false, setAsDefault: false)
157+
await DeepLinks.shared.handleServerConfirmation(
158+
confirmed: false,
159+
setAsDefault: false,
160+
pendingAuthURL: $pendingAuthURL,
161+
pendingAuthSignature: $pendingAuthSignature,
162+
context: sharedModelContainer.mainContext,
163+
sessionManager: sessionManager,
164+
hasExistingSessions: $hasExistingSessions,
165+
selectedTab: $selectedTab
166+
)
130167
}
131168
},
132169
context: sharedModelContainer.mainContext
133170
)
134171
}
172+
.fullScreenCover(isPresented: $previewStateManager.showingDeepLinkPreview) {
173+
if let file = previewStateManager.deepLinkFile {
174+
FilePreviewView(
175+
file: .constant(file),
176+
server: .constant(nil),
177+
showingPreview: $previewStateManager.showingDeepLinkPreview,
178+
showFileInfo: $showFileInfo,
179+
fileListDelegate: nil,
180+
allFiles: .constant([file]),
181+
currentIndex: 0,
182+
onNavigate: { _ in }
183+
)
184+
.onDisappear {
185+
previewStateManager.deepLinkFile = nil
186+
}
187+
}
188+
}
135189
}
136190
.modelContainer(sharedModelContainer)
137191
#if os(macOS)
@@ -141,145 +195,6 @@ struct Django_FilesApp: App {
141195
#endif
142196
}
143197

144-
private func handleDeepLink(_ url: URL) {
145-
print("Deep link received: \(url)")
146-
guard url.scheme == "djangofiles" else { return }
147-
148-
// Extract the signature from the URL parameters
149-
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
150-
print("Invalid deep link URL")
151-
return
152-
}
153-
print("Deep link host: \(components.host ?? "unknown")")
154-
switch components.host {
155-
case "authorize":
156-
deepLinkAuth(components)
157-
case "serverlist":
158-
selectedTab = .settings
159-
case "filelist":
160-
handleFileListDeepLink(components)
161-
default:
162-
ToastManager.shared.showToast(message: "Unsupported deep link \(url)")
163-
print("Unsupported deep link type: \(components.host ?? "unknown")")
164-
}
165-
}
166-
167-
private func handleFileListDeepLink(_ components: URLComponents) {
168-
guard let urlString = components.queryItems?.first(where: { $0.name == "url" })?.value?.removingPercentEncoding,
169-
let serverURL = URL(string: urlString) else {
170-
print("Invalid server URL in filelist deep link")
171-
return
172-
}
173-
174-
// Find the session with matching URL and select it
175-
let context = sharedModelContainer.mainContext
176-
let descriptor = FetchDescriptor<DjangoFilesSession>()
177-
178-
Task {
179-
do {
180-
let existingSessions = try context.fetch(descriptor)
181-
if let matchingSession = existingSessions.first(where: { $0.url == serverURL.absoluteString }) {
182-
await MainActor.run {
183-
sessionManager.selectedSession = matchingSession
184-
selectedTab = .files
185-
}
186-
} else {
187-
print("No session found for URL: \(serverURL.absoluteString)")
188-
}
189-
} catch {
190-
print("Error fetching sessions: \(error)")
191-
}
192-
}
193-
}
194-
195-
private func deepLinkAuth(_ components: URLComponents) {
196-
guard let signature = components.queryItems?.first(where: { $0.name == "signature" })?.value?.removingPercentEncoding,
197-
let serverURL = URL(string: components.queryItems?.first(where: { $0.name == "url" })?.value?.removingPercentEncoding ?? "") else {
198-
print("Unable to parse auth deep link.")
199-
return
200-
}
201-
202-
// Check if a session with this URL already exists
203-
let context = sharedModelContainer.mainContext
204-
let descriptor = FetchDescriptor<DjangoFilesSession>()
205-
206-
Task {
207-
do {
208-
let existingSessions = try context.fetch(descriptor)
209-
if let existingSession = existingSessions.first(where: { $0.url == serverURL.absoluteString }) {
210-
// If session exists, just select it and update UI
211-
await MainActor.run {
212-
sessionManager.selectedSession = existingSession
213-
hasExistingSessions = true
214-
ToastManager.shared.showToast(message: "Connected to existing server \(existingSession.url)")
215-
}
216-
return
217-
}
218-
219-
// No existing session, show confirmation dialog
220-
await MainActor.run {
221-
pendingAuthURL = serverURL
222-
pendingAuthSignature = signature
223-
showingServerConfirmation = true
224-
}
225-
} catch {
226-
print("Error checking for existing sessions: \(error)")
227-
}
228-
}
229-
}
230-
231-
private func handleServerConfirmation(confirmed: Bool, setAsDefault: Bool) async {
232-
guard let serverURL = pendingAuthURL,
233-
let signature = pendingAuthSignature else {
234-
return
235-
}
236-
237-
// If user cancelled, just clear the pending data and return
238-
if !confirmed {
239-
pendingAuthURL = nil
240-
pendingAuthSignature = nil
241-
return
242-
}
243-
244-
await MainActor.run {
245-
// Create and authenticate the new session
246-
let context = sharedModelContainer.mainContext
247-
248-
do {
249-
let descriptor = FetchDescriptor<DjangoFilesSession>()
250-
let existingSessions = try context.fetch(descriptor)
251-
252-
// Create and authenticate the new session
253-
Task {
254-
if let newSession = await sessionManager.createAndAuthenticateSession(
255-
url: serverURL,
256-
signature: signature,
257-
context: context
258-
) {
259-
if setAsDefault {
260-
// Reset all other sessions to not be default
261-
for session in existingSessions {
262-
session.defaultSession = false
263-
}
264-
newSession.defaultSession = true
265-
}
266-
sessionManager.selectedSession = newSession
267-
hasExistingSessions = true
268-
selectedTab = .files
269-
ToastManager.shared.showToast(message: "Successfully logged into \(newSession.url)")
270-
}
271-
}
272-
} catch {
273-
ToastManager.shared.showToast(message: "Problem signing into server \(error)")
274-
print("Error creating new session: \(error)")
275-
}
276-
277-
// Clear pending auth data
278-
pendingAuthURL = nil
279-
pendingAuthSignature = nil
280-
}
281-
}
282-
283198
private func checkDefaultServer() {
284199
let context = sharedModelContainer.mainContext
285200
let descriptor = FetchDescriptor<DjangoFilesSession>()
@@ -313,10 +228,10 @@ struct Django_FilesApp: App {
313228
if hasExistingSessions {
314229
checkDefaultServer()
315230
}
316-
isLoading = false // Set loading to false after check completes
231+
isLoading = false
317232
} catch {
318233
print("Error checking for existing sessions: \(error)")
319-
isLoading = false // Ensure we exit loading state even on error
234+
isLoading = false
320235
}
321236
}
322237
}

0 commit comments

Comments
 (0)