├── .gitignore ├── Blooply.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Blooply ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Blooply.png │ │ └── Contents.json │ └── Contents.json ├── BlooplyApp.swift ├── Extensions │ └── View+Extension.swift ├── Item.swift ├── Model │ └── ConvosManager.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── VariableBlur.swift └── Views │ ├── BottomBarView.swift │ ├── CameraSheetView.swift │ ├── ContentView.swift │ └── ConvoRowView.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Blooply.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1E4269122B51AC3D0000984E /* BlooplyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4269112B51AC3D0000984E /* BlooplyApp.swift */; }; 11 | 1E4269142B51AC3D0000984E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4269132B51AC3D0000984E /* ContentView.swift */; }; 12 | 1E4269162B51AC3D0000984E /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4269152B51AC3D0000984E /* Item.swift */; }; 13 | 1E4269182B51AC3F0000984E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E4269172B51AC3F0000984E /* Assets.xcassets */; }; 14 | 1E42691B2B51AC3F0000984E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E42691A2B51AC3F0000984E /* Preview Assets.xcassets */; }; 15 | 1E4269232B51C3620000984E /* BottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4269222B51C3620000984E /* BottomBarView.swift */; }; 16 | 1E4269252B51C4EF0000984E /* ConvoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4269242B51C4EF0000984E /* ConvoRowView.swift */; }; 17 | 1E4269282B51DCC10000984E /* ConvosManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4269272B51DCC10000984E /* ConvosManager.swift */; }; 18 | 1E42692B2B51E10D0000984E /* ScrollKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1E42692A2B51E10D0000984E /* ScrollKit */; }; 19 | 1E7C004A2B521D9300069ED0 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7C00492B521D9300069ED0 /* View+Extension.swift */; }; 20 | 1EABCC062B520F7100EB782E /* Camera-SwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 1EABCC052B520F7100EB782E /* Camera-SwiftUI */; }; 21 | 1EABCC082B5210C900EB782E /* CameraSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCC072B5210C900EB782E /* CameraSheetView.swift */; }; 22 | 1EABCC0A2B52144000EB782E /* VariableBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCC092B52144000EB782E /* VariableBlur.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 1E42690E2B51AC3D0000984E /* Blooply.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Blooply.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 1E4269112B51AC3D0000984E /* BlooplyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlooplyApp.swift; sourceTree = ""; }; 28 | 1E4269132B51AC3D0000984E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 29 | 1E4269152B51AC3D0000984E /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 30 | 1E4269172B51AC3F0000984E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 31 | 1E42691A2B51AC3F0000984E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 32 | 1E4269222B51C3620000984E /* BottomBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomBarView.swift; sourceTree = ""; }; 33 | 1E4269242B51C4EF0000984E /* ConvoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvoRowView.swift; sourceTree = ""; }; 34 | 1E4269272B51DCC10000984E /* ConvosManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvosManager.swift; sourceTree = ""; }; 35 | 1E4F5D112B52C8CB006E45B8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 36 | 1E7C00492B521D9300069ED0 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = ""; }; 37 | 1EABCC072B5210C900EB782E /* CameraSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraSheetView.swift; sourceTree = ""; }; 38 | 1EABCC092B52144000EB782E /* VariableBlur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariableBlur.swift; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 1E42690B2B51AC3D0000984E /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | 1E42692B2B51E10D0000984E /* ScrollKit in Frameworks */, 47 | 1EABCC062B520F7100EB782E /* Camera-SwiftUI in Frameworks */, 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 1E4269052B51AC3D0000984E = { 55 | isa = PBXGroup; 56 | children = ( 57 | 1E4F5D112B52C8CB006E45B8 /* README.md */, 58 | 1E4269102B51AC3D0000984E /* Blooply */, 59 | 1E42690F2B51AC3D0000984E /* Products */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | 1E42690F2B51AC3D0000984E /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 1E42690E2B51AC3D0000984E /* Blooply.app */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | 1E4269102B51AC3D0000984E /* Blooply */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 1E7C00482B521D8B00069ED0 /* Extensions */, 75 | 1E4269262B51DCB90000984E /* Model */, 76 | 1E4269212B51C17B0000984E /* Views */, 77 | 1E4269112B51AC3D0000984E /* BlooplyApp.swift */, 78 | 1E4269152B51AC3D0000984E /* Item.swift */, 79 | 1E4269172B51AC3F0000984E /* Assets.xcassets */, 80 | 1E4269192B51AC3F0000984E /* Preview Content */, 81 | 1EABCC092B52144000EB782E /* VariableBlur.swift */, 82 | ); 83 | path = Blooply; 84 | sourceTree = ""; 85 | }; 86 | 1E4269192B51AC3F0000984E /* Preview Content */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 1E42691A2B51AC3F0000984E /* Preview Assets.xcassets */, 90 | ); 91 | path = "Preview Content"; 92 | sourceTree = ""; 93 | }; 94 | 1E4269212B51C17B0000984E /* Views */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 1E4269132B51AC3D0000984E /* ContentView.swift */, 98 | 1E4269222B51C3620000984E /* BottomBarView.swift */, 99 | 1E4269242B51C4EF0000984E /* ConvoRowView.swift */, 100 | 1EABCC072B5210C900EB782E /* CameraSheetView.swift */, 101 | ); 102 | path = Views; 103 | sourceTree = ""; 104 | }; 105 | 1E4269262B51DCB90000984E /* Model */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 1E4269272B51DCC10000984E /* ConvosManager.swift */, 109 | ); 110 | path = Model; 111 | sourceTree = ""; 112 | }; 113 | 1E7C00482B521D8B00069ED0 /* Extensions */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 1E7C00492B521D9300069ED0 /* View+Extension.swift */, 117 | ); 118 | path = Extensions; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | 1E42690D2B51AC3D0000984E /* Blooply */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = 1E42691E2B51AC3F0000984E /* Build configuration list for PBXNativeTarget "Blooply" */; 127 | buildPhases = ( 128 | 1E42690A2B51AC3D0000984E /* Sources */, 129 | 1E42690B2B51AC3D0000984E /* Frameworks */, 130 | 1E42690C2B51AC3D0000984E /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = Blooply; 137 | packageProductDependencies = ( 138 | 1E42692A2B51E10D0000984E /* ScrollKit */, 139 | 1EABCC052B520F7100EB782E /* Camera-SwiftUI */, 140 | ); 141 | productName = Blooply; 142 | productReference = 1E42690E2B51AC3D0000984E /* Blooply.app */; 143 | productType = "com.apple.product-type.application"; 144 | }; 145 | /* End PBXNativeTarget section */ 146 | 147 | /* Begin PBXProject section */ 148 | 1E4269062B51AC3D0000984E /* Project object */ = { 149 | isa = PBXProject; 150 | attributes = { 151 | BuildIndependentTargetsInParallel = 1; 152 | LastSwiftUpdateCheck = 1500; 153 | LastUpgradeCheck = 1500; 154 | TargetAttributes = { 155 | 1E42690D2B51AC3D0000984E = { 156 | CreatedOnToolsVersion = 15.0; 157 | }; 158 | }; 159 | }; 160 | buildConfigurationList = 1E4269092B51AC3D0000984E /* Build configuration list for PBXProject "Blooply" */; 161 | compatibilityVersion = "Xcode 14.0"; 162 | developmentRegion = en; 163 | hasScannedForEncodings = 0; 164 | knownRegions = ( 165 | en, 166 | Base, 167 | ); 168 | mainGroup = 1E4269052B51AC3D0000984E; 169 | packageReferences = ( 170 | 1E4269292B51E10D0000984E /* XCRemoteSwiftPackageReference "ScrollKit" */, 171 | 1EABCC042B520F7100EB782E /* XCRemoteSwiftPackageReference "Camera-SwiftUI" */, 172 | ); 173 | productRefGroup = 1E42690F2B51AC3D0000984E /* Products */; 174 | projectDirPath = ""; 175 | projectRoot = ""; 176 | targets = ( 177 | 1E42690D2B51AC3D0000984E /* Blooply */, 178 | ); 179 | }; 180 | /* End PBXProject section */ 181 | 182 | /* Begin PBXResourcesBuildPhase section */ 183 | 1E42690C2B51AC3D0000984E /* Resources */ = { 184 | isa = PBXResourcesBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 1E42691B2B51AC3F0000984E /* Preview Assets.xcassets in Resources */, 188 | 1E4269182B51AC3F0000984E /* Assets.xcassets in Resources */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXResourcesBuildPhase section */ 193 | 194 | /* Begin PBXSourcesBuildPhase section */ 195 | 1E42690A2B51AC3D0000984E /* Sources */ = { 196 | isa = PBXSourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | 1EABCC0A2B52144000EB782E /* VariableBlur.swift in Sources */, 200 | 1E4269142B51AC3D0000984E /* ContentView.swift in Sources */, 201 | 1E4269162B51AC3D0000984E /* Item.swift in Sources */, 202 | 1E4269252B51C4EF0000984E /* ConvoRowView.swift in Sources */, 203 | 1E4269122B51AC3D0000984E /* BlooplyApp.swift in Sources */, 204 | 1E4269232B51C3620000984E /* BottomBarView.swift in Sources */, 205 | 1E4269282B51DCC10000984E /* ConvosManager.swift in Sources */, 206 | 1E7C004A2B521D9300069ED0 /* View+Extension.swift in Sources */, 207 | 1EABCC082B5210C900EB782E /* CameraSheetView.swift in Sources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | /* End PBXSourcesBuildPhase section */ 212 | 213 | /* Begin XCBuildConfiguration section */ 214 | 1E42691C2B51AC3F0000984E /* Debug */ = { 215 | isa = XCBuildConfiguration; 216 | buildSettings = { 217 | ALWAYS_SEARCH_USER_PATHS = NO; 218 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 219 | CLANG_ANALYZER_NONNULL = YES; 220 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 221 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 222 | CLANG_ENABLE_MODULES = YES; 223 | CLANG_ENABLE_OBJC_ARC = YES; 224 | CLANG_ENABLE_OBJC_WEAK = YES; 225 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 226 | CLANG_WARN_BOOL_CONVERSION = YES; 227 | CLANG_WARN_COMMA = YES; 228 | CLANG_WARN_CONSTANT_CONVERSION = YES; 229 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 230 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 231 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 232 | CLANG_WARN_EMPTY_BODY = YES; 233 | CLANG_WARN_ENUM_CONVERSION = YES; 234 | CLANG_WARN_INFINITE_RECURSION = YES; 235 | CLANG_WARN_INT_CONVERSION = YES; 236 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 238 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 239 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 240 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 241 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 242 | CLANG_WARN_STRICT_PROTOTYPES = YES; 243 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 244 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 245 | CLANG_WARN_UNREACHABLE_CODE = YES; 246 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 247 | COPY_PHASE_STRIP = NO; 248 | DEBUG_INFORMATION_FORMAT = dwarf; 249 | ENABLE_STRICT_OBJC_MSGSEND = YES; 250 | ENABLE_TESTABILITY = YES; 251 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 252 | GCC_C_LANGUAGE_STANDARD = gnu17; 253 | GCC_DYNAMIC_NO_PIC = NO; 254 | GCC_NO_COMMON_BLOCKS = YES; 255 | GCC_OPTIMIZATION_LEVEL = 0; 256 | GCC_PREPROCESSOR_DEFINITIONS = ( 257 | "DEBUG=1", 258 | "$(inherited)", 259 | ); 260 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 261 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 262 | GCC_WARN_UNDECLARED_SELECTOR = YES; 263 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 264 | GCC_WARN_UNUSED_FUNCTION = YES; 265 | GCC_WARN_UNUSED_VARIABLE = YES; 266 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 267 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 268 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 269 | MTL_FAST_MATH = YES; 270 | ONLY_ACTIVE_ARCH = YES; 271 | SDKROOT = iphoneos; 272 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 273 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 274 | }; 275 | name = Debug; 276 | }; 277 | 1E42691D2B51AC3F0000984E /* Release */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 282 | CLANG_ANALYZER_NONNULL = YES; 283 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 284 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_ARC = YES; 287 | CLANG_ENABLE_OBJC_WEAK = YES; 288 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 289 | CLANG_WARN_BOOL_CONVERSION = YES; 290 | CLANG_WARN_COMMA = YES; 291 | CLANG_WARN_CONSTANT_CONVERSION = YES; 292 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 295 | CLANG_WARN_EMPTY_BODY = YES; 296 | CLANG_WARN_ENUM_CONVERSION = YES; 297 | CLANG_WARN_INFINITE_RECURSION = YES; 298 | CLANG_WARN_INT_CONVERSION = YES; 299 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 300 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 301 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 302 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 303 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 304 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 305 | CLANG_WARN_STRICT_PROTOTYPES = YES; 306 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 307 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 308 | CLANG_WARN_UNREACHABLE_CODE = YES; 309 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 310 | COPY_PHASE_STRIP = NO; 311 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 312 | ENABLE_NS_ASSERTIONS = NO; 313 | ENABLE_STRICT_OBJC_MSGSEND = YES; 314 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 315 | GCC_C_LANGUAGE_STANDARD = gnu17; 316 | GCC_NO_COMMON_BLOCKS = YES; 317 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 318 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 319 | GCC_WARN_UNDECLARED_SELECTOR = YES; 320 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 321 | GCC_WARN_UNUSED_FUNCTION = YES; 322 | GCC_WARN_UNUSED_VARIABLE = YES; 323 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 324 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 325 | MTL_ENABLE_DEBUG_INFO = NO; 326 | MTL_FAST_MATH = YES; 327 | SDKROOT = iphoneos; 328 | SWIFT_COMPILATION_MODE = wholemodule; 329 | VALIDATE_PRODUCT = YES; 330 | }; 331 | name = Release; 332 | }; 333 | 1E42691F2B51AC3F0000984E /* Debug */ = { 334 | isa = XCBuildConfiguration; 335 | buildSettings = { 336 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 337 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 338 | CODE_SIGN_STYLE = Automatic; 339 | CURRENT_PROJECT_VERSION = 1; 340 | DEVELOPMENT_ASSET_PATHS = "\"Blooply/Preview Content\""; 341 | DEVELOPMENT_TEAM = J85899W596; 342 | ENABLE_PREVIEWS = YES; 343 | GENERATE_INFOPLIST_FILE = YES; 344 | INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera to talk with AI"; 345 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 346 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 347 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 348 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 349 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 350 | LD_RUNPATH_SEARCH_PATHS = ( 351 | "$(inherited)", 352 | "@executable_path/Frameworks", 353 | ); 354 | MARKETING_VERSION = 1.0; 355 | PRODUCT_BUNDLE_IDENTIFIER = tech.miguelferreira.Blooply; 356 | PRODUCT_NAME = "$(TARGET_NAME)"; 357 | SWIFT_EMIT_LOC_STRINGS = YES; 358 | SWIFT_VERSION = 5.0; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Debug; 362 | }; 363 | 1E4269202B51AC3F0000984E /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 367 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 368 | CODE_SIGN_STYLE = Automatic; 369 | CURRENT_PROJECT_VERSION = 1; 370 | DEVELOPMENT_ASSET_PATHS = "\"Blooply/Preview Content\""; 371 | DEVELOPMENT_TEAM = J85899W596; 372 | ENABLE_PREVIEWS = YES; 373 | GENERATE_INFOPLIST_FILE = YES; 374 | INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera to talk with AI"; 375 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 376 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 377 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 378 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 379 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 380 | LD_RUNPATH_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "@executable_path/Frameworks", 383 | ); 384 | MARKETING_VERSION = 1.0; 385 | PRODUCT_BUNDLE_IDENTIFIER = tech.miguelferreira.Blooply; 386 | PRODUCT_NAME = "$(TARGET_NAME)"; 387 | SWIFT_EMIT_LOC_STRINGS = YES; 388 | SWIFT_VERSION = 5.0; 389 | TARGETED_DEVICE_FAMILY = "1,2"; 390 | }; 391 | name = Release; 392 | }; 393 | /* End XCBuildConfiguration section */ 394 | 395 | /* Begin XCConfigurationList section */ 396 | 1E4269092B51AC3D0000984E /* Build configuration list for PBXProject "Blooply" */ = { 397 | isa = XCConfigurationList; 398 | buildConfigurations = ( 399 | 1E42691C2B51AC3F0000984E /* Debug */, 400 | 1E42691D2B51AC3F0000984E /* Release */, 401 | ); 402 | defaultConfigurationIsVisible = 0; 403 | defaultConfigurationName = Release; 404 | }; 405 | 1E42691E2B51AC3F0000984E /* Build configuration list for PBXNativeTarget "Blooply" */ = { 406 | isa = XCConfigurationList; 407 | buildConfigurations = ( 408 | 1E42691F2B51AC3F0000984E /* Debug */, 409 | 1E4269202B51AC3F0000984E /* Release */, 410 | ); 411 | defaultConfigurationIsVisible = 0; 412 | defaultConfigurationName = Release; 413 | }; 414 | /* End XCConfigurationList section */ 415 | 416 | /* Begin XCRemoteSwiftPackageReference section */ 417 | 1E4269292B51E10D0000984E /* XCRemoteSwiftPackageReference "ScrollKit" */ = { 418 | isa = XCRemoteSwiftPackageReference; 419 | repositoryURL = "https://github.com/danielsaidi/ScrollKit"; 420 | requirement = { 421 | kind = upToNextMajorVersion; 422 | minimumVersion = 0.4.0; 423 | }; 424 | }; 425 | 1EABCC042B520F7100EB782E /* XCRemoteSwiftPackageReference "Camera-SwiftUI" */ = { 426 | isa = XCRemoteSwiftPackageReference; 427 | repositoryURL = "https://github.com/rorodriguez116/Camera-SwiftUI.git"; 428 | requirement = { 429 | kind = upToNextMajorVersion; 430 | minimumVersion = 0.5.0; 431 | }; 432 | }; 433 | /* End XCRemoteSwiftPackageReference section */ 434 | 435 | /* Begin XCSwiftPackageProductDependency section */ 436 | 1E42692A2B51E10D0000984E /* ScrollKit */ = { 437 | isa = XCSwiftPackageProductDependency; 438 | package = 1E4269292B51E10D0000984E /* XCRemoteSwiftPackageReference "ScrollKit" */; 439 | productName = ScrollKit; 440 | }; 441 | 1EABCC052B520F7100EB782E /* Camera-SwiftUI */ = { 442 | isa = XCSwiftPackageProductDependency; 443 | package = 1EABCC042B520F7100EB782E /* XCRemoteSwiftPackageReference "Camera-SwiftUI" */; 444 | productName = "Camera-SwiftUI"; 445 | }; 446 | /* End XCSwiftPackageProductDependency section */ 447 | }; 448 | rootObject = 1E4269062B51AC3D0000984E /* Project object */; 449 | } 450 | -------------------------------------------------------------------------------- /Blooply.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Blooply.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Blooply.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "camera-swiftui", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/rorodriguez116/Camera-SwiftUI.git", 7 | "state" : { 8 | "revision" : "a83ad04ec213095e14d71491af8b55e7575a7b40", 9 | "version" : "0.5.0" 10 | } 11 | }, 12 | { 13 | "identity" : "scrollkit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/danielsaidi/ScrollKit", 16 | "state" : { 17 | "revision" : "7502b326742f7fcd14c4c2ebb8c0d50c5dacbdcc", 18 | "version" : "0.4.0" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Blooply/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Blooply/Assets.xcassets/AppIcon.appiconset/Blooply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguel-arrf/Blooply/3b551d5206c0b9261ed96aeac7bb9969c87ed301/Blooply/Assets.xcassets/AppIcon.appiconset/Blooply.png -------------------------------------------------------------------------------- /Blooply/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Blooply.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Blooply/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Blooply/BlooplyApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlooplyApp.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 12/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftData 10 | 11 | @main 12 | struct BlooplyApp: App { 13 | @State private var convosManager = ConvosManager() 14 | 15 | 16 | var sharedModelContainer: ModelContainer = { 17 | let schema = Schema([ 18 | Convo.self, 19 | ]) 20 | let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) 21 | 22 | do { 23 | return try ModelContainer(for: schema, configurations: [modelConfiguration]) 24 | } catch { 25 | fatalError("Could not create ModelContainer: \(error)") 26 | } 27 | }() 28 | 29 | var body: some Scene { 30 | WindowGroup { 31 | ContentView() 32 | .environment(convosManager) 33 | } 34 | .modelContainer(sharedModelContainer) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Blooply/Extensions/View+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Extension.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 13/01/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Combine 11 | 12 | public extension Publishers { 13 | static var keyboardHeight: AnyPublisher { 14 | let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification) 15 | .map { $0.keyboardHeight } 16 | let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification) 17 | .map { _ in CGFloat(0) } 18 | 19 | return MergeMany(willShow, willHide) 20 | .eraseToAnyPublisher() 21 | } 22 | } 23 | 24 | public extension Notification { 25 | var keyboardHeight: CGFloat { 26 | return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 27 | } 28 | } 29 | 30 | public struct KeyboardAvoiding: ViewModifier { 31 | @State private var keyboardActiveAdjustment: CGFloat = 0 32 | 33 | public func body(content: Content) -> some View { 34 | content 35 | .safeAreaInset(edge: .bottom, spacing: keyboardActiveAdjustment) { 36 | EmptyView().frame(height: 0) 37 | } 38 | .onReceive(Publishers.keyboardHeight) { 39 | self.keyboardActiveAdjustment = min($0, 10) 40 | } 41 | .animation(.smooth, value: keyboardActiveAdjustment) 42 | } 43 | } 44 | 45 | public extension View { 46 | func keyboardAvoiding() -> some View { 47 | modifier(KeyboardAvoiding()) 48 | } 49 | } 50 | 51 | 52 | /// https://www.hackingwithswift.com/quick-start/swiftui/how-to-dismiss-the-keyboard-for-a-textfield 53 | #if canImport(UIKit) 54 | extension View { 55 | func hideKeyboard() { 56 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 57 | } 58 | } 59 | #endif 60 | -------------------------------------------------------------------------------- /Blooply/Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Item.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 12/01/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftData 10 | 11 | @Model 12 | final class Convo { 13 | var timestamp: Date 14 | var icon: String 15 | var title: String 16 | var lastResponse: String 17 | 18 | init(timestamp: Date, icon: String, title: String, lastResponse: String) { 19 | self.timestamp = timestamp 20 | self.icon = icon 21 | self.title = title 22 | self.lastResponse = lastResponse 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Blooply/Model/ConvosManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConvosManager.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 12/01/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | @Observable 11 | class ConvosManager { 12 | var expandedConvo: Convo? 13 | var textfieldIsPressed: Bool = false 14 | 15 | func setExpandedConvo(convo: Convo) { 16 | expandedConvo = convo 17 | } 18 | 19 | func setTextfieldIsPressed(_ isPressed: Bool) { 20 | textfieldIsPressed = isPressed 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Blooply/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Blooply/VariableBlur.swift: -------------------------------------------------------------------------------- 1 | 2 | import SwiftUI 3 | import UIKit 4 | import CoreImage.CIFilterBuiltins 5 | import QuartzCore 6 | 7 | 8 | public enum VariableBlurDirection { 9 | case blurredTopClearBottom 10 | case blurredBottomClearTop 11 | } 12 | 13 | 14 | public struct VariableBlurView: UIViewRepresentable { 15 | 16 | public var maxBlurRadius: CGFloat = 20 17 | 18 | public var direction: VariableBlurDirection = .blurredTopClearBottom 19 | 20 | /// By default, variable blur starts from 0 blur radius and linearly increases to `maxBlurRadius`. Setting `startOffset` to a small negative coefficient (e.g. -0.1) will start blur from larger radius value which might look better in some cases. 21 | public var startOffset: CGFloat = 0 22 | 23 | public func makeUIView(context: Context) -> VariableBlurUIView { 24 | VariableBlurUIView(maxBlurRadius: maxBlurRadius, direction: direction, startOffset: startOffset) 25 | } 26 | 27 | public func updateUIView(_ uiView: VariableBlurUIView, context: Context) { 28 | } 29 | } 30 | 31 | 32 | /// credit https://github.com/jtrivedi/VariableBlurView 33 | open class VariableBlurUIView: UIVisualEffectView { 34 | 35 | public init(maxBlurRadius: CGFloat = 20, direction: VariableBlurDirection = .blurredTopClearBottom, startOffset: CGFloat = 0) { 36 | super.init(effect: UIBlurEffect(style: .regular)) 37 | 38 | // `CAFilter` is a private QuartzCore class that we dynamically declare in `CAFilter.h`. 39 | // let variableBlur = CAFilter.filter(withType: "variableBlur") as! NSObject 40 | 41 | // Same but no need for `CAFilter.h`. 42 | let CAFilter = NSClassFromString("CAFilter")! as! NSObject.Type 43 | let variableBlur = CAFilter.self.perform(NSSelectorFromString("filterWithType:"), with: "variableBlur").takeRetainedValue() as! NSObject 44 | 45 | // The blur radius at each pixel depends on the alpha value of the corresponding pixel in the gradient mask. 46 | // An alpha of 1 results in the max blur radius, while an alpha of 0 is completely unblurred. 47 | let gradientImage = makeGradientImage(startOffset: startOffset, direction: direction) 48 | 49 | variableBlur.setValue(maxBlurRadius, forKey: "inputRadius") 50 | variableBlur.setValue(gradientImage, forKey: "inputMaskImage") 51 | variableBlur.setValue(true, forKey: "inputNormalizeEdges") 52 | 53 | // We use a `UIVisualEffectView` here purely to get access to its `CABackdropLayer`, 54 | // which is able to apply various, real-time CAFilters onto the views underneath. 55 | let backdropLayer = subviews.first?.layer 56 | 57 | // Replace the standard filters (i.e. `gaussianBlur`, `colorSaturate`, etc.) with only the variableBlur. 58 | backdropLayer?.filters = [variableBlur] 59 | 60 | // Get rid of the visual effect view's dimming/tint view, so we don't see a hard line. 61 | for subview in subviews.dropFirst() { 62 | subview.alpha = 0 63 | } 64 | } 65 | 66 | required public init?(coder: NSCoder) { 67 | fatalError("init(coder:) has not been implemented") 68 | } 69 | 70 | open override func didMoveToWindow() { 71 | // fixes visible pixelization at unblurred edge (https://github.com/nikstar/VariableBlur/issues/1) 72 | guard let window, let backdropLayer = subviews.first?.layer else { return } 73 | backdropLayer.setValue(window.screen.scale, forKey: "scale") 74 | } 75 | 76 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 77 | // `super.traitCollectionDidChange(previousTraitCollection)` crashes the app 78 | } 79 | 80 | private func makeGradientImage(width: CGFloat = 100, height: CGFloat = 100, startOffset: CGFloat, direction: VariableBlurDirection) -> CGImage { // much lower resolution might be acceptable 81 | let ciGradientFilter = CIFilter.linearGradient() 82 | // let ciGradientFilter = CIFilter.smoothLinearGradient() 83 | ciGradientFilter.color0 = CIColor.black 84 | ciGradientFilter.color1 = CIColor.clear 85 | ciGradientFilter.point0 = CGPoint(x: 0, y: height) 86 | ciGradientFilter.point1 = CGPoint(x: 0, y: startOffset * height) // small negative value looks better with vertical lines 87 | if case .blurredBottomClearTop = direction { 88 | ciGradientFilter.point0.y = 0 89 | ciGradientFilter.point1.y = height - ciGradientFilter.point1.y 90 | } 91 | return CIContext().createCGImage(ciGradientFilter.outputImage!, from: CGRect(x: 0, y: 0, width: width, height: height))! 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /Blooply/Views/BottomBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottomBarView.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 12/01/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct BottomBarView: View { 12 | var isDown: Bool = false 13 | var hasOpenSheet: Bool = false 14 | 15 | var onOptionSelected: (OptionSelected) -> Void 16 | 17 | @State private var galleryRippleYOffset: Double = 0 18 | @State private var cameraRippleYOffset: Double = 0 19 | @State private var keyboardRippleYOffset: Double = 0 20 | @State private var voiceRippleYOffset: Double = 0 21 | @State private var audioRippleYOffset: Double = 0 22 | 23 | var body: some View { 24 | let _ = Self._printChanges() 25 | 26 | VStack(spacing: 26) { 27 | 28 | Text("How can I help you?") 29 | .foregroundStyle(isDown ? .primary : .tertiary) 30 | .fontWeight(isDown ? .bold : .regular) 31 | .animation(.easeInOut, value: isDown) 32 | .padding(.bottom, isDown ? 26 : 0) 33 | .opacity(hasOpenSheet ? 0 : 0.8) 34 | .onTapGesture { 35 | onOptionSelected(.keyboard) 36 | onKeyboardOptionSelected() 37 | } 38 | .frame(maxWidth: .infinity, alignment: .center) 39 | 40 | HStack { 41 | Spacer() 42 | 43 | Text(Image(systemName: "photo.stack.fill")) 44 | .foregroundStyle(.white) 45 | .offset(y: galleryRippleYOffset) 46 | 47 | .transaction { transaction in 48 | transaction.animation = transaction.animation?.delay(0.12) 49 | } 50 | .offset(y: isDown ? 100 : 0) 51 | .onTapGesture { 52 | onOptionSelected(.gallery) 53 | onGalleryOptionSelected() 54 | } 55 | 56 | Spacer() 57 | 58 | Text(Image(systemName: "camera.fill")) 59 | .foregroundStyle(.white) 60 | .offset(y: cameraRippleYOffset) 61 | 62 | .transaction { transaction in 63 | transaction.animation = transaction.animation?.delay(0.09) 64 | } 65 | .offset(y: isDown ? 100 : 0) 66 | .onTapGesture { 67 | onOptionSelected(.camera) 68 | onCameraOptionSelected() 69 | } 70 | 71 | Spacer() 72 | 73 | Text(Image(systemName: "keyboard.fill")) 74 | .foregroundStyle(.white) 75 | .offset(y: keyboardRippleYOffset) 76 | 77 | .transaction { transaction in 78 | transaction.animation = transaction.animation?.delay(0.06) 79 | } 80 | .offset(y: isDown ? 100 : 0) 81 | .onTapGesture { 82 | onOptionSelected(.keyboard) 83 | onKeyboardOptionSelected() 84 | } 85 | 86 | Spacer() 87 | 88 | Text(Image(systemName: "waveform.badge.mic")) 89 | .foregroundStyle(.white) 90 | .offset(y: audioRippleYOffset) 91 | 92 | .transaction { transaction in 93 | transaction.animation = transaction.animation?.delay(0.03) 94 | } 95 | .offset(y: isDown ? 100 : 0) 96 | .onTapGesture { 97 | onOptionSelected(.voice) 98 | onAudioOptionSelected() 99 | } 100 | 101 | Spacer() 102 | 103 | Text(Image(systemName: "headphones")) 104 | .foregroundStyle(.white) 105 | .offset(y: voiceRippleYOffset) 106 | 107 | .transaction { transaction in 108 | transaction.animation = transaction.animation?.delay(0.0) 109 | } 110 | .offset(y: isDown ? 100 : 0) 111 | .onTapGesture { 112 | onOptionSelected(.audio) 113 | onVoiceOptionSelected() 114 | } 115 | 116 | Spacer() 117 | } 118 | .font(.title2) 119 | .fontWeight(.bold) 120 | } 121 | } 122 | 123 | func onKeyboardOptionSelected() { 124 | withAnimation(.bouncy) { 125 | galleryRippleYOffset = 5 126 | cameraRippleYOffset = 2 127 | keyboardRippleYOffset = -5 128 | audioRippleYOffset = 2 129 | voiceRippleYOffset = 5 130 | } 131 | 132 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: { 133 | resetRippleEffect() 134 | }) 135 | } 136 | 137 | func onGalleryOptionSelected() { 138 | withAnimation(.bouncy) { 139 | galleryRippleYOffset = -5 140 | cameraRippleYOffset = 5 141 | keyboardRippleYOffset = 4 142 | audioRippleYOffset = 3 143 | voiceRippleYOffset = 2 144 | } 145 | 146 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: { 147 | resetRippleEffect() 148 | }) 149 | } 150 | 151 | func onCameraOptionSelected() { 152 | withAnimation(.bouncy) { 153 | galleryRippleYOffset = 5 154 | cameraRippleYOffset = -5 155 | keyboardRippleYOffset = 5 156 | audioRippleYOffset = 4 157 | voiceRippleYOffset = 3 158 | } 159 | 160 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: { 161 | resetRippleEffect() 162 | }) 163 | } 164 | 165 | func onAudioOptionSelected() { 166 | withAnimation(.bouncy) { 167 | galleryRippleYOffset = 3 168 | cameraRippleYOffset = -4 169 | keyboardRippleYOffset = 5 170 | audioRippleYOffset = -5 171 | voiceRippleYOffset = 5 172 | } 173 | 174 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: { 175 | resetRippleEffect() 176 | }) 177 | } 178 | 179 | func onVoiceOptionSelected() { 180 | withAnimation(.bouncy) { 181 | galleryRippleYOffset = 2 182 | cameraRippleYOffset = 3 183 | keyboardRippleYOffset = 4 184 | audioRippleYOffset = 5 185 | voiceRippleYOffset = -5 186 | } 187 | 188 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: { 189 | resetRippleEffect() 190 | }) 191 | } 192 | 193 | func resetRippleEffect() { 194 | withAnimation(.bouncy) { 195 | galleryRippleYOffset = 0 196 | cameraRippleYOffset = 0 197 | keyboardRippleYOffset = 0 198 | audioRippleYOffset = 0 199 | voiceRippleYOffset = 0 200 | } 201 | } 202 | 203 | } 204 | 205 | private struct _BottomBarViewPreview: View { 206 | @State private var isDown: Bool = false 207 | 208 | var body: some View { 209 | ZStack { 210 | Color.pink 211 | .ignoresSafeArea() 212 | 213 | Button("Down", action: { 214 | withAnimation(.bouncy) { 215 | isDown.toggle() 216 | } 217 | }) 218 | .buttonStyle(.bordered) 219 | .tint(.gray) 220 | } 221 | .overlay(alignment: .bottom, content: { 222 | BottomBarView(isDown: isDown, onOptionSelected: {_ in}) 223 | .preferredColorScheme(.dark) 224 | .offset(y: isDown ? 70 : 0) 225 | }) 226 | .animation(/*@START_MENU_TOKEN@*/.easeIn/*@END_MENU_TOKEN@*/, value: isDown) 227 | } 228 | } 229 | 230 | #Preview { 231 | _BottomBarViewPreview() 232 | } 233 | -------------------------------------------------------------------------------- /Blooply/Views/CameraSheetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraSheetView.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 13/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | import Camera_SwiftUI 10 | import AVFoundation 11 | import Combine 12 | 13 | final class CameraModel: ObservableObject { 14 | 15 | private let service = CameraService() 16 | 17 | @Published var photo: Photo! 18 | 19 | @Published var showAlertError = false 20 | 21 | @Published var isFlashOn = false 22 | 23 | @Published var willCapturePhoto = false 24 | 25 | var alertError: AlertError! 26 | 27 | var session: AVCaptureSession 28 | 29 | private var subscriptions = Set() 30 | 31 | init() { 32 | self.session = service.session 33 | 34 | service.$photo.sink { [weak self] (photo) in 35 | guard let pic = photo else { return } 36 | self?.photo = pic 37 | } 38 | .store(in: &self.subscriptions) 39 | 40 | service.$shouldShowAlertView.sink { [weak self] (val) in 41 | self?.alertError = self?.service.alertError 42 | self?.showAlertError = val 43 | } 44 | .store(in: &self.subscriptions) 45 | 46 | service.$flashMode.sink { [weak self] (mode) in 47 | self?.isFlashOn = mode == .on 48 | } 49 | .store(in: &self.subscriptions) 50 | 51 | service.$willCapturePhoto.sink { [weak self] (val) in 52 | self?.willCapturePhoto = val 53 | } 54 | .store(in: &self.subscriptions) 55 | } 56 | 57 | func configure() { 58 | service.checkForPermissions() 59 | service.configure() 60 | } 61 | 62 | func capturePhoto() { 63 | print("capturingPhoto") 64 | service.capturePhoto() 65 | } 66 | 67 | func flipCamera() { 68 | service.changeCamera() 69 | } 70 | 71 | func zoom(with factor: CGFloat) { 72 | service.set(zoom: factor) 73 | } 74 | 75 | func switchFlash() { 76 | service.flashMode = service.flashMode == .on ? .off : .on 77 | } 78 | } 79 | 80 | 81 | struct CameraSheetView: View { 82 | @StateObject var model = CameraModel() 83 | 84 | var showing: Bool = false 85 | var onClose: () -> Void 86 | 87 | var body: some View { 88 | Rectangle() 89 | .foregroundStyle(.clear) 90 | 91 | .overlay(alignment: .bottom, content: { 92 | if showing { 93 | VStack(alignment: .leading) { 94 | Rectangle() 95 | .overlay(content: { 96 | CameraPreview(session: model.session) 97 | // .scaledToFill() 98 | // .frame(maxWidth: .infinity, maxHeight: .infinity) 99 | // .frame(minWidth: UIScreen.main.bounds.width*1.5) 100 | // .onAppear { 101 | // model.configure() 102 | // } 103 | .scaleEffect(1.4) 104 | .onAppear { 105 | model.configure() 106 | } 107 | }) 108 | .clipped() 109 | } 110 | .frame(height: 600) 111 | .frame(maxWidth: .infinity, alignment: .leading) 112 | .background( 113 | Color.init(white: 0.15) 114 | ) 115 | .clipShape(RoundedRectangle(cornerRadius: 36)) 116 | .transition(.move(edge: .bottom)) 117 | .overlay(alignment: .topTrailing, content: { 118 | Text(Image(systemName: "xmark")) 119 | .padding() 120 | .padding() 121 | .onTapGesture { 122 | onClose() 123 | } 124 | }) 125 | .overlay(alignment: .bottom, content: { 126 | Rectangle() 127 | .foregroundStyle(.thinMaterial) 128 | .frame(height: 90) 129 | .ignoresSafeArea() 130 | }) 131 | // .overlay(alignment: .bottom, content: { 132 | // VariableBlurView(direction: .blurredBottomClearTop) 133 | // .frame(height: 100) 134 | // .ignoresSafeArea() 135 | // }) 136 | } 137 | }) 138 | .ignoresSafeArea() 139 | } 140 | } 141 | 142 | #Preview { 143 | CameraSheetView(onClose: {}) 144 | } 145 | -------------------------------------------------------------------------------- /Blooply/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 12/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftData 10 | import ScrollKit 11 | import Combine 12 | 13 | enum OptionSelected: Identifiable { 14 | var id: Self { 15 | return self 16 | } 17 | 18 | case gallery, camera, keyboard, voice, audio 19 | } 20 | 21 | struct AudioSheetView: View { 22 | 23 | var onClose: () -> Void 24 | 25 | var body: some View { 26 | Rectangle() 27 | .foregroundStyle(.blue) 28 | .frame(height: 300) 29 | .transition(.move(edge: .bottom)) 30 | .overlay(alignment: .topTrailing, content: { 31 | Text(Image(systemName: "xmark")) 32 | .padding() 33 | .onTapGesture { 34 | onClose() 35 | } 36 | }) 37 | } 38 | } 39 | 40 | struct VoiceSheetView: View { 41 | 42 | var onClose: () -> Void 43 | 44 | var body: some View { 45 | Rectangle() 46 | .foregroundStyle(.blue) 47 | .frame(height: 300) 48 | .transition(.move(edge: .bottom)) 49 | .overlay(alignment: .topTrailing, content: { 50 | Text(Image(systemName: "xmark")) 51 | .padding() 52 | .onTapGesture { 53 | onClose() 54 | } 55 | }) 56 | } 57 | } 58 | 59 | struct GallerySheetView: View { 60 | 61 | var onClose: () -> Void 62 | 63 | var body: some View { 64 | Rectangle() 65 | .foregroundStyle(.blue) 66 | .frame(height: 300) 67 | .transition(.move(edge: .bottom)) 68 | .overlay(alignment: .topTrailing, content: { 69 | Text(Image(systemName: "xmark")) 70 | .padding() 71 | .onTapGesture { 72 | onClose() 73 | } 74 | }) 75 | } 76 | } 77 | 78 | 79 | 80 | struct KeyBoardSheetView: View { 81 | @Environment(ConvosManager.self) private var convosManager 82 | 83 | @State private var convoText: String = "" 84 | 85 | enum FocusedField { 86 | case firstName, lastName 87 | } 88 | 89 | @FocusState private var focusedField: FocusedField? 90 | 91 | var onClose: () -> Void 92 | 93 | var body: some View { 94 | VStack(alignment: .leading) { 95 | HStack { 96 | 97 | VStack(alignment: .leading) { 98 | Text("New Thread") 99 | .fontWeight(.medium) 100 | Text("GPT-4") 101 | .foregroundStyle(.secondary) 102 | 103 | TextField("Convo", text: $convoText) 104 | .focused($focusedField, equals: .firstName) 105 | 106 | } 107 | .onChange(of: focusedField, { old, new in 108 | convosManager.setTextfieldIsPressed(new != nil) 109 | }) 110 | 111 | Spacer() 112 | 113 | 114 | } 115 | 116 | Spacer() 117 | } 118 | .padding() 119 | .padding() 120 | .frame(height: 400) 121 | .frame(maxWidth: .infinity, alignment: .leading) 122 | .background( 123 | Color.init(white: 0.15) 124 | ) 125 | .clipShape(RoundedRectangle(cornerRadius: 36)) 126 | .transition(.move(edge: .bottom)) 127 | .overlay(alignment: .topTrailing, content: { 128 | Text(Image(systemName: "xmark")) 129 | .padding() 130 | .padding() 131 | .onTapGesture { 132 | onClose() 133 | } 134 | }) 135 | .keyboardAvoiding() 136 | } 137 | } 138 | 139 | 140 | struct ContentView: View { 141 | @Environment(ConvosManager.self) private var convosManager 142 | @Environment(\.modelContext) private var modelContext 143 | @Query private var convos: [Convo] 144 | 145 | // @State private var scrollOffset = CGPoint.zero 146 | @State private var offsetDownBottomBarView: Bool = false 147 | @State private var lastOffsetWhenBottomBarWasChanged: Double = .zero 148 | 149 | @State private var selectedOption: OptionSelected? 150 | @State private var compressionSpacing: Double = 0 151 | @State private var canCompress: Bool = true 152 | 153 | var body: some View { 154 | NavigationStack { 155 | ScrollViewWithOffsetTracking { offset in 156 | if offset.y > 100 && canCompress == true { 157 | canCompress = false 158 | if compressionSpacing == 40 { 159 | withAnimation(.easeInOut(duration: 0.6)) { 160 | compressionSpacing = 0 161 | } 162 | } else { 163 | withAnimation(.bouncy) { 164 | compressionSpacing = 40 165 | } 166 | } 167 | } else if offset.y <= 0 { 168 | canCompress = true 169 | } 170 | 171 | // Can be optimized 172 | if offset.y - lastOffsetWhenBottomBarWasChanged < -40 { 173 | if !offsetDownBottomBarView { 174 | withAnimation(.bouncy) { 175 | offsetDownBottomBarView = true 176 | } 177 | } 178 | 179 | lastOffsetWhenBottomBarWasChanged = offset.y 180 | } else if abs( offset.y - lastOffsetWhenBottomBarWasChanged) > 40 { 181 | 182 | if offsetDownBottomBarView { 183 | withAnimation(.bouncy) { 184 | offsetDownBottomBarView = false 185 | } 186 | } 187 | 188 | lastOffsetWhenBottomBarWasChanged = offset.y 189 | } 190 | 191 | } content: { 192 | VStack(alignment: .leading, spacing: 50 - compressionSpacing) { 193 | ForEach(convos) { convo in 194 | ConvoRowView(convo: convo) 195 | .transition(.opacity.combined(with: .scale(scale: 0.9)).combined(with: .offset(x: -100))) 196 | .environment(convosManager) 197 | .id(convo) 198 | // NavigationLink(value: item, label: { 199 | // HStack { 200 | // Text(Image(systemName: item.icon)) 201 | // Text(item.title) 202 | // } 203 | // }) 204 | } 205 | .onDelete(perform: deleteItems) 206 | 207 | Spacer() 208 | } 209 | .padding(.top, 40) 210 | .padding(.bottom, 200) 211 | .animation(.smooth, value: convos) 212 | } 213 | .scrollBounceBehavior(.basedOnSize) 214 | // .toolbar { 215 | // ToolbarItem(placement: .navigationBarTrailing) { 216 | // EditButton() 217 | // } 218 | // ToolbarItem { 219 | // Button(action: addItem) { 220 | // Label("Add Item", systemImage: "plus") 221 | // } 222 | // } 223 | // } 224 | .navigationDestination(for: Convo.self) { item in 225 | Text("Select an item") 226 | } 227 | .navigationTitle("Convos") 228 | } 229 | .overlay(alignment: .bottom, content: { 230 | LinearGradient(stops: [.init(color: .black, location: 0.4), .init(color: .clear, location: 1.0)], startPoint: .bottom, endPoint: .top) 231 | .ignoresSafeArea() 232 | .frame(height: 360) 233 | .allowsHitTesting(false) 234 | .offset(y: 40) 235 | // .opacity(selectedOption == nil ? 1 : 0) 236 | .offset(y: offsetDownBottomBarView ? 70 : 0) 237 | }) 238 | .overlay(content: { 239 | Color.black 240 | .opacity(selectedOption == nil ? 0 : 0.4) 241 | .onTapGesture(perform: { 242 | withAnimation { 243 | selectedOption = nil 244 | } 245 | }) 246 | }) 247 | .overlay(alignment: .bottom, content: { 248 | CameraSheetView(showing: selectedOption == .camera, onClose: { 249 | withAnimation(.snappy) { 250 | selectedOption = nil 251 | } 252 | }) 253 | .id("cameraView") 254 | }) 255 | .overlay(alignment: .bottom, content: { 256 | VStack { 257 | Spacer() 258 | 259 | switch selectedOption { 260 | case .gallery: 261 | GallerySheetView(onClose: { 262 | withAnimation(.snappy) { 263 | selectedOption = nil 264 | } 265 | }) 266 | case .camera: 267 | // CameraSheetView(onClose: { 268 | // withAnimation(.snappy) { 269 | // selectedOption = nil 270 | // } 271 | // }) 272 | EmptyView() 273 | case .keyboard: 274 | KeyBoardSheetView(onClose: { 275 | hideKeyboard() 276 | withAnimation(.snappy) { 277 | selectedOption = nil 278 | } 279 | }) 280 | case .voice: 281 | VoiceSheetView(onClose: { 282 | withAnimation(.snappy) { 283 | selectedOption = nil 284 | } 285 | }) 286 | case .audio: 287 | AudioSheetView(onClose: { 288 | withAnimation(.snappy) { 289 | selectedOption = nil 290 | } 291 | }) 292 | case nil: 293 | EmptyView() 294 | } 295 | 296 | 297 | 298 | } 299 | // .ignoresSafeArea() 300 | .ignoresSafeArea(.container) 301 | .animation(.snappy, value: selectedOption) 302 | }) 303 | 304 | .overlay(alignment: .bottom, content: { 305 | BottomBarView(isDown: offsetDownBottomBarView, hasOpenSheet: selectedOption != nil, onOptionSelected: { optionSelected in 306 | withAnimation(.snappy) { 307 | self.selectedOption = optionSelected 308 | } 309 | }) 310 | .opacity(selectedOption == nil ? 1 : 0.8) 311 | .offset(y: offsetDownBottomBarView ? 70 : 0) 312 | .ignoresSafeArea() 313 | .offset(y: convosManager.textfieldIsPressed ? -26 : 0) 314 | }) 315 | .preferredColorScheme(.dark) 316 | .onAppear { 317 | // for _ in 0...30 { 318 | // addItem() 319 | // } 320 | for convo in gptConvos { 321 | withAnimation { 322 | modelContext.insert(convo) 323 | } 324 | } 325 | } 326 | } 327 | 328 | private func addItem() { 329 | withAnimation { 330 | let newItem = Convo.init(timestamp: .now, icon: "desktopcomputer", title: "Understanding Quantum Computing: Basics and Byond", lastResponse: "This is a preview of the last response in this thread.") 331 | modelContext.insert(newItem) 332 | } 333 | } 334 | 335 | private func deleteItems(offsets: IndexSet) { 336 | withAnimation { 337 | for index in offsets { 338 | modelContext.delete(convos[index]) 339 | } 340 | } 341 | } 342 | } 343 | 344 | struct _Preview: View { 345 | @State private var convosManager = ConvosManager() 346 | var body: some View { 347 | ContentView() 348 | .modelContainer(for: Convo.self, inMemory: true) 349 | .environment(convosManager) 350 | } 351 | } 352 | 353 | #Preview { 354 | _Preview() 355 | } 356 | 357 | let gptConvos = [ 358 | Convo( 359 | timestamp: Date(), 360 | icon: "brain.head.profile", 361 | title: "AI Mastery", 362 | lastResponse: "Taking design to the next level with cognitive assistance." 363 | ), 364 | Convo( 365 | timestamp: Date(), 366 | icon: "pencil.and.outline", 367 | title: "Dream Design", 368 | lastResponse: "Building visions with Saint Laurent Del Rey's inspirations." 369 | ), 370 | Convo( 371 | timestamp: Date(), 372 | icon: "paintbrush.pointed", 373 | title: "Color Theory", 374 | lastResponse: "AI suggests a palette inspired by Jordan Singer's taste." 375 | ), 376 | Convo( 377 | timestamp: Date(), 378 | icon: "rectangle.grid.1x2.fill", 379 | title: "Layout Logic", 380 | lastResponse: "Figuring out Figma fundamentals." 381 | ), 382 | Convo( 383 | timestamp: Date(), 384 | icon: "lightbulb.fill", 385 | title: "Idea Illumination", 386 | lastResponse: "When AI lights up the path to innovation." 387 | ), 388 | Convo( 389 | timestamp: Date(), 390 | icon: "lasso.and.sparkles", 391 | title: "Pixel Perfect", 392 | lastResponse: "AI's precision is as sharp as Laurent's style." 393 | ), 394 | Convo( 395 | timestamp: Date(), 396 | icon: "waveform.path.ecg", 397 | title: "Heartbeat of Design", 398 | lastResponse: "Putting a pulse on trends with neural networks." 399 | ), 400 | Convo( 401 | timestamp: Date(), 402 | icon: "photo.artframe", 403 | title: "Aesthetic Algorithm", 404 | lastResponse: "What would singer.ai paint in this frame?" 405 | ), 406 | Convo( 407 | timestamp: Date(), 408 | icon: "cpu", 409 | title: "Process: Creativity", 410 | lastResponse: "Powering through projects with AI as my copilot." 411 | ), 412 | Convo( 413 | timestamp: Date(), 414 | icon: "person.crop.circle.badge.checkmark", 415 | title: "Verified Visionary", 416 | lastResponse: "Jordan Singer's algorithmic autograph." 417 | ), 418 | Convo( 419 | timestamp: Date(), 420 | icon: "bolt.fill", 421 | title: "Energy of Excitement", 422 | lastResponse: "The spark that started it all - Saint Laurent Del Rey's mockup." 423 | ), 424 | Convo( 425 | timestamp: Date(), 426 | icon: "arrow.forward.square", 427 | title: "Future Forward", 428 | lastResponse: "AI predicts the next big thing in user interfaces." 429 | ), 430 | Convo( 431 | timestamp: Date(), 432 | icon: "slider.horizontal.3", 433 | title: "Adjusting Aesthetics", 434 | lastResponse: "Fine-tuning the user experience with a digital touch." 435 | ), 436 | Convo( 437 | timestamp: Date(), 438 | icon: "eyeglasses", 439 | title: "Insightful Interface", 440 | lastResponse: "Seeing the design world through AI lenses." 441 | ), 442 | Convo( 443 | timestamp: Date(), 444 | icon: "chart.bar.doc.horizontal", 445 | title: "Analytics of Artistry", 446 | lastResponse: "Charting the impact of design with AI." 447 | ), 448 | Convo( 449 | timestamp: Date(), 450 | icon: "magnifyingglass", 451 | title: "Detail Detective", 452 | lastResponse: "AI's attention to detail rivals that of Saint Laurent Del Rey." 453 | ), 454 | Convo( 455 | timestamp: Date(), 456 | icon: "link.badge.plus", 457 | title: "Connected Creativity", 458 | lastResponse: "Linking Jordan Singer's principles to my project." 459 | ), 460 | Convo( 461 | timestamp: Date(), 462 | icon: "scribble.variable", 463 | title: "Design Doodles", 464 | lastResponse: "Random AI sketches paving the way for masterpieces." 465 | ), 466 | Convo( 467 | timestamp: Date(), 468 | icon: "doc.on.clipboard", 469 | title: "Briefing the Bot", 470 | lastResponse: "AI, grasp the essence of this mockup!" 471 | ), 472 | Convo( 473 | timestamp: Date(), 474 | icon: "swift", 475 | title: "Swiftly Styled", 476 | lastResponse: "Coding a sleek UI as smooth as Saint Laurent's sketches." 477 | ), 478 | Convo( 479 | timestamp: Date().addingTimeInterval(-3600), // 1 hour ago 480 | icon: "text.bubble.fill", 481 | title: "Chatter with Chatbots", 482 | lastResponse: "Dissecting the dialogue with advanced AI." 483 | ), 484 | Convo( 485 | timestamp: Date().addingTimeInterval(-7200), // 2 hours ago 486 | icon: "network", 487 | title: "Linked by Logic", 488 | lastResponse: "Jordan Singer's designs connected in an AI web." 489 | ), 490 | Convo( 491 | timestamp: Date().addingTimeInterval(-10800), // 3 hours ago 492 | icon: "paintpalette.fill", 493 | title: "Palette Prodigy", 494 | lastResponse: "Crafting color schemes with computational creativity." 495 | ), 496 | Convo( 497 | timestamp: Date().addingTimeInterval(-14400), // 4 hours ago 498 | icon: "app.connected.to.app.below.fill", 499 | title: "Intertwined Interfaces", 500 | lastResponse: "AI and design in a harmonious dance." 501 | ), 502 | Convo( 503 | timestamp: Date().addingTimeInterval(-18000), // 5 hours ago 504 | icon: "figure.walk.diamond.fill", 505 | title: "Design's Runway", 506 | lastResponse: "AI struts on the runway of Saint Laurent Del Rey's aesthetic." 507 | ), 508 | Convo( 509 | timestamp: Date().addingTimeInterval(-21600), // 6 hours ago 510 | icon: "cube.transparent.fill", 511 | title: "Dimensional Design", 512 | lastResponse: "Venturing into 3D interfaces with AI-driven insights." 513 | ), 514 | Convo( 515 | timestamp: Date().addingTimeInterval(-25200), // 7 hours ago 516 | icon: "function", 517 | title: "Function meets Form", 518 | lastResponse: "Algorithmic artistry shaping the future of UX." 519 | ), 520 | Convo( 521 | timestamp: Date().addingTimeInterval(-28800), // 8 hours ago 522 | icon: "rectangle.3.offgrid.fill", 523 | title: "Boxes & Beyond", 524 | lastResponse: "Thinking outside the box with Jordan Singer's philosophy." 525 | ), 526 | Convo( 527 | timestamp: Date().addingTimeInterval(-32400), // 9 hours ago 528 | icon: "circle.grid.cross.fill", 529 | title: "Pattern Perfection", 530 | lastResponse: "AI decodes design patterns in Saint Laurent's mockups." 531 | ), 532 | Convo( 533 | timestamp: Date().addingTimeInterval(-36000), // 10 hours ago 534 | icon: "eyebrow", 535 | title: "Expressive Interfaces", 536 | lastResponse: "Crafting emotive UI that sings with personality." 537 | ) 538 | ] 539 | -------------------------------------------------------------------------------- /Blooply/Views/ConvoRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConvoRowView.swift 3 | // Blooply 4 | // 5 | // Created by Miguel Ferreira on 12/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConvoRowView: View { 11 | @Environment(ConvosManager.self) private var convosManager 12 | 13 | let convo: Convo 14 | 15 | @State private var pressing: Bool = false 16 | @State private var expanded: Bool = false 17 | @State private var longPressTimer: Timer? = nil 18 | 19 | @State private var animationTimer: Timer? = nil 20 | @State private var canExpand: Bool = true 21 | 22 | var body: some View { 23 | HStack { 24 | VStack(spacing: 20) { 25 | HStack(alignment: .top, spacing: 0) { 26 | Text(Image(systemName: convo.icon)) 27 | .bold() 28 | 29 | Text(convo.title) 30 | .frame(maxWidth: .infinity, alignment: .leading) 31 | .padding(.horizontal) 32 | .padding(.leading) 33 | // .bold() 34 | 35 | } 36 | .frame(maxWidth: .infinity, alignment: .leading) 37 | 38 | if expanded { 39 | HStack(alignment: .top, spacing: 0) { 40 | Text(Image(systemName: "arrowshape.turn.up.backward.circle.fill")) 41 | .foregroundStyle(.quaternary) 42 | 43 | VStack(alignment: .leading, spacing: 10) { 44 | Text(convo.lastResponse) 45 | .foregroundStyle(.secondary) 46 | 47 | Text("Tap again to open") 48 | .font(.caption) 49 | .foregroundStyle(.quaternary) 50 | } 51 | .padding(.horizontal) 52 | .padding(.leading) 53 | .frame(maxWidth: .infinity, alignment: .leading) 54 | } 55 | .frame(maxWidth: .infinity, alignment: .leading) 56 | .transition(.opacity) 57 | 58 | } 59 | } 60 | 61 | Text(Image(systemName: "chevron.right")) 62 | .opacity(0.5) 63 | .bold() 64 | } 65 | .animation(.snappy, value: expanded) 66 | .padding(.horizontal) 67 | .padding(.horizontal, 8) 68 | .blur(radius: pressing ? 2 : 0) 69 | .scaleEffect(pressing ? 0.96 : 1.0) 70 | .animation(.snappy, value: pressing) 71 | .onTapGesture { 72 | 73 | } 74 | .onLongPressGesture(minimumDuration: 0.5, perform: { 75 | print("onLongPressGesture") 76 | 77 | withAnimation { 78 | self.expanded = !self.expanded 79 | } 80 | 81 | if self.expanded { 82 | convosManager.setExpandedConvo(convo: convo) 83 | } 84 | 85 | }, onPressingChanged: { value in 86 | self.pressing = value 87 | }) 88 | // .simultaneousGesture( 89 | // // When the user is pressing, and if the pane is not extended, let's blur the row 90 | // DragGesture(minimumDistance: 0) 91 | // .onChanged({ pressing in 92 | // if canExpand { 93 | // self.pressing = true 94 | // 95 | // longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: false, block: { timer in 96 | // canExpand = false 97 | // self.pressing = false 98 | // withAnimation { 99 | // self.expanded = !self.expanded 100 | // } 101 | // 102 | // if self.expanded { 103 | // convosManager.setExpandedConvo(convo: convo) 104 | // } 105 | // }) 106 | // } 107 | // }) 108 | // .onEnded { _ in 109 | // self.pressing = false 110 | // animationTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false, block: { timer in 111 | // canExpand = true 112 | // }) 113 | // longPressTimer?.invalidate() 114 | // longPressTimer = nil 115 | // } 116 | // ) 117 | .onChange(of: convosManager.expandedConvo, { old, new in 118 | if new != convo { 119 | withAnimation { 120 | self.expanded = false 121 | } 122 | } 123 | }) 124 | } 125 | } 126 | 127 | private struct _ConvoRowViewPreview: View { 128 | @State private var convosManager: ConvosManager = .init() 129 | var body: some View { 130 | VStack(spacing: 30) { 131 | ConvoRowView(convo: .init(timestamp: .now, icon: "desktopcomputer", title: "Understanding Quantum Computing: Basics and Byond", lastResponse: "This is a preview of the last response in this thread.")) 132 | .environment(convosManager) 133 | 134 | ConvoRowView(convo: .init(timestamp: .now, icon: "swirl.circle.righthalf.filled", title: "Exploring Space: A Discussion on Mars Colonization", lastResponse: "This is a preview of the last response in this thread.")) 135 | .environment(convosManager) 136 | } 137 | .preferredColorScheme(.dark) 138 | } 139 | } 140 | 141 | #Preview { 142 | _ConvoRowViewPreview() 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blooply 2 | 3 | 4 | Blooply is a SwiftUI implementation of the amazing work done [here](https://twitter.com/laurentdelrey/status/1745834693931487340) by **Laurent Del Rey**. 5 | 6 | 7 | --------------------------------------------------------------------------------