Skip to content

gif support and preview deep linking #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Django Files.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
4CE57E8B2D7C9F440073CFC1 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE57E8A2D7C9F440073CFC1 /* SnapshotHelper.swift */; };
A21CC8852DEB967300EF776C /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = A21CC8842DEB967300EF776C /* FirebaseAnalytics */; };
A21CC88B2DEB991100EF776C /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = A21CC88A2DEB991100EF776C /* FirebaseCrashlytics */; };
A22380612DFD33F000A544A0 /* FLAnimatedImage in Frameworks */ = {isa = PBXBuildFile; productRef = A22380602DFD33F000A544A0 /* FLAnimatedImage */; };
A2DF11D52DDA13FE0096E7C4 /* HighlightSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A2DF11D42DDA13FE0096E7C4 /* HighlightSwift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -135,6 +136,7 @@
A2DF11D52DDA13FE0096E7C4 /* HighlightSwift in Frameworks */,
4C82CB512D62372200C0893B /* HTTPTypes in Frameworks */,
A21CC88B2DEB991100EF776C /* FirebaseCrashlytics in Frameworks */,
A22380612DFD33F000A544A0 /* FLAnimatedImage in Frameworks */,
A21CC8852DEB967300EF776C /* FirebaseAnalytics in Frameworks */,
4C82CB532D62372200C0893B /* HTTPTypesFoundation in Frameworks */,
);
Expand Down Expand Up @@ -224,6 +226,7 @@
A2DF11D42DDA13FE0096E7C4 /* HighlightSwift */,
A21CC8842DEB967300EF776C /* FirebaseAnalytics */,
A21CC88A2DEB991100EF776C /* FirebaseCrashlytics */,
A22380602DFD33F000A544A0 /* FLAnimatedImage */,
);
productName = "Django Files";
productReference = 4C5E20EC2D603C3B009EE83A /* Django Files.app */;
Expand Down Expand Up @@ -339,6 +342,7 @@
4CE57E892D7C9EB70073CFC1 /* XCRemoteSwiftPackageReference "fastlane" */,
A2DF11D32DDA12CA0096E7C4 /* XCRemoteSwiftPackageReference "highlightswift" */,
A21CC8832DEB950C00EF776C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
A223805D2DFD335A00A544A0 /* XCRemoteSwiftPackageReference "FLAnimatedImage" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 4C5E20ED2D603C3B009EE83A /* Products */;
Expand Down Expand Up @@ -871,6 +875,14 @@
minimumVersion = 11.13.0;
};
};
A223805D2DFD335A00A544A0 /* XCRemoteSwiftPackageReference "FLAnimatedImage" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Flipboard/FLAnimatedImage.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.17;
};
};
A2DF11D32DDA12CA0096E7C4 /* XCRemoteSwiftPackageReference "highlightswift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/appstefan/highlightswift";
Expand Down Expand Up @@ -912,6 +924,11 @@
package = A21CC8832DEB950C00EF776C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseCrashlytics;
};
A22380602DFD33F000A544A0 /* FLAnimatedImage */ = {
isa = XCSwiftPackageProductDependency;
package = A223805D2DFD335A00A544A0 /* XCRemoteSwiftPackageReference "FLAnimatedImage" */;
productName = FLAnimatedImage;
};
A2DF11D42DDA13FE0096E7C4 /* HighlightSwift */ = {
isa = XCSwiftPackageProductDependency;
package = A2DF11D32DDA12CA0096E7C4 /* XCRemoteSwiftPackageReference "highlightswift" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Django Files/API/Files.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,17 @@ extension DFAPI {
return nil
}

public func getFileDetails(fileID: Int, selectedServer: DjangoFilesSession? = nil) async -> DFFile? {
public func getFileDetails(fileID: Int, password: String? = nil, selectedServer: DjangoFilesSession? = nil) async -> DFFile? {
do {
var parameters: [String: String] = [:]
if let password = password {
parameters["password"] = password
}

let responseBody = try await makeAPIRequest(
body: Data(),
path: getAPIPath(.file) + "\(fileID)",
parameters: [:],
parameters: parameters,
method: .get,
selectedServer: selectedServer
)
Expand Down
205 changes: 60 additions & 145 deletions Django Files/Django_FilesApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ import FirebaseCore
import FirebaseAnalytics
import FirebaseCrashlytics

class PreviewStateManager: ObservableObject {
@Published var deepLinkFile: DFFile?
@Published var showingDeepLinkPreview = false
@Published var deepLinkTargetFileID: Int? = nil
@Published var deepLinkFilePassword: String? = nil
}

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// Skip Firebase initialization if disabled via launch arguments
let shouldDisableFirebase = ProcessInfo.processInfo.arguments.contains("--DisableFirebase")
if !shouldDisableFirebase {
FirebaseApp.configure()
Expand All @@ -36,6 +42,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
@main
struct Django_FilesApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject private var previewStateManager = PreviewStateManager()
@State private var showFileInfo = false
var sharedModelContainer: ModelContainer = {
let schema = Schema([
DjangoFilesSession.self,
Expand Down Expand Up @@ -112,26 +120,72 @@ struct Django_FilesApp: App {
TabViewWindow(sessionManager: sessionManager, selectedTab: $selectedTab)
}
}
.environmentObject(previewStateManager)
.onOpenURL { url in
handleDeepLink(url)
DeepLinks.shared.handleDeepLink(
url,
context: sharedModelContainer.mainContext,
sessionManager: sessionManager,
previewStateManager: previewStateManager,
selectedTab: $selectedTab,
hasExistingSessions: $hasExistingSessions,
showingServerConfirmation: $showingServerConfirmation,
pendingAuthURL: $pendingAuthURL,
pendingAuthSignature: $pendingAuthSignature
)
}
.sheet(isPresented: $showingServerConfirmation) {
ServerConfirmationView(
serverURL: $pendingAuthURL,
signature: $pendingAuthSignature,
onConfirm: { setAsDefault in
Task {
await handleServerConfirmation(confirmed: true, setAsDefault: setAsDefault)
await DeepLinks.shared.handleServerConfirmation(
confirmed: true,
setAsDefault: setAsDefault,
pendingAuthURL: $pendingAuthURL,
pendingAuthSignature: $pendingAuthSignature,
context: sharedModelContainer.mainContext,
sessionManager: sessionManager,
hasExistingSessions: $hasExistingSessions,
selectedTab: $selectedTab
)
}
},
onCancel: {
Task {
await handleServerConfirmation(confirmed: false, setAsDefault: false)
await DeepLinks.shared.handleServerConfirmation(
confirmed: false,
setAsDefault: false,
pendingAuthURL: $pendingAuthURL,
pendingAuthSignature: $pendingAuthSignature,
context: sharedModelContainer.mainContext,
sessionManager: sessionManager,
hasExistingSessions: $hasExistingSessions,
selectedTab: $selectedTab
)
}
},
context: sharedModelContainer.mainContext
)
}
.fullScreenCover(isPresented: $previewStateManager.showingDeepLinkPreview) {
if let file = previewStateManager.deepLinkFile {
FilePreviewView(
file: .constant(file),
server: .constant(nil),
showingPreview: $previewStateManager.showingDeepLinkPreview,
showFileInfo: $showFileInfo,
fileListDelegate: nil,
allFiles: .constant([file]),
currentIndex: 0,
onNavigate: { _ in }
)
.onDisappear {
previewStateManager.deepLinkFile = nil
}
}
}
}
.modelContainer(sharedModelContainer)
#if os(macOS)
Expand All @@ -141,145 +195,6 @@ struct Django_FilesApp: App {
#endif
}

private func handleDeepLink(_ url: URL) {
print("Deep link received: \(url)")
guard url.scheme == "djangofiles" else { return }

// Extract the signature from the URL parameters
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
print("Invalid deep link URL")
return
}
print("Deep link host: \(components.host ?? "unknown")")
switch components.host {
case "authorize":
deepLinkAuth(components)
case "serverlist":
selectedTab = .settings
case "filelist":
handleFileListDeepLink(components)
default:
ToastManager.shared.showToast(message: "Unsupported deep link \(url)")
print("Unsupported deep link type: \(components.host ?? "unknown")")
}
}

private func handleFileListDeepLink(_ components: URLComponents) {
guard let urlString = components.queryItems?.first(where: { $0.name == "url" })?.value?.removingPercentEncoding,
let serverURL = URL(string: urlString) else {
print("Invalid server URL in filelist deep link")
return
}

// Find the session with matching URL and select it
let context = sharedModelContainer.mainContext
let descriptor = FetchDescriptor<DjangoFilesSession>()

Task {
do {
let existingSessions = try context.fetch(descriptor)
if let matchingSession = existingSessions.first(where: { $0.url == serverURL.absoluteString }) {
await MainActor.run {
sessionManager.selectedSession = matchingSession
selectedTab = .files
}
} else {
print("No session found for URL: \(serverURL.absoluteString)")
}
} catch {
print("Error fetching sessions: \(error)")
}
}
}

private func deepLinkAuth(_ components: URLComponents) {
guard let signature = components.queryItems?.first(where: { $0.name == "signature" })?.value?.removingPercentEncoding,
let serverURL = URL(string: components.queryItems?.first(where: { $0.name == "url" })?.value?.removingPercentEncoding ?? "") else {
print("Unable to parse auth deep link.")
return
}

// Check if a session with this URL already exists
let context = sharedModelContainer.mainContext
let descriptor = FetchDescriptor<DjangoFilesSession>()

Task {
do {
let existingSessions = try context.fetch(descriptor)
if let existingSession = existingSessions.first(where: { $0.url == serverURL.absoluteString }) {
// If session exists, just select it and update UI
await MainActor.run {
sessionManager.selectedSession = existingSession
hasExistingSessions = true
ToastManager.shared.showToast(message: "Connected to existing server \(existingSession.url)")
}
return
}

// No existing session, show confirmation dialog
await MainActor.run {
pendingAuthURL = serverURL
pendingAuthSignature = signature
showingServerConfirmation = true
}
} catch {
print("Error checking for existing sessions: \(error)")
}
}
}

private func handleServerConfirmation(confirmed: Bool, setAsDefault: Bool) async {
guard let serverURL = pendingAuthURL,
let signature = pendingAuthSignature else {
return
}

// If user cancelled, just clear the pending data and return
if !confirmed {
pendingAuthURL = nil
pendingAuthSignature = nil
return
}

await MainActor.run {
// Create and authenticate the new session
let context = sharedModelContainer.mainContext

do {
let descriptor = FetchDescriptor<DjangoFilesSession>()
let existingSessions = try context.fetch(descriptor)

// Create and authenticate the new session
Task {
if let newSession = await sessionManager.createAndAuthenticateSession(
url: serverURL,
signature: signature,
context: context
) {
if setAsDefault {
// Reset all other sessions to not be default
for session in existingSessions {
session.defaultSession = false
}
newSession.defaultSession = true
}
sessionManager.selectedSession = newSession
hasExistingSessions = true
selectedTab = .files
ToastManager.shared.showToast(message: "Successfully logged into \(newSession.url)")
}
}
} catch {
ToastManager.shared.showToast(message: "Problem signing into server \(error)")
print("Error creating new session: \(error)")
}

// Clear pending auth data
pendingAuthURL = nil
pendingAuthSignature = nil
}
}

private func checkDefaultServer() {
let context = sharedModelContainer.mainContext
let descriptor = FetchDescriptor<DjangoFilesSession>()
Expand Down Expand Up @@ -313,10 +228,10 @@ struct Django_FilesApp: App {
if hasExistingSessions {
checkDefaultServer()
}
isLoading = false // Set loading to false after check completes
isLoading = false
} catch {
print("Error checking for existing sessions: \(error)")
isLoading = false // Ensure we exit loading state even on error
isLoading = false
}
}
}
Loading
Loading