Skip to content

Native SwiftUI Interface for most functions #34

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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
8 changes: 6 additions & 2 deletions Django Files.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,15 @@
4CA2E47D2D6D22CA006EF3F0 /* Exceptions for "Django Files" folder in "UploadAndCopy" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
API/Albums.swift,
API/DFAPI.swift,
API/Error.swift,
API/Files.swift,
API/Gallery.swift,
API/Short.swift,
API/Stats.swift,
API/Upload.swift,
API/Websocket.swift,
Models/DjangoFilesSession.swift,
);
target = 4C82CB7E2D624E8700C0893B /* UploadAndCopy */;
Expand Down Expand Up @@ -445,7 +449,7 @@
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This lets you save or upload photos to your Django Files";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down Expand Up @@ -491,7 +495,7 @@
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This lets you save or upload photos to your Django Files";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "92f8ebe1937b4256bd2ad3830b28e1a8d86ccbc17fb04b921574cc4f6e156702",
"originHash" : "8a862c5942fae692f106a0e41c0fdd2a42f554e2bc2075d972093519c0e5fc16",
"pins" : [
{
"identity" : "fastlane",
Expand Down
82 changes: 82 additions & 0 deletions Django Files/API/Albums.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// Albums.swift
// Django Files
//
// Created by Ralph Luaces on 4/29/25.
//

import Foundation

// Album model that matches the JSON payload
struct DFAlbum: Identifiable, Decodable, Hashable {
let id: Int
let user: Int
let name: String
let password: String
let `private`: Bool
let info: String
let view: Int
let maxv: Int
let expr: String
let date: String
let url: String

// Format date for display
func formattedDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"

if let date = dateFormatter.date(from: date) {
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .medium
displayFormatter.timeStyle = .short
return displayFormatter.string(from: date)
}

return date
}
}

// Response structure for album API call
struct AlbumsResponse: Decodable {
let albums: [DFAlbum]
let next: Int?
let count: Int
}

extension DFAPI {
// Fetch albums with pagination
func getAlbums(page: Int = 1) async -> AlbumsResponse? {
guard var components = URLComponents(string: "\(url)/api/albums/") else {
return nil
}

components.queryItems = [URLQueryItem(name: "page", value: "\(page)")]

guard let requestURL = components.url else {
return nil
}

var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")

do {
let (data, response) = try await URLSession.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return nil
}

let decoder = JSONDecoder()
let albumsResponse = try decoder.decode(AlbumsResponse.self, from: data)
return albumsResponse

} catch {
print("Error fetching albums: \(error)")
return nil
}
}
}

82 changes: 81 additions & 1 deletion Django Files/API/DFAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,36 @@
import Foundation
import HTTPTypes
import HTTPTypesFoundation
import UIKit

// Custom imports
import SwiftUI // Needed for ToastManager

// Add an import for the models file
// This line should be modified if the module structure is different
// Or the models should be declared here if needed

struct DFAPI {
private static let API_PATH = "/api/"

// Add a shared WebSocket instance
private static var sharedWebSocket: DFWebSocket?

enum DjangoFilesAPIs: String {
case stats = "stats/"
case upload = "upload/"
case short = "shorten/"
case auth_methods = "auth/methods/"
case login = "auth/token/"
case files = "files/"
case shorts = "shorts/"
}

let url: URL
let token: String
var decoder: JSONDecoder


init(url: URL, token: String){
self.url = url
self.token = token
Expand Down Expand Up @@ -159,6 +173,28 @@ struct DFAPI {
}
}

public func getShorts(amount: Int = 50, start: Int? = nil) async -> ShortsResponse? {
var parameters: [String: String] = ["amount": "\(amount)"]
if let start = start {
parameters["start"] = "\(start)"
}

do {
let responseBody = try await makeAPIRequest(
path: getAPIPath(.shorts),
parameters: parameters,
method: .get
)

let shorts = try decoder.decode([DFShort].self, from: responseBody)
return ShortsResponse(shorts: shorts)

} catch {
print("Error fetching shorts: \(error)")
return nil
}
}

public func getAuthMethods() async -> DFAuthMethodsResponse? {
do {
let responseBody = try await makeAPIRequest(
Expand Down Expand Up @@ -248,6 +284,7 @@ struct DFAPI {
if let url = urlRequest.url {
// Set the cookie directly in the request header
urlRequest.setValue("sessionid=\(sessionKey)", forHTTPHeaderField: "Cookie")
print("Using session key cookie: \(sessionKey) on \(url)")

// Also set it in the cookie storage
let cookieProperties: [HTTPCookiePropertyKey: Any] = [
Expand Down Expand Up @@ -279,6 +316,8 @@ struct DFAPI {
let session = URLSession(configuration: configuration)
let (_, response) = try await session.data(for: urlRequest)

print("response: \(response)")

guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
print("Request failed with status: \((response as? HTTPURLResponse)?.statusCode ?? -1)")
Expand Down Expand Up @@ -321,9 +360,50 @@ struct DFAPI {
return false
}
}
}

public func getFiles(page: Int = 1) async -> DFFilesResponse? {
do {
let responseBody = try await makeAPIRequest(
path: getAPIPath(.files) + "\(page)/",
parameters: [:],
method: .get
)

// Use the default decoder since dates are now handled as strings
let specialDecoder = JSONDecoder()
specialDecoder.keyDecodingStrategy = .convertFromSnakeCase
return try specialDecoder.decode(DFFilesResponse.self, from: responseBody)
} catch let DecodingError.keyNotFound(key, context) {
print("Missing key: \(key.stringValue) in context: \(context.debugDescription)")
} catch {
print("Request failed \(error)")
}
return nil
}

// Create and connect to a WebSocket, also setting up WebSocketToastObserver
public func connectToWebSocket() -> DFWebSocket {
let webSocket = self.createWebSocket()

// Instead of directly accessing WebSocketToastObserver, post a notification
// that the observer will pick up
NotificationCenter.default.post(
name: Notification.Name("DFWebSocketConnectionRequest"),
object: nil,
userInfo: ["api": self]
)

// Store as the shared instance
DFAPI.sharedWebSocket = webSocket

return webSocket
}

// Get the shared WebSocket or create a new one if none exists
public static func getSharedWebSocket() -> DFWebSocket? {
return sharedWebSocket
}
}

class DjangoFilesUploadDelegate: NSObject, StreamDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate, URLSessionStreamDelegate{
enum States {
Expand Down
85 changes: 85 additions & 0 deletions Django Files/API/Files.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// Files.swift
// Django Files
//
// Created by Ralph Luaces on 4/23/25.
//

import Foundation

public struct DFFile: Codable, Hashable, Equatable {
public let id: Int
public let user: Int
public let size: Int
public let mime: String
public let name: String
public let userName: String? = ""
public let userUsername: String? = ""
public let info: String
public let expr: String
public let view: Int
public let maxv: Int
public let password: String
public let `private`: Bool
public let avatar: Bool
public let url: String
public let thumb: String
public let raw: String
public let date: String
public let albums: [Int]

// Skip nested JSON structures
enum CodingKeys: String, CodingKey {
case id, user, size, mime, name, info, expr, view, maxv, password, `private`, avatar, userName, userUsername, url, thumb, raw, date, albums
}

// Helper property to get a Date object when needed
public var dateObject: Date? {
let iso8601Formatter = ISO8601DateFormatter()
iso8601Formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

if let date = iso8601Formatter.date(from: date) {
return date
}

// Fall back to other formatters if needed
let backupFormatter = DateFormatter()
backupFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
backupFormatter.locale = Locale(identifier: "en_US_POSIX")
backupFormatter.timeZone = TimeZone(secondsFromGMT: 0)

if let date = backupFormatter.date(from: date) {
return date
}

return nil
}

// Format the date string for display
public func formattedDate() -> String {
guard let date = dateObject else {
return date // Return the raw string if we can't parse it
}

let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}

// Add hash implementation for Hashable conformance
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

// Add equality implementation for Equatable conformance
public static func == (lhs: DFFile, rhs: DFFile) -> Bool {
return lhs.id == rhs.id
}
}

public struct DFFilesResponse: Codable {
public let files: [DFFile]
public let next: Int?
public let count: Int
}
8 changes: 8 additions & 0 deletions Django Files/API/Gallery.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//
// Gallery.swift
// Django Files
//
// Created by Ralph Luaces on 4/29/25.
//

import Foundation
27 changes: 27 additions & 0 deletions Django Files/API/Short.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,30 @@ struct DFShortResponse: Codable{
url = try container.decode(String.self, forKey: .url)
}
}

struct DFShort: Identifiable, Codable, Hashable {
let id: Int
let short: String
let url: String
let max: Int
let views: Int
let user: Int
let fullUrl: String

enum CodingKeys: String, CodingKey {
case id, short, url, max, views, user, fullUrl
}
}

// Response structure for shorts API call
struct ShortsResponse: Codable {
let shorts: [DFShort]

init(shorts: [DFShort]) {
self.shorts = shorts
}

enum CodingKeys: String, CodingKey {
case shorts
}
}
Loading