From b35f47b56d0e1e2f6e7963dc1fba86a207fc32cb Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 10 Jul 2024 11:10:22 -0300 Subject: [PATCH 1/6] fix(storage): optional fields # Conflicts: # Sources/Storage/FileObject.swift --- Sources/Storage/Bucket.swift | 21 +++++++++++++-------- Sources/Storage/FileObject.swift | 28 ++++++++++++---------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Sources/Storage/Bucket.swift b/Sources/Storage/Bucket.swift index ccdd29e4..3437d254 100644 --- a/Sources/Storage/Bucket.swift +++ b/Sources/Storage/Bucket.swift @@ -3,17 +3,22 @@ import Foundation public struct Bucket: Identifiable, Hashable, Codable, Sendable { public var id: String public var name: String - public var owner: String - public var isPublic: Bool - public var createdAt: Date - public var updatedAt: Date + public var owner: String? + public var isPublic: Bool? + public var createdAt: Date? + public var updatedAt: Date? public var allowedMimeTypes: [String]? - public var fileSizeLimit: Int? + public var fileSizeLimit: Int64? public init( - id: String, name: String, owner: String, isPublic: Bool, createdAt: Date, updatedAt: Date, - allowedMimeTypes: [String]?, - fileSizeLimit: Int? + id: String, + name: String, + owner: String? = nil, + isPublic: Bool? = nil, + createdAt: Date? = nil, + updatedAt: Date? = nil, + allowedMimeTypes: [String]? = nil, + fileSizeLimit: Int64? = nil ) { self.id = id self.name = name diff --git a/Sources/Storage/FileObject.swift b/Sources/Storage/FileObject.swift index d9c102b0..00e35894 100644 --- a/Sources/Storage/FileObject.swift +++ b/Sources/Storage/FileObject.swift @@ -2,26 +2,24 @@ import Foundation import Helpers public struct FileObject: Identifiable, Hashable, Codable, Sendable { - public var name: String + public var name: String? public var bucketId: String? public var owner: String? - public var id: String - public var updatedAt: Date - public var createdAt: Date - public var lastAccessedAt: Date - public var metadata: [String: AnyJSON] - public var buckets: Bucket? + public var id: UUID + public var updatedAt: Date? + public var createdAt: Date? + public var lastAccessedAt: Date? + public var metadata: [String: AnyJSON]? public init( - name: String, + name: String? = nil, bucketId: String? = nil, owner: String? = nil, - id: String, - updatedAt: Date, - createdAt: Date, - lastAccessedAt: Date, - metadata: [String: AnyJSON], - buckets: Bucket? = nil + id: UUID, + updatedAt: Date? = nil, + createdAt: Date? = nil, + lastAccessedAt: Date? = nil, + metadata: [String: AnyJSON]? = nil ) { self.name = name self.bucketId = bucketId @@ -31,7 +29,6 @@ public struct FileObject: Identifiable, Hashable, Codable, Sendable { self.createdAt = createdAt self.lastAccessedAt = lastAccessedAt self.metadata = metadata - self.buckets = buckets } enum CodingKeys: String, CodingKey { @@ -43,6 +40,5 @@ public struct FileObject: Identifiable, Hashable, Codable, Sendable { case createdAt = "created_at" case lastAccessedAt = "last_accessed_at" case metadata - case buckets } } From 97888eb4fdc189f6c6fea26d680f10e94d966f1a Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 2 Jul 2024 16:19:51 -0300 Subject: [PATCH 2/6] docs: add SupaDrive sample app --- Examples/Examples.xcodeproj/project.pbxproj | 183 ++++++++++++++++- Examples/SupaDrive/AppView.swift | 172 ++++++++++++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 85 ++++++++ .../SupaDrive/Assets.xcassets/Contents.json | 6 + Examples/SupaDrive/AuthView.swift | 32 +++ .../Preview Assets.xcassets/Contents.json | 6 + Examples/SupaDrive/SupaDrive.entitlements | 12 ++ Examples/SupaDrive/SupaDriveApp.swift | 30 +++ Examples/SupaDrive/supabase/.gitignore | 4 + Examples/SupaDrive/supabase/config.toml | 194 ++++++++++++++++++ .../migrations/20240702175048_init.sql | 14 ++ Examples/SupaDrive/supabase/seed.sql | 0 13 files changed, 748 insertions(+), 1 deletion(-) create mode 100644 Examples/SupaDrive/AppView.swift create mode 100644 Examples/SupaDrive/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/SupaDrive/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/SupaDrive/Assets.xcassets/Contents.json create mode 100644 Examples/SupaDrive/AuthView.swift create mode 100644 Examples/SupaDrive/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Examples/SupaDrive/SupaDrive.entitlements create mode 100644 Examples/SupaDrive/SupaDriveApp.swift create mode 100644 Examples/SupaDrive/supabase/.gitignore create mode 100644 Examples/SupaDrive/supabase/config.toml create mode 100644 Examples/SupaDrive/supabase/migrations/20240702175048_init.sql create mode 100644 Examples/SupaDrive/supabase/seed.sql diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index eb1c1c6d..3988cd74 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 792404BA2C3454A9002959B3 /* SupaDriveApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792404B92C3454A9002959B3 /* SupaDriveApp.swift */; }; + 792404BE2C3454AA002959B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 792404BD2C3454AA002959B3 /* Assets.xcassets */; }; + 792404C22C3454AA002959B3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 792404C12C3454AA002959B3 /* Preview Assets.xcassets */; }; + 792404E32C3473EC002959B3 /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 792404E22C3473EC002959B3 /* Supabase */; }; + 792404E52C347466002959B3 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792404E42C347463002959B3 /* AppView.swift */; }; + 792404E72C348620002959B3 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792404E62C34861E002959B3 /* AuthView.swift */; }; 793895CA2954ABFF0044F2B8 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793895C92954ABFF0044F2B8 /* ExamplesApp.swift */; }; 793895CC2954ABFF0044F2B8 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793895CB2954ABFF0044F2B8 /* RootView.swift */; }; 793895CE2954AC000044F2B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 793895CD2954AC000044F2B8 /* Assets.xcassets */; }; @@ -76,6 +82,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 792404B72C3454A9002959B3 /* SupaDrive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SupaDrive.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 792404B92C3454A9002959B3 /* SupaDriveApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupaDriveApp.swift; sourceTree = ""; }; + 792404BD2C3454AA002959B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 792404BF2C3454AA002959B3 /* SupaDrive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SupaDrive.entitlements; sourceTree = ""; }; + 792404C12C3454AA002959B3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 792404E42C347463002959B3 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; + 792404E62C34861E002959B3 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = ""; }; 793895C62954ABFF0044F2B8 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 793895C92954ABFF0044F2B8 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; 793895CB2954ABFF0044F2B8 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; @@ -145,6 +158,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 792404B42C3454A9002959B3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 792404E32C3473EC002959B3 /* Supabase in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 793895C32954ABFF0044F2B8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -178,12 +199,34 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 792404B82C3454A9002959B3 /* SupaDrive */ = { + isa = PBXGroup; + children = ( + 792404E62C34861E002959B3 /* AuthView.swift */, + 792404E42C347463002959B3 /* AppView.swift */, + 792404B92C3454A9002959B3 /* SupaDriveApp.swift */, + 792404BD2C3454AA002959B3 /* Assets.xcassets */, + 792404BF2C3454AA002959B3 /* SupaDrive.entitlements */, + 792404C02C3454AA002959B3 /* Preview Content */, + ); + path = SupaDrive; + sourceTree = ""; + }; + 792404C02C3454AA002959B3 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 792404C12C3454AA002959B3 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; 793895BD2954ABFF0044F2B8 = { isa = PBXGroup; children = ( 793895C82954ABFF0044F2B8 /* Examples */, 79FEFFAD2B07873600D36347 /* UserManagement */, 79D884C82B3C18830009EA4A /* SlackClone */, + 792404B82C3454A9002959B3 /* SupaDrive */, 793895C72954ABFF0044F2B8 /* Products */, 7956405A2954AC3E0088A06F /* Frameworks */, ); @@ -195,6 +238,7 @@ 793895C62954ABFF0044F2B8 /* Examples.app */, 79FEFFAC2B07873600D36347 /* UserManagement.app */, 79D884C72B3C18830009EA4A /* SlackClone.app */, + 792404B72C3454A9002959B3 /* SupaDrive.app */, ); name = Products; sourceTree = ""; @@ -341,6 +385,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 792404B62C3454A9002959B3 /* SupaDrive */ = { + isa = PBXNativeTarget; + buildConfigurationList = 792404DF2C3454AA002959B3 /* Build configuration list for PBXNativeTarget "SupaDrive" */; + buildPhases = ( + 792404B32C3454A9002959B3 /* Sources */, + 792404B42C3454A9002959B3 /* Frameworks */, + 792404B52C3454A9002959B3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SupaDrive; + packageProductDependencies = ( + 792404E22C3473EC002959B3 /* Supabase */, + ); + productName = SupaDrive; + productReference = 792404B72C3454A9002959B3 /* SupaDrive.app */; + productType = "com.apple.product-type.application"; + }; 793895C52954ABFF0044F2B8 /* Examples */ = { isa = PBXNativeTarget; buildConfigurationList = 793895D52954AC000044F2B8 /* Build configuration list for PBXNativeTarget "Examples" */; @@ -414,9 +478,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1510; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1510; TargetAttributes = { + 792404B62C3454A9002959B3 = { + CreatedOnToolsVersion = 16.0; + }; 793895C52954ABFF0044F2B8 = { CreatedOnToolsVersion = 14.1; }; @@ -450,11 +517,21 @@ 793895C52954ABFF0044F2B8 /* Examples */, 79FEFFAB2B07873600D36347 /* UserManagement */, 79D884C62B3C18830009EA4A /* SlackClone */, + 792404B62C3454A9002959B3 /* SupaDrive */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 792404B52C3454A9002959B3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 792404C22C3454AA002959B3 /* Preview Assets.xcassets in Resources */, + 792404BE2C3454AA002959B3 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 793895C42954ABFF0044F2B8 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -485,6 +562,16 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 792404B32C3454A9002959B3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 792404BA2C3454A9002959B3 /* SupaDriveApp.swift in Sources */, + 792404E52C347466002959B3 /* AppView.swift in Sources */, + 792404E72C348620002959B3 /* AuthView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 793895C22954ABFF0044F2B8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -560,6 +647,87 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ + 792404D92C3454AA002959B3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SupaDrive/SupaDrive.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SupaDrive/Preview Content\""; + DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.supabase.SupaDrive; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.0; + }; + name = Debug; + }; + 792404DA2C3454AA002959B3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SupaDrive/SupaDrive.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SupaDrive/Preview Content\""; + DEVELOPMENT_TEAM = ELTTE7K8TT; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.supabase.SupaDrive; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.0; + }; + name = Release; + }; 793895D32954AC000044F2B8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -904,6 +1072,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 792404DF2C3454AA002959B3 /* Build configuration list for PBXNativeTarget "SupaDrive" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 792404D92C3454AA002959B3 /* Debug */, + 792404DA2C3454AA002959B3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 793895C12954ABFF0044F2B8 /* Build configuration list for PBXProject "Examples" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -978,6 +1155,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 792404E22C3473EC002959B3 /* Supabase */ = { + isa = XCSwiftPackageProductDependency; + productName = Supabase; + }; 7956406C2955B3500088A06F /* SwiftUINavigation */ = { isa = XCSwiftPackageProductDependency; package = 7956406B2955B3500088A06F /* XCRemoteSwiftPackageReference "swiftui-navigation" */; diff --git a/Examples/SupaDrive/AppView.swift b/Examples/SupaDrive/AppView.swift new file mode 100644 index 00000000..44dbb9b7 --- /dev/null +++ b/Examples/SupaDrive/AppView.swift @@ -0,0 +1,172 @@ +// +// AppView.swift +// Examples +// +// Created by Guilherme Souza on 02/07/24. +// + +import Supabase +import SwiftUI + +enum Item: Identifiable, Hashable { + case folder(Folder) + case file(File) + + var id: String { + switch self { + case let .file(file): file.id + case let .folder(folder): folder.id + } + } + + var name: String { + switch self { + case let .file(file): file.name + case let .folder(folder): folder.name + } + } + + var isFolder: Bool { + if case .folder = self { return true } + return false + } + + var isFile: Bool { + if case .file = self { return true } + return false + } +} + +struct Folder: Identifiable, Hashable { + let id: String + let name: String + let items: [Item] +} + +struct File: Identifiable, Hashable { + let id: String + let name: String +} + +struct AppView: View { + @State var path: [String] + @State var selectedItemPerPath: [String: Item] = [:] + + @State var reload = UUID() + + var body: some View { + ScrollView(.horizontal) { + HStack { + ForEach(path.indices, id: \.self) { pathIndex in + PanelView( + path: path[0 ... pathIndex].joined(separator: "/"), + selectedItem: Binding( + get: { + selectedItemPerPath[path[pathIndex]] + }, + set: { newValue in + selectedItemPerPath[path[pathIndex]] = newValue + + if case let .folder(folder) = newValue { + path.replaceSubrange((pathIndex + 1)..., with: [folder.name]) + } else { + path.replaceSubrange((pathIndex + 1)..., with: []) + } + } + ) + ) + .frame(width: 200) + } + } + } + .overlay(alignment: .trailing) { + if + let lastPath = path.last, + let selectedItem = selectedItemPerPath[lastPath], + case let .file(file) = selectedItem + { + ScrollView { + VStack { + Text(file.name) + } + .frame(width: 200) + .padding() + } + .background(Color.secondary) + } + } + } +} + +struct PanelView: View { + var path: String + @Binding var selectedItem: Item? + + @State private var isDraggingOver = false + @State private var items: [Item] = [] + + @State private var reload = UUID() + + var body: some View { + List { + Section(path) { + ForEach(items) { item in + Button { + selectedItem = item + } label: { + Text(item.name) + .background(selectedItem == item ? Color.blue : Color.clear) + } + } + } + .buttonStyle(.plain) + } + .task(id: reload) { + do { + let files = try await supabase.storage.from("main").list(path: path) + + items = files.map(Item.init) + } catch { + dump(error) + } + } + .onDrop(of: [.fileURL], isTargeted: $isDraggingOver) { providers in + for provider in providers { + _ = provider.loadDataRepresentation(for: .fileURL) { data, _ in + guard let data, let url = URL(dataRepresentation: data, relativeTo: nil) else { + return + } + + Task { + let path = url.lastPathComponent + let file = try! Data(contentsOf: url) + try! await supabase.storage.from("main") + .upload(path: "\(self.path)/\(path)", file: file) + + reload = UUID() + } + } + } + return true + } + .overlay { + if isDraggingOver { + Color.gray.opacity(0.2) + } + } + } +} + +extension Item { + init(file: FileObject) { + if file.id == nil { + self = .folder(Folder(id: file.name, name: file.name, items: [])) + } else { + self = .file(File(id: file.id!, name: file.name)) + } + } +} + +#Preview { + AppView(path: []) +} diff --git a/Examples/SupaDrive/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/SupaDrive/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Examples/SupaDrive/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SupaDrive/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/SupaDrive/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..ffdfe150 --- /dev/null +++ b/Examples/SupaDrive/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,85 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SupaDrive/Assets.xcassets/Contents.json b/Examples/SupaDrive/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Examples/SupaDrive/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SupaDrive/AuthView.swift b/Examples/SupaDrive/AuthView.swift new file mode 100644 index 00000000..1d18304a --- /dev/null +++ b/Examples/SupaDrive/AuthView.swift @@ -0,0 +1,32 @@ +// +// AuthView.swift +// Examples +// +// Created by Guilherme Souza on 02/07/24. +// + +import SwiftUI + +struct AuthView: View { + @State var userId: String? + + var body: some View { + Group { + if let userId { + AppView(path: [userId.lowercased()]) + } else { + ProgressView() + .task { + do { + userId = try? await supabase.auth.session.user.id.uuidString + if userId == nil { + userId = try await supabase.auth.signIn(email: "admin@supabase.io", password: "The.pass@00!").user.id.uuidString + } + } catch { + dump(error) + } + } + } + } + } +} diff --git a/Examples/SupaDrive/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/SupaDrive/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Examples/SupaDrive/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/SupaDrive/SupaDrive.entitlements b/Examples/SupaDrive/SupaDrive.entitlements new file mode 100644 index 00000000..625af03d --- /dev/null +++ b/Examples/SupaDrive/SupaDrive.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/Examples/SupaDrive/SupaDriveApp.swift b/Examples/SupaDrive/SupaDriveApp.swift new file mode 100644 index 00000000..6c564f74 --- /dev/null +++ b/Examples/SupaDrive/SupaDriveApp.swift @@ -0,0 +1,30 @@ +// +// SupaDriveApp.swift +// SupaDrive +// +// Created by Guilherme Souza on 02/07/24. +// + +import Supabase +import SwiftUI + +let supabase = SupabaseClient( + supabaseURL: URL(string: "http://127.0.0.1:54321")!, + supabaseKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0", + options: SupabaseClientOptions(global: .init(logger: AppLogger())) +) + +struct AppLogger: SupabaseLogger { + func log(message: SupabaseLogMessage) { + print(message.description) + } +} + +@main +struct SupaDriveApp: App { + var body: some Scene { + WindowGroup { + AuthView() + } + } +} diff --git a/Examples/SupaDrive/supabase/.gitignore b/Examples/SupaDrive/supabase/.gitignore new file mode 100644 index 00000000..a3ad8805 --- /dev/null +++ b/Examples/SupaDrive/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/Examples/SupaDrive/supabase/config.toml b/Examples/SupaDrive/supabase/config.toml new file mode 100644 index 00000000..22216e46 --- /dev/null +++ b/Examples/SupaDrive/supabase/config.toml @@ -0,0 +1,194 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "SupaDrive" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` is always included. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. `public` is always included. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 15 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +[storage.image_transformation] +enabled = true + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" + +# Use a production-ready SMTP server +# [auth.email.smtp] +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = true +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }} ." +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false + +[edge_runtime] +enabled = true +# Configure one of the supported request policies: `oneshot`, `per_worker`. +# Use `oneshot` for hot reload, or `per_worker` for load testing. +policy = "oneshot" +inspector_port = 8083 + +[analytics] +enabled = false +port = 54327 +vector_port = 54328 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/Examples/SupaDrive/supabase/migrations/20240702175048_init.sql b/Examples/SupaDrive/supabase/migrations/20240702175048_init.sql new file mode 100644 index 00000000..e3c1b7d3 --- /dev/null +++ b/Examples/SupaDrive/supabase/migrations/20240702175048_init.sql @@ -0,0 +1,14 @@ +INSERT INTO storage.buckets(id, name) + VALUES ('main', 'main'); + +CREATE POLICY "Allow authenticated access to own folder" ON storage.objects + FOR ALL TO authenticated + USING (bucket_id = 'main' + AND (storage.foldername(name))[1] =( + SELECT + auth.uid()::text)) + WITH CHECK (bucket_id = 'main' + AND (storage.foldername(name))[1] =( + SELECT + auth.uid()::text)); + diff --git a/Examples/SupaDrive/supabase/seed.sql b/Examples/SupaDrive/supabase/seed.sql new file mode 100644 index 00000000..e69de29b From 0195728d037eb2e8858d26603811358a175a6dad Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 9 Jul 2024 14:37:35 -0300 Subject: [PATCH 3/6] wip --- Examples/Examples.xcodeproj/project.pbxproj | 17 +++++ Examples/SupaDrive/AppView.swift | 78 +++++++++++++++------ Examples/SupaDrive/AuthView.swift | 59 ++++++++++++---- Examples/SupaDrive/SupaDriveApp.swift | 4 +- 4 files changed, 122 insertions(+), 36 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 3988cd74..977e7e9a 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 7956406A2955AFBD0088A06F /* ErrorText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795640692955AFBD0088A06F /* ErrorText.swift */; }; 7956406D2955B3500088A06F /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = 7956406C2955B3500088A06F /* SwiftUINavigation */; }; 795640702955B5190088A06F /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 7956406F2955B5190088A06F /* IdentifiedCollections */; }; + 79615DF92C3DA92C005AE6E0 /* CustomDump in Frameworks */ = {isa = PBXBuildFile; productRef = 79615DF82C3DA92C005AE6E0 /* CustomDump */; }; 796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796298982AEBBA77000AA957 /* MFAFlow.swift */; }; 7962989D2AEBC6F9000AA957 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = 7962989C2AEBC6F9000AA957 /* SVGView */; }; 79719ECE2ADF26C400737804 /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 79719ECD2ADF26C400737804 /* Supabase */; }; @@ -163,6 +164,7 @@ buildActionMask = 2147483647; files = ( 792404E32C3473EC002959B3 /* Supabase in Frameworks */, + 79615DF92C3DA92C005AE6E0 /* CustomDump in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -400,6 +402,7 @@ name = SupaDrive; packageProductDependencies = ( 792404E22C3473EC002959B3 /* Supabase */, + 79615DF82C3DA92C005AE6E0 /* CustomDump */, ); productName = SupaDrive; productReference = 792404B72C3454A9002959B3 /* SupaDrive.app */; @@ -509,6 +512,7 @@ 7956406E2955B5190088A06F /* XCRemoteSwiftPackageReference "swift-identified-collections" */, 7962989B2AEBC6F9000AA957 /* XCRemoteSwiftPackageReference "SVGView" */, 79E2B5562B97890F0042CD21 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + 79615DF72C3DA92C005AE6E0 /* XCRemoteSwiftPackageReference "swift-custom-dump" */, ); productRefGroup = 793895C72954ABFF0044F2B8 /* Products */; projectDirPath = ""; @@ -1136,6 +1140,14 @@ minimumVersion = 1.0.0; }; }; + 79615DF72C3DA92C005AE6E0 /* XCRemoteSwiftPackageReference "swift-custom-dump" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-custom-dump"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; 7962989B2AEBC6F9000AA957 /* XCRemoteSwiftPackageReference "SVGView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/exyte/SVGView"; @@ -1169,6 +1181,11 @@ package = 7956406E2955B5190088A06F /* XCRemoteSwiftPackageReference "swift-identified-collections" */; productName = IdentifiedCollections; }; + 79615DF82C3DA92C005AE6E0 /* CustomDump */ = { + isa = XCSwiftPackageProductDependency; + package = 79615DF72C3DA92C005AE6E0 /* XCRemoteSwiftPackageReference "swift-custom-dump" */; + productName = CustomDump; + }; 7962989C2AEBC6F9000AA957 /* SVGView */ = { isa = XCSwiftPackageProductDependency; package = 7962989B2AEBC6F9000AA957 /* XCRemoteSwiftPackageReference "SVGView" */; diff --git a/Examples/SupaDrive/AppView.swift b/Examples/SupaDrive/AppView.swift index 44dbb9b7..254ee5a8 100644 --- a/Examples/SupaDrive/AppView.swift +++ b/Examples/SupaDrive/AppView.swift @@ -5,14 +5,15 @@ // Created by Guilherme Souza on 02/07/24. // +import CustomDump import Supabase import SwiftUI enum Item: Identifiable, Hashable { - case folder(Folder) - case file(File) + case folder(FileObject) + case file(FileObject) - var id: String { + var id: String? { switch self { case let .file(file): file.id case let .folder(folder): folder.id @@ -37,16 +38,17 @@ enum Item: Identifiable, Hashable { } } -struct Folder: Identifiable, Hashable { - let id: String - let name: String - let items: [Item] -} - -struct File: Identifiable, Hashable { - let id: String - let name: String -} +// +// struct Folder: Identifiable, Hashable { +// let id: String +// let name: String +// let items: [Item] +// } +// +// struct File: Identifiable, Hashable { +// let id: String +// let name: String +// } struct AppView: View { @State var path: [String] @@ -85,16 +87,27 @@ struct AppView: View { let selectedItem = selectedItemPerPath[lastPath], case let .file(file) = selectedItem { - ScrollView { - VStack { - Text(file.name) + Form { + Text(file.name) + .font(.title2) + Divider() + + if let contentLenth = file.metadata?["contentLength"]?.intValue { + LabeledContent("Size", value: "\(contentLenth)") + } + + if let mimeType = file.metadata?["mimetype"]?.stringValue { + LabeledContent("MIME Type", value: mimeType) } - .frame(width: 200) - .padding() } - .background(Color.secondary) + .frame(width: 300) + .frame(maxHeight: .infinity, alignment: .top) + .padding() + .background(Color(NSColor.controlBackgroundColor)) + .transition(.move(edge: .trailing)) } } + .animation(.default, value: path.last) } } @@ -125,7 +138,7 @@ struct PanelView: View { do { let files = try await supabase.storage.from("main").list(path: path) - items = files.map(Item.init) + items = files.compactMap(Item.init) } catch { dump(error) } @@ -154,19 +167,38 @@ struct PanelView: View { Color.gray.opacity(0.2) } } + .contextMenu { + Button("New folder") { + Task { + try! await supabase.storage.from("main") + .upload(path: "\(path)/Untiltled/.dummy", file: Data()) + reload = UUID() + } + } + } } } extension Item { - init(file: FileObject) { + init?(file: FileObject) { + if file.name.hasPrefix(".") { return nil } + if file.id == nil { - self = .folder(Folder(id: file.name, name: file.name, items: [])) + self = .folder(file) } else { - self = .file(File(id: file.id!, name: file.name)) + self = .file(file) } } } +extension FileObject { + var metadataDump: String { + var output = "" + customDump(metadata, to: &output) + return output + } +} + #Preview { AppView(path: []) } diff --git a/Examples/SupaDrive/AuthView.swift b/Examples/SupaDrive/AuthView.swift index 1d18304a..41b58328 100644 --- a/Examples/SupaDrive/AuthView.swift +++ b/Examples/SupaDrive/AuthView.swift @@ -5,28 +5,63 @@ // Created by Guilherme Souza on 02/07/24. // +import Supabase import SwiftUI -struct AuthView: View { - @State var userId: String? +struct AuthView: View { + @ViewBuilder var content: (Session) -> Content + + @State var session: Session? var body: some View { Group { - if let userId { - AppView(path: [userId.lowercased()]) + if let session { + content(session) + .environment(\.supabaseSession, session) } else { - ProgressView() - .task { - do { - userId = try? await supabase.auth.session.user.id.uuidString - if userId == nil { - userId = try await supabase.auth.signIn(email: "admin@supabase.io", password: "The.pass@00!").user.id.uuidString + LoginView() + } + } + .task { + for await (_, session) in supabase.auth.authStateChanges { + self.session = session + } + } + } + + struct LoginView: View { + @State var email = "" + @State var password = "" + + var body: some View { + Form { + Section { + TextField("Email", text: $email) + SecureField("Password", text: $password) + } + Section { + Button("Sign in") { + Task { + do { + try await supabase.auth.signIn(email: email, password: password) + } catch { + try await supabase.auth.signUp(email: email, password: password) } - } catch { - dump(error) } } + } } } } } + +enum SupabaseSesstionEnvironmentKey: EnvironmentKey { + static var defaultValue: Session? +} + +extension EnvironmentValues { + var supabaseSession: Session? { + get { self[SupabaseSesstionEnvironmentKey.self] } + set { self[SupabaseSesstionEnvironmentKey.self] = newValue } + } +} diff --git a/Examples/SupaDrive/SupaDriveApp.swift b/Examples/SupaDrive/SupaDriveApp.swift index 6c564f74..2f99c62f 100644 --- a/Examples/SupaDrive/SupaDriveApp.swift +++ b/Examples/SupaDrive/SupaDriveApp.swift @@ -24,7 +24,9 @@ struct AppLogger: SupabaseLogger { struct SupaDriveApp: App { var body: some Scene { WindowGroup { - AuthView() + AuthView { session in + AppView(path: [session.user.id.uuidString.lowercased()]) + } } } } From b8b0c60d9aae4ce0b8f2ca6ef1fff307406ca1a1 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 15 Jul 2024 09:10:55 -0300 Subject: [PATCH 4/6] add breadcrumb --- Examples/SupaDrive/AppView.swift | 146 ++++++++++++------------------- Sources/Storage/FileObject.swift | 4 +- 2 files changed, 58 insertions(+), 92 deletions(-) diff --git a/Examples/SupaDrive/AppView.swift b/Examples/SupaDrive/AppView.swift index 254ee5a8..ac4d964d 100644 --- a/Examples/SupaDrive/AppView.swift +++ b/Examples/SupaDrive/AppView.swift @@ -9,75 +9,38 @@ import CustomDump import Supabase import SwiftUI -enum Item: Identifiable, Hashable { - case folder(FileObject) - case file(FileObject) - - var id: String? { - switch self { - case let .file(file): file.id - case let .folder(folder): folder.id - } - } - - var name: String { - switch self { - case let .file(file): file.name - case let .folder(folder): folder.name - } - } - - var isFolder: Bool { - if case .folder = self { return true } - return false - } - - var isFile: Bool { - if case .file = self { return true } - return false - } -} - -// -// struct Folder: Identifiable, Hashable { -// let id: String -// let name: String -// let items: [Item] -// } -// -// struct File: Identifiable, Hashable { -// let id: String -// let name: String -// } - struct AppView: View { @State var path: [String] - @State var selectedItemPerPath: [String: Item] = [:] + @State var selectedItemPerPath: [String: FileObject] = [:] @State var reload = UUID() var body: some View { - ScrollView(.horizontal) { - HStack { - ForEach(path.indices, id: \.self) { pathIndex in - PanelView( - path: path[0 ... pathIndex].joined(separator: "/"), - selectedItem: Binding( - get: { - selectedItemPerPath[path[pathIndex]] - }, - set: { newValue in - selectedItemPerPath[path[pathIndex]] = newValue - - if case let .folder(folder) = newValue { - path.replaceSubrange((pathIndex + 1)..., with: [folder.name]) - } else { - path.replaceSubrange((pathIndex + 1)..., with: []) + VStack(alignment: .leading, spacing: 0) { + breadcrump + + ScrollView(.horizontal) { + HStack { + ForEach(path.indices, id: \.self) { pathIndex in + PanelView( + path: path[0 ... pathIndex].joined(separator: "/"), + selectedItem: Binding( + get: { + selectedItemPerPath[path[pathIndex]] + }, + set: { newValue in + selectedItemPerPath[path[pathIndex]] = newValue + + if let newValue, let name = newValue.name, newValue.id == nil { + path.replaceSubrange((pathIndex + 1)..., with: [name]) + } else { + path.replaceSubrange((pathIndex + 1)..., with: []) + } } - } + ) ) - ) - .frame(width: 200) + .frame(width: 200) + } } } } @@ -85,18 +48,18 @@ struct AppView: View { if let lastPath = path.last, let selectedItem = selectedItemPerPath[lastPath], - case let .file(file) = selectedItem + selectedItem.id != nil { Form { - Text(file.name) + Text(selectedItem.name ?? "") .font(.title2) Divider() - if let contentLenth = file.metadata?["contentLength"]?.intValue { + if let contentLenth = selectedItem.metadata?["contentLength"]?.intValue { LabeledContent("Size", value: "\(contentLenth)") } - if let mimeType = file.metadata?["mimetype"]?.stringValue { + if let mimeType = selectedItem.metadata?["mimetype"]?.stringValue { LabeledContent("MIME Type", value: mimeType) } } @@ -107,29 +70,44 @@ struct AppView: View { .transition(.move(edge: .trailing)) } } - .animation(.default, value: path.last) + .animation(.default, value: path) + .animation(.default, value: selectedItemPerPath) + } + + var breadcrump: some View { + HStack { + ForEach(Array(zip(path.indices, path)), id: \.0) { idx, path in + Button(path) { + self.path.replaceSubrange((idx + 1)..., with: []) + } + .buttonStyle(.plain) + + if idx != self.path.indices.last { + Text(">") + } + } + } + .padding() } } struct PanelView: View { var path: String - @Binding var selectedItem: Item? + @Binding var selectedItem: FileObject? @State private var isDraggingOver = false - @State private var items: [Item] = [] + @State private var items: [FileObject] = [] @State private var reload = UUID() var body: some View { List { - Section(path) { - ForEach(items) { item in - Button { - selectedItem = item - } label: { - Text(item.name) - .background(selectedItem == item ? Color.blue : Color.clear) - } + ForEach(items) { item in + Button { + selectedItem = item + } label: { + Text(item.name ?? "") + .bold(selectedItem == item) } } .buttonStyle(.plain) @@ -138,7 +116,7 @@ struct PanelView: View { do { let files = try await supabase.storage.from("main").list(path: path) - items = files.compactMap(Item.init) + items = files.filter { $0.name?.hasPrefix(".") == false } } catch { dump(error) } @@ -150,7 +128,7 @@ struct PanelView: View { return } - Task { + Task { @MainActor in let path = url.lastPathComponent let file = try! Data(contentsOf: url) try! await supabase.storage.from("main") @@ -179,18 +157,6 @@ struct PanelView: View { } } -extension Item { - init?(file: FileObject) { - if file.name.hasPrefix(".") { return nil } - - if file.id == nil { - self = .folder(file) - } else { - self = .file(file) - } - } -} - extension FileObject { var metadataDump: String { var output = "" diff --git a/Sources/Storage/FileObject.swift b/Sources/Storage/FileObject.swift index 00e35894..03dce1f5 100644 --- a/Sources/Storage/FileObject.swift +++ b/Sources/Storage/FileObject.swift @@ -5,7 +5,7 @@ public struct FileObject: Identifiable, Hashable, Codable, Sendable { public var name: String? public var bucketId: String? public var owner: String? - public var id: UUID + public var id: UUID? public var updatedAt: Date? public var createdAt: Date? public var lastAccessedAt: Date? @@ -15,7 +15,7 @@ public struct FileObject: Identifiable, Hashable, Codable, Sendable { name: String? = nil, bucketId: String? = nil, owner: String? = nil, - id: UUID, + id: UUID?, updatedAt: Date? = nil, createdAt: Date? = nil, lastAccessedAt: Date? = nil, From dac21366a8870f24bbab522b7f72c8d7cbb8385b Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 15 Jul 2024 11:12:04 -0300 Subject: [PATCH 5/6] refactor --- Examples/Examples.xcodeproj/project.pbxproj | 8 + Examples/SupaDrive/AppView.swift | 233 +++++++++++++------- Examples/SupaDrive/SupaDriveApp.swift | 4 +- 3 files changed, 170 insertions(+), 75 deletions(-) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 977e7e9a..257d851d 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 79615DF92C3DA92C005AE6E0 /* CustomDump in Frameworks */ = {isa = PBXBuildFile; productRef = 79615DF82C3DA92C005AE6E0 /* CustomDump */; }; 796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796298982AEBBA77000AA957 /* MFAFlow.swift */; }; 7962989D2AEBC6F9000AA957 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = 7962989C2AEBC6F9000AA957 /* SVGView */; }; + 796B32BE2C4559E900DDD7B4 /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 796B32BD2C4559E900DDD7B4 /* IdentifiedCollections */; }; 79719ECE2ADF26C400737804 /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 79719ECD2ADF26C400737804 /* Supabase */; }; 797D664A2B46A1D8007592ED /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797D66492B46A1D8007592ED /* Dependencies.swift */; }; 797EFB662BABD82A00098D6B /* BucketList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797EFB652BABD82A00098D6B /* BucketList.swift */; }; @@ -163,6 +164,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 796B32BE2C4559E900DDD7B4 /* IdentifiedCollections in Frameworks */, 792404E32C3473EC002959B3 /* Supabase in Frameworks */, 79615DF92C3DA92C005AE6E0 /* CustomDump in Frameworks */, ); @@ -403,6 +405,7 @@ packageProductDependencies = ( 792404E22C3473EC002959B3 /* Supabase */, 79615DF82C3DA92C005AE6E0 /* CustomDump */, + 796B32BD2C4559E900DDD7B4 /* IdentifiedCollections */, ); productName = SupaDrive; productReference = 792404B72C3454A9002959B3 /* SupaDrive.app */; @@ -1191,6 +1194,11 @@ package = 7962989B2AEBC6F9000AA957 /* XCRemoteSwiftPackageReference "SVGView" */; productName = SVGView; }; + 796B32BD2C4559E900DDD7B4 /* IdentifiedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = 7956406E2955B5190088A06F /* XCRemoteSwiftPackageReference "swift-identified-collections" */; + productName = IdentifiedCollections; + }; 79719ECD2ADF26C400737804 /* Supabase */ = { isa = XCSwiftPackageProductDependency; productName = Supabase; diff --git a/Examples/SupaDrive/AppView.swift b/Examples/SupaDrive/AppView.swift index ac4d964d..9d35de13 100644 --- a/Examples/SupaDrive/AppView.swift +++ b/Examples/SupaDrive/AppView.swift @@ -8,12 +8,54 @@ import CustomDump import Supabase import SwiftUI +import IdentifiedCollections + +@MainActor +@Observable +final class AppModel { + var panels: IdentifiedArrayOf { + didSet { + bindPanelModels() + } + } -struct AppView: View { - @State var path: [String] - @State var selectedItemPerPath: [String: FileObject] = [:] + init(panels: IdentifiedArrayOf) { + self.panels = panels + bindPanelModels() + } + + var path: String { + panels.last?.path ?? "" + } + + var pathComponents: [String] { + path.components(separatedBy: "/") + } + + var selectedFile: FileObject? { + panels.last?.selectedItem + } + + private func bindPanelModels() { + for panel in panels { + panel.onSelectItem = { [weak self, weak panel] item in + guard let self, let panel else { return } + +// self.panels.append(PanelModel(path: self.path.appending)) +// +// if let name = item.name, item.id == nil { +// self.panels.replaceSubrange( +// (index + 1)..., +// with: [PanelModel(path: self.path.appending("/\(name)"))] +// ) +// } + } + } + } +} - @State var reload = UUID() +struct AppView: View { + @Bindable var model: AppModel var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -21,23 +63,25 @@ struct AppView: View { ScrollView(.horizontal) { HStack { - ForEach(path.indices, id: \.self) { pathIndex in + ForEach(model.panels) { panel in PanelView( - path: path[0 ... pathIndex].joined(separator: "/"), - selectedItem: Binding( - get: { - selectedItemPerPath[path[pathIndex]] - }, - set: { newValue in - selectedItemPerPath[path[pathIndex]] = newValue - - if let newValue, let name = newValue.name, newValue.id == nil { - path.replaceSubrange((pathIndex + 1)..., with: [name]) - } else { - path.replaceSubrange((pathIndex + 1)..., with: []) - } - } - ) + model: panel +// model: PanelModel(path: path[0 ... pathIndex].joined(separator: "/")) +// path: path[0 ... pathIndex].joined(separator: "/"), +// selectedItem: Binding( +// get: { +// selectedItemPerPath[path[pathIndex]] +// }, +// set: { newValue in +// selectedItemPerPath[path[pathIndex]] = newValue +// +// if let newValue, let name = newValue.name, newValue.id == nil { +// path.replaceSubrange((pathIndex + 1)..., with: [name]) +// } else { +// path.replaceSubrange((pathIndex + 1)..., with: []) +// } +// } +// ) ) .frame(width: 200) } @@ -45,21 +89,17 @@ struct AppView: View { } } .overlay(alignment: .trailing) { - if - let lastPath = path.last, - let selectedItem = selectedItemPerPath[lastPath], - selectedItem.id != nil - { + if let selectedFile = model.selectedFile { Form { - Text(selectedItem.name ?? "") + Text(selectedFile.name ?? "") .font(.title2) Divider() - if let contentLenth = selectedItem.metadata?["contentLength"]?.intValue { + if let contentLenth = selectedFile.metadata?["contentLength"]?.intValue { LabeledContent("Size", value: "\(contentLenth)") } - if let mimeType = selectedItem.metadata?["mimetype"]?.stringValue { + if let mimeType = selectedFile.metadata?["mimetype"]?.stringValue { LabeledContent("MIME Type", value: mimeType) } } @@ -70,56 +110,116 @@ struct AppView: View { .transition(.move(edge: .trailing)) } } - .animation(.default, value: path) - .animation(.default, value: selectedItemPerPath) + .animation(.default, value: model.path) + .animation(.default, value: model.selectedFile) } var breadcrump: some View { HStack { - ForEach(Array(zip(path.indices, path)), id: \.0) { idx, path in + ForEach(Array(zip(model.pathComponents.indices, model.pathComponents)), id: \.0) { idx, path in Button(path) { - self.path.replaceSubrange((idx + 1)..., with: []) +// self.path.replaceSubrange((idx + 1)..., with: []) } .buttonStyle(.plain) - if idx != self.path.indices.last { - Text(">") - } +// if idx != self.path.indices.last { +// Text(">") +// } } } .padding() } } +struct DragValue: Codable { + let path: String + let object: FileObject +} + +@MainActor +@Observable +final class PanelModel: Identifiable { + let path: String + var selectedItem: FileObject? + + var items: [FileObject] = [] + + @ObservationIgnored + var onSelectItem: @MainActor (FileObject) -> Void = { _ in } + + init(path: String) { + self.path = path + } + + func load() async { + do { + let files = try await supabase.storage.from("main").list(path: path) + items = files.filter { $0.name?.hasPrefix(".") == false } + } catch { + dump(error) + } + } + + func didSelectItem(_ item: FileObject) { + self.selectedItem = item + onSelectItem(item) + } + + func newFolderButtonTapped() async { + do { + try await supabase.storage.from("main") + .upload(path: "\(path)/Untiltled/.dummy", file: Data()) + } catch { + + } + } + + func uploadFile(at url: URL) async { + let path = url.lastPathComponent + + do { + let file = try Data(contentsOf: url) + try await supabase.storage.from("main") + .upload(path: "\(self.path)/\(path)", file: file) + } catch {} + } +} + struct PanelView: View { - var path: String - @Binding var selectedItem: FileObject? + @Bindable var model: PanelModel @State private var isDraggingOver = false - @State private var items: [FileObject] = [] - - @State private var reload = UUID() var body: some View { List { - ForEach(items) { item in - Button { - selectedItem = item - } label: { - Text(item.name ?? "") - .bold(selectedItem == item) + ForEach(model.items) { item in + Text(item.name ?? "") + .bold(model.selectedItem == item) + .onTapGesture { + model.didSelectItem(item) + } + .onDrag { + let data = try! JSONEncoder().encode(DragValue(path: model.path, object: item)) + let string = String(decoding: data, as: UTF8.self) + return NSItemProvider(object: string as NSString) + } + } + .onInsert(of: ["public.text"]) { index, items in + for item in items { + Task { + guard let data = try await item.loadItem(forTypeIdentifier: "public.text") as? Data, + let value = try? JSONDecoder().decode(DragValue.self, from: data) else { + return + } + + self.model.items.insert(value.object, at: index) + } } + print(index, items) } - .buttonStyle(.plain) } - .task(id: reload) { - do { - let files = try await supabase.storage.from("main").list(path: path) - - items = files.filter { $0.name?.hasPrefix(".") == false } - } catch { - dump(error) - } + .task { + await model.load() } .onDrop(of: [.fileURL], isTargeted: $isDraggingOver) { providers in for provider in providers { @@ -128,13 +228,8 @@ struct PanelView: View { return } - Task { @MainActor in - let path = url.lastPathComponent - let file = try! Data(contentsOf: url) - try! await supabase.storage.from("main") - .upload(path: "\(self.path)/\(path)", file: file) - - reload = UUID() + Task { + await model.uploadFile(at: url) } } } @@ -148,23 +243,13 @@ struct PanelView: View { .contextMenu { Button("New folder") { Task { - try! await supabase.storage.from("main") - .upload(path: "\(path)/Untiltled/.dummy", file: Data()) - reload = UUID() + await model.newFolderButtonTapped() } } } } } -extension FileObject { - var metadataDump: String { - var output = "" - customDump(metadata, to: &output) - return output - } -} - #Preview { - AppView(path: []) + AppView(model: AppModel(panels: [])) } diff --git a/Examples/SupaDrive/SupaDriveApp.swift b/Examples/SupaDrive/SupaDriveApp.swift index 2f99c62f..e948a928 100644 --- a/Examples/SupaDrive/SupaDriveApp.swift +++ b/Examples/SupaDrive/SupaDriveApp.swift @@ -25,7 +25,9 @@ struct SupaDriveApp: App { var body: some Scene { WindowGroup { AuthView { session in - AppView(path: [session.user.id.uuidString.lowercased()]) + AppView( + model: AppModel(panels: [PanelModel(path: session.user.id.uuidString.lowercased())]) + ) } } } From 272bac6a8cdd0df40ebd280d486fb4fbb89e2fce Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 23 Jul 2024 05:28:33 -0300 Subject: [PATCH 6/6] wip --- Examples/SupaDrive/AppView.swift | 176 ++++++++---------- Examples/SupaDrive/SupaDriveApp.swift | 20 +- .../supabase/.branches/_current_branch | 1 + .../supabase/.temp/cli-latest | 1 + 4 files changed, 97 insertions(+), 101 deletions(-) create mode 100644 Tests/IntegrationTests/supabase/.branches/_current_branch create mode 100644 Tests/IntegrationTests/supabase/.temp/cli-latest diff --git a/Examples/SupaDrive/AppView.swift b/Examples/SupaDrive/AppView.swift index 9d35de13..a025edaf 100644 --- a/Examples/SupaDrive/AppView.swift +++ b/Examples/SupaDrive/AppView.swift @@ -13,14 +13,16 @@ import IdentifiedCollections @MainActor @Observable final class AppModel { - var panels: IdentifiedArrayOf { + let root: PanelModel + + var panels: IdentifiedArrayOf = [] { didSet { bindPanelModels() } } - init(panels: IdentifiedArrayOf) { - self.panels = panels + init(root: PanelModel) { + self.root = root bindPanelModels() } @@ -33,22 +35,15 @@ final class AppModel { } var selectedFile: FileObject? { - panels.last?.selectedItem + nil// panels.last?.selectedItem } private func bindPanelModels() { - for panel in panels { + for panel in [root] + panels { panel.onSelectItem = { [weak self, weak panel] item in guard let self, let panel else { return } -// self.panels.append(PanelModel(path: self.path.appending)) -// -// if let name = item.name, item.id == nil { -// self.panels.replaceSubrange( -// (index + 1)..., -// with: [PanelModel(path: self.path.appending("/\(name)"))] -// ) -// } + self.panels.append(PanelModel(path: panel.path.appending("/\(item.name!)"))) } } } @@ -58,35 +53,11 @@ struct AppView: View { @Bindable var model: AppModel var body: some View { - VStack(alignment: .leading, spacing: 0) { - breadcrump - - ScrollView(.horizontal) { - HStack { - ForEach(model.panels) { panel in - PanelView( - model: panel -// model: PanelModel(path: path[0 ... pathIndex].joined(separator: "/")) -// path: path[0 ... pathIndex].joined(separator: "/"), -// selectedItem: Binding( -// get: { -// selectedItemPerPath[path[pathIndex]] -// }, -// set: { newValue in -// selectedItemPerPath[path[pathIndex]] = newValue -// -// if let newValue, let name = newValue.name, newValue.id == nil { -// path.replaceSubrange((pathIndex + 1)..., with: [name]) -// } else { -// path.replaceSubrange((pathIndex + 1)..., with: []) -// } -// } -// ) - ) - .frame(width: 200) - } + NavigationStack(path: $model.panels) { + PanelView(model: model.root) + .navigationDestination(for: PanelModel.self) { model in + PanelView(model: model) } - } } .overlay(alignment: .trailing) { if let selectedFile = model.selectedFile { @@ -110,24 +81,8 @@ struct AppView: View { .transition(.move(edge: .trailing)) } } - .animation(.default, value: model.path) - .animation(.default, value: model.selectedFile) - } - - var breadcrump: some View { - HStack { - ForEach(Array(zip(model.pathComponents.indices, model.pathComponents)), id: \.0) { idx, path in - Button(path) { -// self.path.replaceSubrange((idx + 1)..., with: []) - } - .buttonStyle(.plain) - -// if idx != self.path.indices.last { -// Text(">") -// } - } - } - .padding() +// .animation(.default, value: model.path) +// .animation(.default, value: model.selectedFile) } } @@ -138,9 +93,17 @@ struct DragValue: Codable { @MainActor @Observable -final class PanelModel: Identifiable { +final class PanelModel: Identifiable, Hashable { + nonisolated func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + nonisolated static func == (lhs: PanelModel, rhs: PanelModel) -> Bool { + lhs === rhs + } + let path: String - var selectedItem: FileObject? + var selectedItem: FileObject.ID? var items: [FileObject] = [] @@ -160,15 +123,21 @@ final class PanelModel: Identifiable { } } - func didSelectItem(_ item: FileObject) { - self.selectedItem = item + func onPrimaryAction(_ itemID: FileObject.ID) { + guard let item = items.first(where: { $0.id == itemID }) else { return } onSelectItem(item) } +// func didSelectItem(_ item: FileObject) { +// self.selectedItem = item +// onSelectItem(item) +// } + func newFolderButtonTapped() async { do { try await supabase.storage.from("main") .upload(path: "\(path)/Untiltled/.dummy", file: Data()) + await load() } catch { } @@ -181,6 +150,7 @@ final class PanelModel: Identifiable { let file = try Data(contentsOf: url) try await supabase.storage.from("main") .upload(path: "\(self.path)/\(path)", file: file) + await load() } catch {} } } @@ -191,33 +161,58 @@ struct PanelView: View { @State private var isDraggingOver = false var body: some View { - List { - ForEach(model.items) { item in - Text(item.name ?? "") - .bold(model.selectedItem == item) - .onTapGesture { - model.didSelectItem(item) - } - .onDrag { - let data = try! JSONEncoder().encode(DragValue(path: model.path, object: item)) - let string = String(decoding: data, as: UTF8.self) - return NSItemProvider(object: string as NSString) - } + Table(model.items, selection: $model.selectedItem) { + TableColumn("Name") { item in + Text(item.name ?? "No name") } - .onInsert(of: ["public.text"]) { index, items in - for item in items { - Task { - guard let data = try await item.loadItem(forTypeIdentifier: "public.text") as? Data, - let value = try? JSONDecoder().decode(DragValue.self, from: data) else { - return - } - self.model.items.insert(value.object, at: index) - } + TableColumn("Date modified") { item in + if let lastModifiedStringValue = item.metadata?["lastModified"]?.stringValue, + let lastModified = try? Date(lastModifiedStringValue, strategy: .iso8601.day().month().year().dateTimeSeparator(.standard).time(includingFractionalSeconds: true)) + { + Text(lastModified.formatted(date: .abbreviated, time: .shortened)) + } else { + Text("-") + } + } + + TableColumn("Size") { item in + if let sizeRawValue = item.metadata?["size"]?.intValue { + Text(sizeRawValue.formatted(.byteCount(style: .file))) + } else { + Text("-") } - print(index, items) } } + .contextMenu( + forSelectionType: FileObject.ID.self, + menu: { items in + Button("New folder") { + Task { + await model.newFolderButtonTapped() + } + } + }, + primaryAction: { items in + guard let item = items.first else { return } + + model.onPrimaryAction(item) + } + ) +// .onInsert(of: ["public.text"]) { index, items in +// for item in items { +// Task { +// guard let data = try await item.loadItem(forTypeIdentifier: "public.text") as? Data, +// let value = try? JSONDecoder().decode(DragValue.self, from: data) else { +// return +// } +// +// self.model.items.insert(value.object, at: index) +// } +// } +// print(index, items) +// } + .navigationTitle(model.path) .task { await model.load() } @@ -240,16 +235,5 @@ struct PanelView: View { Color.gray.opacity(0.2) } } - .contextMenu { - Button("New folder") { - Task { - await model.newFolderButtonTapped() - } - } - } } } - -#Preview { - AppView(model: AppModel(panels: [])) -} diff --git a/Examples/SupaDrive/SupaDriveApp.swift b/Examples/SupaDrive/SupaDriveApp.swift index e948a928..922634aa 100644 --- a/Examples/SupaDrive/SupaDriveApp.swift +++ b/Examples/SupaDrive/SupaDriveApp.swift @@ -7,16 +7,26 @@ import Supabase import SwiftUI +import OSLog let supabase = SupabaseClient( supabaseURL: URL(string: "http://127.0.0.1:54321")!, supabaseKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0", - options: SupabaseClientOptions(global: .init(logger: AppLogger())) + options: SupabaseClientOptions(global: .init(logger: Logger.supabase)) ) -struct AppLogger: SupabaseLogger { - func log(message: SupabaseLogMessage) { - print(message.description) +extension Logger: @retroactive SupabaseLogger { + static let supabase = Logger(subsystem: "supadrive", category: "supabase") + + public func log(message: SupabaseLogMessage) { + let logType: OSLogType = switch message.level { + case .debug: .debug + case .error: .error + case .verbose: .info + case .warning: .fault + } + + self.log(level: logType, "\(message)") } } @@ -26,7 +36,7 @@ struct SupaDriveApp: App { WindowGroup { AuthView { session in AppView( - model: AppModel(panels: [PanelModel(path: session.user.id.uuidString.lowercased())]) + model: AppModel(root: PanelModel(path: session.user.id.uuidString.lowercased())) ) } } diff --git a/Tests/IntegrationTests/supabase/.branches/_current_branch b/Tests/IntegrationTests/supabase/.branches/_current_branch new file mode 100644 index 00000000..88d050b1 --- /dev/null +++ b/Tests/IntegrationTests/supabase/.branches/_current_branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/Tests/IntegrationTests/supabase/.temp/cli-latest b/Tests/IntegrationTests/supabase/.temp/cli-latest new file mode 100644 index 00000000..8f675396 --- /dev/null +++ b/Tests/IntegrationTests/supabase/.temp/cli-latest @@ -0,0 +1 @@ +v1.183.5 \ No newline at end of file