diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index eb1c1c6d..257d851d 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 */; }; @@ -27,8 +33,10 @@ 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 */; }; + 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 */; }; @@ -76,6 +84,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 +160,16 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 792404B42C3454A9002959B3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 796B32BE2C4559E900DDD7B4 /* IdentifiedCollections in Frameworks */, + 792404E32C3473EC002959B3 /* Supabase in Frameworks */, + 79615DF92C3DA92C005AE6E0 /* CustomDump in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 793895C32954ABFF0044F2B8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -178,12 +203,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 +242,7 @@ 793895C62954ABFF0044F2B8 /* Examples.app */, 79FEFFAC2B07873600D36347 /* UserManagement.app */, 79D884C72B3C18830009EA4A /* SlackClone.app */, + 792404B72C3454A9002959B3 /* SupaDrive.app */, ); name = Products; sourceTree = ""; @@ -341,6 +389,28 @@ /* 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 */, + 79615DF82C3DA92C005AE6E0 /* CustomDump */, + 796B32BD2C4559E900DDD7B4 /* IdentifiedCollections */, + ); + 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 +484,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1510; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1510; TargetAttributes = { + 792404B62C3454A9002959B3 = { + CreatedOnToolsVersion = 16.0; + }; 793895C52954ABFF0044F2B8 = { CreatedOnToolsVersion = 14.1; }; @@ -442,6 +515,7 @@ 7956406E2955B5190088A06F /* XCRemoteSwiftPackageReference "swift-identified-collections" */, 7962989B2AEBC6F9000AA957 /* XCRemoteSwiftPackageReference "SVGView" */, 79E2B5562B97890F0042CD21 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + 79615DF72C3DA92C005AE6E0 /* XCRemoteSwiftPackageReference "swift-custom-dump" */, ); productRefGroup = 793895C72954ABFF0044F2B8 /* Products */; projectDirPath = ""; @@ -450,11 +524,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 +569,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 +654,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 +1079,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 = ( @@ -959,6 +1143,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"; @@ -978,6 +1170,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 792404E22C3473EC002959B3 /* Supabase */ = { + isa = XCSwiftPackageProductDependency; + productName = Supabase; + }; 7956406C2955B3500088A06F /* SwiftUINavigation */ = { isa = XCSwiftPackageProductDependency; package = 7956406B2955B3500088A06F /* XCRemoteSwiftPackageReference "swiftui-navigation" */; @@ -988,11 +1184,21 @@ 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" */; 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 new file mode 100644 index 00000000..a025edaf --- /dev/null +++ b/Examples/SupaDrive/AppView.swift @@ -0,0 +1,239 @@ +// +// AppView.swift +// Examples +// +// Created by Guilherme Souza on 02/07/24. +// + +import CustomDump +import Supabase +import SwiftUI +import IdentifiedCollections + +@MainActor +@Observable +final class AppModel { + let root: PanelModel + + var panels: IdentifiedArrayOf = [] { + didSet { + bindPanelModels() + } + } + + init(root: PanelModel) { + self.root = root + bindPanelModels() + } + + var path: String { + panels.last?.path ?? "" + } + + var pathComponents: [String] { + path.components(separatedBy: "/") + } + + var selectedFile: FileObject? { + nil// panels.last?.selectedItem + } + + private func bindPanelModels() { + 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: panel.path.appending("/\(item.name!)"))) + } + } + } +} + +struct AppView: View { + @Bindable var model: AppModel + + var body: some View { + 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 { + Form { + Text(selectedFile.name ?? "") + .font(.title2) + Divider() + + if let contentLenth = selectedFile.metadata?["contentLength"]?.intValue { + LabeledContent("Size", value: "\(contentLenth)") + } + + if let mimeType = selectedFile.metadata?["mimetype"]?.stringValue { + LabeledContent("MIME Type", value: mimeType) + } + } + .frame(width: 300) + .frame(maxHeight: .infinity, alignment: .top) + .padding() + .background(Color(NSColor.controlBackgroundColor)) + .transition(.move(edge: .trailing)) + } + } +// .animation(.default, value: model.path) +// .animation(.default, value: model.selectedFile) + } +} + +struct DragValue: Codable { + let path: String + let object: FileObject +} + +@MainActor +@Observable +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.ID? + + 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 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 { + + } + } + + 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) + await load() + } catch {} + } +} + +struct PanelView: View { + @Bindable var model: PanelModel + + @State private var isDraggingOver = false + + var body: some View { + Table(model.items, selection: $model.selectedItem) { + TableColumn("Name") { item in + Text(item.name ?? "No name") + } + + 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("-") + } + } + } + .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() + } + .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 { + await model.uploadFile(at: url) + } + } + } + return true + } + .overlay { + if isDraggingOver { + Color.gray.opacity(0.2) + } + } + } +} 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..41b58328 --- /dev/null +++ b/Examples/SupaDrive/AuthView.swift @@ -0,0 +1,67 @@ +// +// AuthView.swift +// Examples +// +// Created by Guilherme Souza on 02/07/24. +// + +import Supabase +import SwiftUI + +struct AuthView: View { + @ViewBuilder var content: (Session) -> Content + + @State var session: Session? + + var body: some View { + Group { + if let session { + content(session) + .environment(\.supabaseSession, session) + } else { + 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) + } + } + } + } + } + } + } +} + +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/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..922634aa --- /dev/null +++ b/Examples/SupaDrive/SupaDriveApp.swift @@ -0,0 +1,44 @@ +// +// SupaDriveApp.swift +// SupaDrive +// +// Created by Guilherme Souza on 02/07/24. +// + +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: Logger.supabase)) +) + +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)") + } +} + +@main +struct SupaDriveApp: App { + var body: some Scene { + WindowGroup { + AuthView { session in + AppView( + model: AppModel(root: PanelModel(path: session.user.id.uuidString.lowercased())) + ) + } + } + } +} 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 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..03dce1f5 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 } } 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