├── .gitattributes ├── .gitignore ├── LICENSE ├── Quiebro.xcodeproj └── project.pbxproj ├── Quiebro ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon-1024.png │ │ ├── AppIcon-128.png │ │ ├── AppIcon-16.png │ │ ├── AppIcon-256 1.png │ │ ├── AppIcon-256.png │ │ ├── AppIcon-32 1.png │ │ ├── AppIcon-32.png │ │ ├── AppIcon-512 1.png │ │ ├── AppIcon-512.png │ │ ├── AppIcon-64.png │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── DropZoneView.swift ├── FileHandlerManager.swift ├── LoaderView.swift ├── MenuBarView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Quiebro.entitlements ├── QuiebroApp.swift ├── Resources │ ├── Extensions.swift │ ├── MenuBarController.swift │ └── UpdateChecker.swift └── UI Components │ ├── ButtonGroup.swift │ ├── GlassButtonStyle.swift │ ├── TitleBarAccessory.swift │ ├── UpdateView.swift │ ├── VisualEffectBlur.swift │ └── WindowAccessor.swift └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Xcode 5 | DerivedData/ 6 | build/ 7 | *.xcworkspace 8 | xcuserdata/ 9 | *.xcuserstate 10 | *.xcodeproj/project.xcworkspace/xcuserdata/ 11 | *.swiftpm/ 12 | .idea/ 13 | *.xcodeproj/project.xcworkspace/ 14 | *.xcodeproj/xcuserdata/ 15 | *.xcodeproj/xcshareddata/ 16 | Quiebro.xcodeproj/project.xcworkspace/xcuserdata/joaquintournier.xcuserdatad/UserInterfaceState.xcuserstate 17 | Quiebro.xcodeproj/xcuserdata/joaquintournier.xcuserdatad/xcschemes/xcschememanagement.plist 18 | 19 | # Swift Package Manager 20 | .build/ 21 | Packages/ 22 | Package.pins 23 | Package.resolved 24 | 25 | # Cocoapods 26 | Pods/ 27 | Podfile.lock 28 | 29 | # Carthage 30 | Carthage/Build/ 31 | 32 | # Fastlane 33 | fastlane/report.xml 34 | fastlane/Preview.html 35 | fastlane/screenshots 36 | fastlane/test_output 37 | *.ipa 38 | *.app.dSYM.zip 39 | *.app.zip 40 | 41 | # Code signing 42 | *.xcconfig 43 | *.mobileprovision 44 | *.cer 45 | *.p12 46 | 47 | # Temporary files 48 | *.tmp 49 | *.swp 50 | *.lock 51 | *~ 52 | 53 | # Logs 54 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 nuance-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Quiebro.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | C82F044E2CCB3DD20012C07B /* Quiebro.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Quiebro.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | /* End PBXFileReference section */ 12 | 13 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 14 | C82F04502CCB3DD20012C07B /* Quiebro */ = { 15 | isa = PBXFileSystemSynchronizedRootGroup; 16 | path = Quiebro; 17 | sourceTree = ""; 18 | }; 19 | /* End PBXFileSystemSynchronizedRootGroup section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | C82F044B2CCB3DD20012C07B /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | C82F04452CCB3DD20012C07B = { 33 | isa = PBXGroup; 34 | children = ( 35 | C82F04502CCB3DD20012C07B /* Quiebro */, 36 | C82F044F2CCB3DD20012C07B /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | C82F044F2CCB3DD20012C07B /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | C82F044E2CCB3DD20012C07B /* Quiebro.app */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | /* End PBXGroup section */ 49 | 50 | /* Begin PBXNativeTarget section */ 51 | C82F044D2CCB3DD20012C07B /* Quiebro */ = { 52 | isa = PBXNativeTarget; 53 | buildConfigurationList = C82F045D2CCB3DD40012C07B /* Build configuration list for PBXNativeTarget "Quiebro" */; 54 | buildPhases = ( 55 | C82F044A2CCB3DD20012C07B /* Sources */, 56 | C82F044B2CCB3DD20012C07B /* Frameworks */, 57 | C82F044C2CCB3DD20012C07B /* Resources */, 58 | ); 59 | buildRules = ( 60 | ); 61 | dependencies = ( 62 | ); 63 | fileSystemSynchronizedGroups = ( 64 | C82F04502CCB3DD20012C07B /* Quiebro */, 65 | ); 66 | name = Quiebro; 67 | packageProductDependencies = ( 68 | ); 69 | productName = Quiebro; 70 | productReference = C82F044E2CCB3DD20012C07B /* Quiebro.app */; 71 | productType = "com.apple.product-type.application"; 72 | }; 73 | /* End PBXNativeTarget section */ 74 | 75 | /* Begin PBXProject section */ 76 | C82F04462CCB3DD20012C07B /* Project object */ = { 77 | isa = PBXProject; 78 | attributes = { 79 | BuildIndependentTargetsInParallel = 1; 80 | LastSwiftUpdateCheck = 1600; 81 | LastUpgradeCheck = 1610; 82 | TargetAttributes = { 83 | C82F044D2CCB3DD20012C07B = { 84 | CreatedOnToolsVersion = 16.0; 85 | }; 86 | }; 87 | }; 88 | buildConfigurationList = C82F04492CCB3DD20012C07B /* Build configuration list for PBXProject "Quiebro" */; 89 | developmentRegion = en; 90 | hasScannedForEncodings = 0; 91 | knownRegions = ( 92 | en, 93 | Base, 94 | ); 95 | mainGroup = C82F04452CCB3DD20012C07B; 96 | minimizedProjectReferenceProxies = 1; 97 | preferredProjectObjectVersion = 77; 98 | productRefGroup = C82F044F2CCB3DD20012C07B /* Products */; 99 | projectDirPath = ""; 100 | projectRoot = ""; 101 | targets = ( 102 | C82F044D2CCB3DD20012C07B /* Quiebro */, 103 | ); 104 | }; 105 | /* End PBXProject section */ 106 | 107 | /* Begin PBXResourcesBuildPhase section */ 108 | C82F044C2CCB3DD20012C07B /* Resources */ = { 109 | isa = PBXResourcesBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXResourcesBuildPhase section */ 116 | 117 | /* Begin PBXSourcesBuildPhase section */ 118 | C82F044A2CCB3DD20012C07B /* Sources */ = { 119 | isa = PBXSourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXSourcesBuildPhase section */ 126 | 127 | /* Begin XCBuildConfiguration section */ 128 | C82F045B2CCB3DD40012C07B /* Debug */ = { 129 | isa = XCBuildConfiguration; 130 | buildSettings = { 131 | ALWAYS_SEARCH_USER_PATHS = NO; 132 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 133 | CLANG_ANALYZER_NONNULL = YES; 134 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 135 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 136 | CLANG_ENABLE_MODULES = YES; 137 | CLANG_ENABLE_OBJC_ARC = YES; 138 | CLANG_ENABLE_OBJC_WEAK = YES; 139 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 140 | CLANG_WARN_BOOL_CONVERSION = YES; 141 | CLANG_WARN_COMMA = YES; 142 | CLANG_WARN_CONSTANT_CONVERSION = YES; 143 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 144 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 145 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 146 | CLANG_WARN_EMPTY_BODY = YES; 147 | CLANG_WARN_ENUM_CONVERSION = YES; 148 | CLANG_WARN_INFINITE_RECURSION = YES; 149 | CLANG_WARN_INT_CONVERSION = YES; 150 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 151 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 152 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 153 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 155 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 156 | CLANG_WARN_STRICT_PROTOTYPES = YES; 157 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 158 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 159 | CLANG_WARN_UNREACHABLE_CODE = YES; 160 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 161 | COPY_PHASE_STRIP = NO; 162 | DEAD_CODE_STRIPPING = YES; 163 | DEBUG_INFORMATION_FORMAT = dwarf; 164 | ENABLE_STRICT_OBJC_MSGSEND = YES; 165 | ENABLE_TESTABILITY = YES; 166 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 167 | GCC_C_LANGUAGE_STANDARD = gnu17; 168 | GCC_DYNAMIC_NO_PIC = NO; 169 | GCC_NO_COMMON_BLOCKS = YES; 170 | GCC_OPTIMIZATION_LEVEL = 0; 171 | GCC_PREPROCESSOR_DEFINITIONS = ( 172 | "DEBUG=1", 173 | "$(inherited)", 174 | ); 175 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 176 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 177 | GCC_WARN_UNDECLARED_SELECTOR = YES; 178 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 179 | GCC_WARN_UNUSED_FUNCTION = YES; 180 | GCC_WARN_UNUSED_VARIABLE = YES; 181 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 182 | MACOSX_DEPLOYMENT_TARGET = 15.0; 183 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 184 | MTL_FAST_MATH = YES; 185 | ONLY_ACTIVE_ARCH = YES; 186 | SDKROOT = macosx; 187 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 188 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 189 | }; 190 | name = Debug; 191 | }; 192 | C82F045C2CCB3DD40012C07B /* Release */ = { 193 | isa = XCBuildConfiguration; 194 | buildSettings = { 195 | ALWAYS_SEARCH_USER_PATHS = NO; 196 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 197 | CLANG_ANALYZER_NONNULL = YES; 198 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 200 | CLANG_ENABLE_MODULES = YES; 201 | CLANG_ENABLE_OBJC_ARC = YES; 202 | CLANG_ENABLE_OBJC_WEAK = YES; 203 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 204 | CLANG_WARN_BOOL_CONVERSION = YES; 205 | CLANG_WARN_COMMA = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 208 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 209 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 210 | CLANG_WARN_EMPTY_BODY = YES; 211 | CLANG_WARN_ENUM_CONVERSION = YES; 212 | CLANG_WARN_INFINITE_RECURSION = YES; 213 | CLANG_WARN_INT_CONVERSION = YES; 214 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 215 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | COPY_PHASE_STRIP = NO; 226 | DEAD_CODE_STRIPPING = YES; 227 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 228 | ENABLE_NS_ASSERTIONS = NO; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu17; 232 | GCC_NO_COMMON_BLOCKS = YES; 233 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 234 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 235 | GCC_WARN_UNDECLARED_SELECTOR = YES; 236 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 237 | GCC_WARN_UNUSED_FUNCTION = YES; 238 | GCC_WARN_UNUSED_VARIABLE = YES; 239 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 240 | MACOSX_DEPLOYMENT_TARGET = 15.0; 241 | MTL_ENABLE_DEBUG_INFO = NO; 242 | MTL_FAST_MATH = YES; 243 | SDKROOT = macosx; 244 | SWIFT_COMPILATION_MODE = wholemodule; 245 | }; 246 | name = Release; 247 | }; 248 | C82F045E2CCB3DD40012C07B /* Debug */ = { 249 | isa = XCBuildConfiguration; 250 | buildSettings = { 251 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 252 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 253 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 254 | CODE_SIGN_ENTITLEMENTS = Quiebro/Quiebro.entitlements; 255 | CODE_SIGN_STYLE = Automatic; 256 | COMBINE_HIDPI_IMAGES = YES; 257 | CURRENT_PROJECT_VERSION = 7; 258 | DEAD_CODE_STRIPPING = YES; 259 | DEVELOPMENT_ASSET_PATHS = "\"Quiebro/Preview Content\""; 260 | DEVELOPMENT_TEAM = YYMLDY74QZ; 261 | ENABLE_HARDENED_RUNTIME = YES; 262 | ENABLE_PREVIEWS = YES; 263 | GENERATE_INFOPLIST_FILE = YES; 264 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 265 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 266 | LD_RUNPATH_SEARCH_PATHS = ( 267 | "$(inherited)", 268 | "@executable_path/../Frameworks", 269 | ); 270 | MACOSX_DEPLOYMENT_TARGET = 14.0; 271 | MARKETING_VERSION = 1.1.0; 272 | PRODUCT_BUNDLE_IDENTIFIER = Minimal.Quiebro; 273 | PRODUCT_NAME = "$(TARGET_NAME)"; 274 | SWIFT_EMIT_LOC_STRINGS = YES; 275 | SWIFT_VERSION = 5.0; 276 | }; 277 | name = Debug; 278 | }; 279 | C82F045F2CCB3DD40012C07B /* Release */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 283 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 284 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 285 | CODE_SIGN_ENTITLEMENTS = Quiebro/Quiebro.entitlements; 286 | CODE_SIGN_STYLE = Automatic; 287 | COMBINE_HIDPI_IMAGES = YES; 288 | CURRENT_PROJECT_VERSION = 7; 289 | DEAD_CODE_STRIPPING = YES; 290 | DEVELOPMENT_ASSET_PATHS = "\"Quiebro/Preview Content\""; 291 | DEVELOPMENT_TEAM = YYMLDY74QZ; 292 | ENABLE_HARDENED_RUNTIME = YES; 293 | ENABLE_PREVIEWS = YES; 294 | GENERATE_INFOPLIST_FILE = YES; 295 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 296 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 297 | LD_RUNPATH_SEARCH_PATHS = ( 298 | "$(inherited)", 299 | "@executable_path/../Frameworks", 300 | ); 301 | MACOSX_DEPLOYMENT_TARGET = 14.0; 302 | MARKETING_VERSION = 1.1.0; 303 | PRODUCT_BUNDLE_IDENTIFIER = Minimal.Quiebro; 304 | PRODUCT_NAME = "$(TARGET_NAME)"; 305 | SWIFT_EMIT_LOC_STRINGS = YES; 306 | SWIFT_VERSION = 5.0; 307 | }; 308 | name = Release; 309 | }; 310 | /* End XCBuildConfiguration section */ 311 | 312 | /* Begin XCConfigurationList section */ 313 | C82F04492CCB3DD20012C07B /* Build configuration list for PBXProject "Quiebro" */ = { 314 | isa = XCConfigurationList; 315 | buildConfigurations = ( 316 | C82F045B2CCB3DD40012C07B /* Debug */, 317 | C82F045C2CCB3DD40012C07B /* Release */, 318 | ); 319 | defaultConfigurationIsVisible = 0; 320 | defaultConfigurationName = Release; 321 | }; 322 | C82F045D2CCB3DD40012C07B /* Build configuration list for PBXNativeTarget "Quiebro" */ = { 323 | isa = XCConfigurationList; 324 | buildConfigurations = ( 325 | C82F045E2CCB3DD40012C07B /* Debug */, 326 | C82F045F2CCB3DD40012C07B /* Release */, 327 | ); 328 | defaultConfigurationIsVisible = 0; 329 | defaultConfigurationName = Release; 330 | }; 331 | /* End XCConfigurationList section */ 332 | }; 333 | rootObject = C82F04462CCB3DD20012C07B /* Project object */; 334 | } 335 | -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.850", 9 | "green" : "0.470", 10 | "red" : "0.250" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-256 1.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-32 1.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-512 1.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuance-dev/quiebro/bd3bd5f0ed369b9bc4f8d2f8b0c9ab4eebd32d8c/Quiebro/Assets.xcassets/AppIcon.appiconset/AppIcon-64.png -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "AppIcon-32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "AppIcon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "AppIcon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "AppIcon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "AppIcon-256 1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "AppIcon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "AppIcon-512 1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "AppIcon-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "AppIcon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Quiebro/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Quiebro/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | 4 | struct ContentView: View { 5 | @StateObject private var manager = FileHandlerManager() 6 | @State private var isDragging = false 7 | @State private var mode = Mode.breakFile 8 | 9 | enum Mode: Hashable { 10 | case breakFile 11 | case mend 12 | } 13 | 14 | var body: some View { 15 | ZStack { 16 | VisualEffectBlur(material: .headerView, blendingMode: .behindWindow) 17 | .ignoresSafeArea() 18 | 19 | VStack(spacing: 24) { 20 | // Modern Tab Switcher with Secure Mode 21 | HStack(spacing: 16) { 22 | // Tab switcher 23 | HStack(spacing: 0) { 24 | ForEach([Mode.breakFile, Mode.mend], id: \.self) { tabMode in 25 | Button(action: { 26 | withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { 27 | mode = tabMode 28 | } 29 | }) { 30 | Text(tabMode == .breakFile ? "Break" : "Mend") 31 | .font(.system(size: 14, weight: .medium)) 32 | .foregroundColor(mode == tabMode ? 33 | Color(NSColor.controlAccentColor) : 34 | Color.secondary) 35 | .frame(maxWidth: .infinity) 36 | .frame(height: 36) 37 | .background( 38 | RoundedRectangle(cornerRadius: 8) 39 | .fill(mode == tabMode ? 40 | Color(NSColor.controlAccentColor).opacity(0.1) : 41 | Color.clear) 42 | ) 43 | .contentShape(Rectangle()) 44 | } 45 | .buttonStyle(PlainButtonStyle()) 46 | .frame(width: 120) 47 | } 48 | } 49 | .background( 50 | RoundedRectangle(cornerRadius: 12) 51 | .fill(Color(NSColor.windowBackgroundColor).opacity(0.5)) 52 | .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) 53 | ) 54 | .overlay( 55 | RoundedRectangle(cornerRadius: 12) 56 | .stroke(Color.primary.opacity(0.08), lineWidth: 1) 57 | ) 58 | 59 | if mode == .breakFile { 60 | // Secure Mode Toggle 61 | Button { 62 | manager.isSecureMode.toggle() 63 | } label: { 64 | HStack(spacing: 8) { 65 | Image(systemName: manager.isSecureMode ? "lock.fill" : "lock.open.fill") 66 | .foregroundStyle(manager.isSecureMode ? Color.accentColor : .secondary) 67 | .font(.system(size: 13, weight: .medium)) 68 | .contentTransition(.symbolEffect(.replace)) 69 | 70 | Text("Secure") 71 | .font(.system(size: 13, weight: .medium)) 72 | .foregroundStyle(manager.isSecureMode ? Color.primary : .secondary) 73 | } 74 | .frame(height: 36) 75 | .padding(.horizontal, 12) 76 | .background( 77 | RoundedRectangle(cornerRadius: 8) 78 | .fill(manager.isSecureMode ? 79 | Color.accentColor.opacity(0.1) : 80 | Color(NSColor.windowBackgroundColor).opacity(0.5)) 81 | ) 82 | .overlay( 83 | RoundedRectangle(cornerRadius: 8) 84 | .stroke(manager.isSecureMode ? 85 | Color.accentColor.opacity(0.2) : 86 | Color.primary.opacity(0.08), 87 | lineWidth: 1) 88 | ) 89 | } 90 | .buttonStyle(.plain) 91 | .help("Enables military-grade encryption (AES-GCM) with unique keys for each piece") 92 | } 93 | } 94 | .padding(.horizontal) 95 | 96 | ZStack { 97 | if mode == .breakFile { 98 | if manager.pieces.isEmpty { 99 | DropZoneView(isDragging: $isDragging) { 100 | handleFileSelection() 101 | } 102 | } else { 103 | // Improved file list view 104 | VStack(spacing: 16) { 105 | HStack { 106 | Text("File Pieces") 107 | .font(.headline) 108 | .foregroundColor(.secondary) 109 | Spacer() 110 | Text("\(manager.pieces.count)/3") 111 | .font(.subheadline) 112 | .foregroundColor(.secondary) 113 | .padding(.horizontal, 8) 114 | .padding(.vertical, 4) 115 | .background(Color.secondary.opacity(0.1)) 116 | .cornerRadius(6) 117 | } 118 | 119 | ForEach(manager.pieces, id: \.self) { url in 120 | HStack(spacing: 12) { 121 | Image(systemName: "doc.fill") 122 | .foregroundColor(.accentColor) 123 | .font(.system(size: 20)) 124 | 125 | VStack(alignment: .leading, spacing: 4) { 126 | Text(url.lastPathComponent) 127 | .font(.system(.body, design: .monospaced)) 128 | .lineLimit(1) 129 | 130 | Text(formatFileSize(url)) 131 | .font(.caption) 132 | .foregroundColor(.secondary) 133 | } 134 | 135 | Spacer() 136 | 137 | Button(action: { 138 | NSWorkspace.shared.selectFile(url.path, 139 | inFileViewerRootedAtPath: url.deletingLastPathComponent().path) 140 | }) { 141 | Image(systemName: "folder") 142 | .foregroundColor(.secondary) 143 | } 144 | .buttonStyle(PlainButtonStyle()) 145 | .help("Show in Finder") 146 | } 147 | .padding(12) 148 | .background( 149 | RoundedRectangle(cornerRadius: 8) 150 | .fill(Color(NSColor.windowBackgroundColor).opacity(0.5)) 151 | ) 152 | .overlay( 153 | RoundedRectangle(cornerRadius: 8) 154 | .stroke(Color.primary.opacity(0.1), lineWidth: 1) 155 | ) 156 | } 157 | } 158 | .padding(.horizontal) 159 | } 160 | } else { 161 | DropZoneView(isDragging: $isDragging) { 162 | handlePieceSelection() 163 | } 164 | } 165 | 166 | if manager.isLoading { 167 | LoaderView() 168 | } 169 | 170 | if case .error(let message) = manager.uploadState { 171 | Text(message) 172 | .foregroundColor(.red) 173 | .padding() 174 | .background(Color.black.opacity(0.7)) 175 | .cornerRadius(8) 176 | } 177 | } 178 | 179 | if !manager.pieces.isEmpty && mode == .breakFile { 180 | ButtonGroup(buttons: [ 181 | ( 182 | title: "Save All", 183 | icon: "arrow.down.circle", 184 | action: savePieces 185 | ), 186 | ( 187 | title: "Clear", 188 | icon: "trash", 189 | action: manager.clearFiles 190 | ) 191 | ]) 192 | .disabled(manager.isLoading) 193 | } 194 | } 195 | .padding(30) 196 | } 197 | .frame(minWidth: 600, minHeight: 700) 198 | .onDrop(of: [UTType.item], isTargeted: $isDragging) { providers in 199 | loadDroppedFiles(providers) 200 | return true 201 | } 202 | } 203 | 204 | private func handleFileSelection() { 205 | let panel = NSOpenPanel() 206 | panel.allowsMultipleSelection = false 207 | panel.canChooseDirectories = false 208 | panel.canChooseFiles = true 209 | panel.allowedContentTypes = [.item] 210 | 211 | panel.begin { response in 212 | if response == .OK, let url = panel.url { 213 | manager.handleFileSelection(url) 214 | } 215 | } 216 | } 217 | 218 | private func handlePieceSelection() { 219 | let panel = NSOpenPanel() 220 | panel.allowsMultipleSelection = true 221 | panel.canChooseDirectories = false 222 | panel.canChooseFiles = true 223 | panel.allowedContentTypes = [.item] 224 | 225 | panel.begin { response in 226 | if response == .OK, panel.urls.count == 3 { 227 | manager.mendFiles(panel.urls) 228 | } else { 229 | manager.uploadState = .error("Please select exactly 3 pieces") 230 | } 231 | } 232 | } 233 | 234 | private func loadDroppedFiles(_ providers: [NSItemProvider]) { 235 | if mode == .breakFile { 236 | guard let provider = providers.first else { return } 237 | provider.loadItem(forTypeIdentifier: UTType.item.identifier, options: nil) { item, error in 238 | if let url = item as? URL { 239 | DispatchQueue.main.async { 240 | self.manager.handleFileSelection(url) 241 | } 242 | } 243 | } 244 | } else { 245 | // Handle mending mode 246 | let group = DispatchGroup() 247 | var urls: [URL] = [] 248 | 249 | providers.forEach { provider in 250 | group.enter() 251 | provider.loadItem(forTypeIdentifier: UTType.item.identifier, options: nil) { item, error in 252 | defer { group.leave() } 253 | if let url = item as? URL { 254 | urls.append(url) 255 | } 256 | } 257 | } 258 | 259 | group.notify(queue: .main) { 260 | if urls.count == 3 { 261 | self.manager.mendFiles(urls) 262 | } else { 263 | self.manager.uploadState = .error("Please drop exactly 3 file pieces") 264 | } 265 | } 266 | } 267 | } 268 | 269 | private func savePieces() { 270 | manager.saveAllPieces() 271 | } 272 | 273 | private func formatFileSize(_ url: URL) -> String { 274 | do { 275 | let resources = try url.resourceValues(forKeys: [.fileSizeKey]) 276 | if let size = resources.fileSize { 277 | let formatter = ByteCountFormatter() 278 | formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB] 279 | formatter.countStyle = .file 280 | return formatter.string(fromByteCount: Int64(size)) 281 | } 282 | } catch { 283 | print("Error getting file size: \(error)") 284 | } 285 | return "" 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /Quiebro/DropZoneView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DropZoneView: View { 4 | @Binding var isDragging: Bool 5 | let onTap: () -> Void 6 | 7 | var body: some View { 8 | Button(action: onTap) { 9 | VStack(spacing: 15) { 10 | Image(systemName: "arrow.down.circle") 11 | .font(.system(size: 50)) 12 | .foregroundColor(.secondary) 13 | 14 | Text("Click or drop a file to split or 3 files to mend") 15 | .font(.title3) 16 | .foregroundColor(.secondary) 17 | } 18 | .frame(maxWidth: .infinity, maxHeight: .infinity) 19 | .background( 20 | RoundedRectangle(cornerRadius: 15) 21 | .strokeBorder(isDragging ? Color.accentColor : Color.secondary.opacity(0.3), 22 | style: StrokeStyle(lineWidth: 2, dash: [10])) 23 | .background(Color.clear) 24 | ) 25 | .padding() 26 | } 27 | .buttonStyle(PlainButtonStyle()) 28 | .overlay( 29 | isDragging ? 30 | RoundedRectangle(cornerRadius: 15) 31 | .stroke(Color.accentColor, lineWidth: 2) 32 | .padding() 33 | : nil 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Quiebro/FileHandlerManager.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CryptoKit 3 | import UniformTypeIdentifiers 4 | import Compression 5 | 6 | class FileHandlerManager: ObservableObject { 7 | @Published var isLoading = false 8 | @Published var inputFile: URL? 9 | @Published var pieces: [URL] = [] 10 | @Published var uploadState: UploadState = .idle 11 | @Published var mendedFile: URL? 12 | @Published var progress: Double = 0 13 | @Published var isSecureMode = false 14 | 15 | enum UploadState { 16 | case idle 17 | case uploading 18 | case processing(String) 19 | case completed 20 | case error(String) 21 | } 22 | 23 | private var temporaryDirectory: URL { 24 | FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) 25 | } 26 | 27 | private let chunkSize = 1024 * 1024 // 1MB chunks for streaming 28 | 29 | deinit { 30 | cleanupTemporaryFiles() 31 | } 32 | 33 | func handleFileSelection(_ url: URL) { 34 | Task { @MainActor in 35 | self.uploadState = .uploading 36 | self.inputFile = url 37 | self.breakFile(url) 38 | } 39 | } 40 | 41 | func clearFiles() { 42 | inputFile = nil 43 | pieces = [] 44 | mendedFile = nil 45 | uploadState = .idle 46 | isLoading = false 47 | progress = 0 48 | cleanupTemporaryFiles() 49 | } 50 | 51 | private func cleanupTemporaryFiles() { 52 | for url in pieces { 53 | try? FileManager.default.removeItem(atPath: url.path(percentEncoded: false)) 54 | } 55 | 56 | if let mendedFile = mendedFile, 57 | mendedFile.path(percentEncoded: false).contains(FileManager.default.temporaryDirectory.path(percentEncoded: false)) { 58 | try? FileManager.default.removeItem(atPath: mendedFile.path(percentEncoded: false)) 59 | } 60 | } 61 | 62 | private func compressData(_ data: Data) throws -> Data { 63 | let sourceSize = data.count 64 | let destinationSize = sourceSize * 2 // Allow for potential compression expansion 65 | let destinationBuffer = UnsafeMutablePointer.allocate(capacity: destinationSize) 66 | defer { destinationBuffer.deallocate() } 67 | 68 | let compressedSize = data.withUnsafeBytes { sourceBuffer -> Int in 69 | guard let baseAddress = sourceBuffer.baseAddress else { return 0 } 70 | return compression_encode_buffer( 71 | destinationBuffer, 72 | destinationSize, 73 | baseAddress.assumingMemoryBound(to: UInt8.self), 74 | sourceBuffer.count, 75 | nil, 76 | COMPRESSION_LZFSE 77 | ) 78 | } 79 | 80 | return Data(bytes: destinationBuffer, count: compressedSize) 81 | } 82 | 83 | private func decompressData(_ data: Data) throws -> Data { 84 | let sourceSize = data.count 85 | let destinationSize = sourceSize * 10 // Allow for significant decompression expansion 86 | let destinationBuffer = UnsafeMutablePointer.allocate(capacity: destinationSize) 87 | defer { destinationBuffer.deallocate() } 88 | 89 | let decompressedSize = data.withUnsafeBytes { sourceBuffer -> Int in 90 | guard let baseAddress = sourceBuffer.baseAddress else { return 0 } 91 | return compression_decode_buffer( 92 | destinationBuffer, 93 | destinationSize, 94 | baseAddress.assumingMemoryBound(to: UInt8.self), 95 | sourceBuffer.count, 96 | nil, 97 | COMPRESSION_LZFSE 98 | ) 99 | } 100 | 101 | return Data(bytes: destinationBuffer, count: decompressedSize) 102 | } 103 | 104 | private func encryptData(_ data: Data, using key: SymmetricKey) throws -> (encryptedData: Data, salt: Data) { 105 | let salt = Data((0..<32).map { _ in UInt8.random(in: 0...255) }) 106 | let derivedKey = deriveKey(from: key.withUnsafeBytes { Data($0) }.base64EncodedString(), salt: salt) 107 | let sealedBox = try AES.GCM.seal(data, using: derivedKey, nonce: AES.GCM.Nonce()) 108 | return (sealedBox.combined!, salt) 109 | } 110 | 111 | private func decryptData(_ data: Data, salt: Data, using key: SymmetricKey) throws -> Data { 112 | let derivedKey = deriveKey(from: key.withUnsafeBytes { Data($0) }.base64EncodedString(), salt: salt) 113 | let sealedBox = try AES.GCM.SealedBox(combined: data) 114 | return try AES.GCM.open(sealedBox, using: derivedKey) 115 | } 116 | 117 | private func deriveKey(from hash: String, salt: Data) -> SymmetricKey { 118 | let hashData = hash.data(using: .utf8)! 119 | return HKDF.deriveKey( 120 | inputKeyMaterial: .init(data: hashData), 121 | salt: salt, 122 | info: "Quiebro.FileEncryption".data(using: .utf8)!, 123 | outputByteCount: 32 124 | ) 125 | } 126 | 127 | func breakFile(_ url: URL) { 128 | Task { @MainActor in 129 | self.isLoading = true 130 | self.uploadState = .processing("Analyzing file...") 131 | self.progress = 0 132 | 133 | do { 134 | let tempDir = temporaryDirectory 135 | try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) 136 | 137 | let fileData = try Data(contentsOf: url) 138 | let fileSize = fileData.count 139 | let pieceSize = Int(ceil(Double(fileSize) / 3.0)) 140 | 141 | let fileId = UUID().uuidString 142 | let fileName = url.lastPathComponent 143 | let originalFileHash = calculateHash(fileData) 144 | 145 | var pieceUrls: [URL] = [] 146 | 147 | for i in 0..<3 { 148 | self.uploadState = .processing("Processing piece \(i + 1)/3") 149 | 150 | let startIndex = i * pieceSize 151 | let endIndex = min(startIndex + pieceSize, fileSize) 152 | let chunk = fileData[startIndex.. String { 319 | let hash = SHA256.hash(data: data) 320 | return hash.compactMap { String(format: "%02x", $0) }.joined() 321 | } 322 | 323 | func saveAllPieces() { 324 | Task { @MainActor in 325 | do { 326 | let openPanel = NSOpenPanel() 327 | openPanel.canChooseDirectories = true 328 | openPanel.canChooseFiles = false 329 | openPanel.canCreateDirectories = true 330 | openPanel.message = "Choose where to save the file pieces" 331 | openPanel.prompt = "Save Pieces" 332 | 333 | guard openPanel.runModal() == .OK, 334 | let targetDirectory = openPanel.url else { 335 | return 336 | } 337 | 338 | self.uploadState = .processing("Saving pieces...") 339 | self.isLoading = true 340 | 341 | for (index, pieceUrl) in pieces.enumerated() { 342 | let fileName = pieceUrl.lastPathComponent 343 | let destinationUrl = targetDirectory.appendingPathComponent(fileName) 344 | 345 | if FileManager.default.fileExists(atPath: destinationUrl.path) { 346 | try FileManager.default.removeItem(at: destinationUrl) 347 | } 348 | try FileManager.default.copyItem(at: pieceUrl, to: destinationUrl) 349 | 350 | self.progress = Double(index + 1) / Double(pieces.count) 351 | } 352 | 353 | self.uploadState = .completed 354 | self.isLoading = false 355 | self.progress = 1.0 356 | 357 | // Clear after successful save 358 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 359 | self.clearFiles() 360 | } 361 | } catch { 362 | self.uploadState = .error("Failed to save pieces: \(error.localizedDescription)") 363 | self.isLoading = false 364 | self.progress = 0 365 | } 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /Quiebro/LoaderView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LoaderView: View { 4 | @State private var isAnimating = false 5 | private let duration: Double = 1.5 6 | 7 | var body: some View { 8 | ZStack { 9 | // Backdrop blur 10 | Rectangle() 11 | .fill(.ultraThinMaterial) 12 | .ignoresSafeArea() 13 | 14 | // Modern spinner container 15 | ZStack { 16 | // Background card 17 | RoundedRectangle(cornerRadius: 16) 18 | .fill(Color(NSColor.windowBackgroundColor).opacity(0.7)) 19 | .frame(width: 120, height: 120) 20 | .overlay( 21 | RoundedRectangle(cornerRadius: 16) 22 | .stroke(Color.white.opacity(0.1), lineWidth: 1) 23 | ) 24 | .background( 25 | RoundedRectangle(cornerRadius: 16) 26 | .fill(Color.black.opacity(0.2)) 27 | .blur(radius: 10) 28 | ) 29 | 30 | // Spinner rings 31 | ZStack { 32 | // Outer ring 33 | Circle() 34 | .trim(from: 0.2, to: 0.8) 35 | .stroke( 36 | AngularGradient( 37 | gradient: Gradient(colors: [Color.clear, Color.accentColor.opacity(0.3)]), 38 | center: .center, 39 | startAngle: .degrees(0), 40 | endAngle: .degrees(360) 41 | ), 42 | style: StrokeStyle(lineWidth: 4, lineCap: .round) 43 | ) 44 | .frame(width: 54, height: 54) 45 | .rotationEffect(.degrees(isAnimating ? 360 : 0)) 46 | 47 | // Inner ring 48 | Circle() 49 | .trim(from: 0, to: 0.6) 50 | .stroke( 51 | Color.accentColor, 52 | style: StrokeStyle(lineWidth: 4, lineCap: .round) 53 | ) 54 | .frame(width: 38, height: 38) 55 | .rotationEffect(.degrees(isAnimating ? 360 : 0)) 56 | 57 | // Label 58 | Text("Processing") 59 | .font(.system(size: 13, weight: .medium)) 60 | .foregroundColor(.secondary) 61 | .offset(y: 46) 62 | } 63 | } 64 | } 65 | .onAppear { 66 | withAnimation( 67 | .linear(duration: duration) 68 | .repeatForever(autoreverses: false) 69 | ) { 70 | isAnimating = true 71 | } 72 | } 73 | } 74 | } 75 | 76 | #Preview { 77 | LoaderView() 78 | .frame(width: 400, height: 400) 79 | } 80 | -------------------------------------------------------------------------------- /Quiebro/MenuBarView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AppKit 3 | 4 | struct MenuBarView: View { 5 | @ObservedObject var updater: UpdateChecker 6 | @EnvironmentObject var menuBarController: MenuBarController 7 | @Environment(\.dismiss) var dismiss 8 | 9 | private var appIcon: NSImage { 10 | if let bundleIcon = NSImage(named: NSImage.applicationIconName) { 11 | return bundleIcon 12 | } 13 | return NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) 14 | } 15 | 16 | var body: some View { 17 | VStack(spacing: 16) { 18 | // App Icon and Version 19 | VStack(spacing: 8) { 20 | Image(nsImage: appIcon) 21 | .resizable() 22 | .aspectRatio(contentMode: .fit) 23 | .frame(width: 64, height: 64) 24 | 25 | Text("Version \(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0")") 26 | .font(.subheadline) 27 | .foregroundColor(.secondary) 28 | } 29 | .padding(.top, 16) 30 | 31 | // Status Section 32 | Group { 33 | if updater.isChecking { 34 | VStack(spacing: 8) { 35 | ProgressView() 36 | .scaleEffect(1.2) 37 | Text("Checking for updates...") 38 | .font(.headline) 39 | .foregroundColor(.secondary) 40 | } 41 | } else if let error = updater.error { 42 | VStack(spacing: 8) { 43 | Image(systemName: "xmark.circle.fill") 44 | .font(.system(size: 28)) 45 | .foregroundColor(.red) 46 | Text(error) 47 | .font(.subheadline) 48 | .foregroundColor(.secondary) 49 | .multilineTextAlignment(.center) 50 | } 51 | } else if updater.updateAvailable { 52 | VStack(spacing: 12) { 53 | Image(systemName: "arrow.down.circle.fill") 54 | .font(.system(size: 28)) 55 | .foregroundColor(.blue) 56 | 57 | if let version = updater.latestVersion { 58 | Text("Version \(version) Available") 59 | .font(.headline) 60 | } 61 | 62 | if let notes = updater.releaseNotes { 63 | ScrollView { 64 | Text(notes) 65 | .font(.footnote) 66 | .foregroundColor(.secondary) 67 | .multilineTextAlignment(.center) 68 | .padding(.horizontal) 69 | } 70 | .frame(maxHeight: 80) 71 | } 72 | 73 | Button { 74 | if let url = updater.downloadURL { 75 | NSWorkspace.shared.open(url) 76 | dismiss() 77 | } 78 | } label: { 79 | Text("Download Update") 80 | .frame(maxWidth: 200) 81 | } 82 | .buttonStyle(.borderedProminent) 83 | } 84 | } else { 85 | VStack(spacing: 8) { 86 | Image(systemName: "checkmark.circle.fill") 87 | .font(.system(size: 28)) 88 | .foregroundColor(.green) 89 | Text("Quiebro is up to date") 90 | .font(.headline) 91 | } 92 | } 93 | } 94 | .frame(maxWidth: .infinity, alignment: .center) 95 | .padding(.vertical, 8) 96 | 97 | Divider() 98 | 99 | // Bottom Buttons 100 | HStack(spacing: 16) { 101 | Button("Check Again") { 102 | updater.checkForUpdates() 103 | } 104 | .buttonStyle(.plain) 105 | .foregroundColor(.blue) 106 | 107 | Button("Close") { 108 | dismiss() 109 | } 110 | .buttonStyle(.plain) 111 | .foregroundColor(.secondary) 112 | } 113 | .padding(.bottom, 16) 114 | 115 | Text("Built by [Nuance](https://nuancedev.vercel.app)") 116 | .font(.footnote) 117 | .foregroundColor(.secondary) 118 | .padding(.bottom, 8) 119 | } 120 | .padding(.horizontal) 121 | .frame(width: 300) 122 | .fixedSize(horizontal: false, vertical: true) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Quiebro/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Quiebro/Quiebro.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Quiebro/QuiebroApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AppKit 3 | 4 | @main 5 | struct QuiebroApp: App { 6 | @AppStorage("isDarkMode") private var isDarkMode = false 7 | @StateObject private var updater = UpdateChecker() 8 | @State private var showingUpdateSheet = false 9 | 10 | var body: some Scene { 11 | WindowGroup { 12 | ContentView() 13 | .preferredColorScheme(isDarkMode ? .dark : .light) 14 | .background(WindowAccessor()) 15 | .sheet(isPresented: $showingUpdateSheet) { 16 | UpdateView(updater: updater) 17 | } 18 | .onAppear { 19 | updater.checkForUpdates() 20 | updater.onUpdateAvailable = { 21 | showingUpdateSheet = true 22 | } 23 | } 24 | } 25 | .windowStyle(HiddenTitleBarWindowStyle()) 26 | .commands { 27 | CommandGroup(after: .appInfo) { 28 | Button("Check for Updates...") { 29 | showingUpdateSheet = true 30 | updater.checkForUpdates() 31 | } 32 | .keyboardShortcut("U", modifiers: [.command]) 33 | 34 | if updater.updateAvailable { 35 | Button("Download Update") { 36 | if let url = updater.downloadURL { 37 | NSWorkspace.shared.open(url) 38 | } 39 | } 40 | } 41 | 42 | Divider() 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Quiebro/Resources/Extensions.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension NSPasteboard { 4 | func getImageFromPasteboard() -> NSImage? { 5 | if let image = NSImage(pasteboard: self) { 6 | return image 7 | } 8 | 9 | // Try reading from file URL if image is not directly available 10 | if let url = self.pasteboardItems?.first?.string(forType: .fileURL), 11 | let fileURL = URL(string: url), 12 | let image = NSImage(contentsOf: fileURL) { 13 | return image 14 | } 15 | 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Quiebro/Resources/MenuBarController.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AppKit 3 | 4 | class MenuBarController: NSObject, ObservableObject { 5 | @Published private(set) var updater = UpdateChecker() 6 | private var statusItem: NSStatusItem! 7 | 8 | override init() { 9 | super.init() 10 | 11 | // Initialize status item on main queue 12 | DispatchQueue.main.async { 13 | self.setupMenuBar() 14 | self.updater.checkForUpdates() 15 | } 16 | } 17 | 18 | private func setupMenuBar() { 19 | // Create the status item 20 | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 21 | 22 | if let button = statusItem.button { 23 | button.image = NSImage(systemSymbolName: "checkmark.circle", accessibilityDescription: "Update Status") 24 | 25 | // Create the menu 26 | let menu = NSMenu() 27 | menu.delegate = self 28 | 29 | // Set the menu 30 | statusItem.menu = menu 31 | 32 | // Update the button image when the status changes 33 | updater.onStatusChange = { [weak self] newIcon in 34 | guard self != nil else { return } 35 | DispatchQueue.main.async { 36 | button.image = NSImage(systemSymbolName: newIcon, accessibilityDescription: "Update Status") 37 | } 38 | } 39 | } 40 | } 41 | 42 | @objc private func checkForUpdates() { 43 | updater.checkForUpdates() 44 | } 45 | 46 | @objc private func downloadUpdate() { 47 | if let url = updater.downloadURL { 48 | NSWorkspace.shared.open(url) 49 | } 50 | } 51 | } 52 | 53 | extension MenuBarController: NSMenuDelegate { 54 | func menuWillOpen(_ menu: NSMenu) { 55 | // Clear existing items 56 | menu.removeAllItems() 57 | 58 | // Add version 59 | let versionItem = NSMenuItem(title: "Quiebro v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0")", action: nil, keyEquivalent: "") 60 | versionItem.isEnabled = false 61 | menu.addItem(versionItem) 62 | 63 | menu.addItem(NSMenuItem.separator()) 64 | 65 | // Add status 66 | if updater.isChecking { 67 | let checkingItem = NSMenuItem(title: "Checking for updates...", action: nil, keyEquivalent: "") 68 | checkingItem.isEnabled = false 69 | menu.addItem(checkingItem) 70 | } else if updater.updateAvailable { 71 | if let version = updater.latestVersion { 72 | let availableItem = NSMenuItem(title: "Version \(version) Available", action: nil, keyEquivalent: "") 73 | availableItem.isEnabled = false 74 | menu.addItem(availableItem) 75 | } 76 | let downloadItem = NSMenuItem(title: "Download Update", action: #selector(downloadUpdate), keyEquivalent: "") 77 | downloadItem.target = self 78 | menu.addItem(downloadItem) 79 | } else { 80 | let upToDateItem = NSMenuItem(title: "App is up to date", action: nil, keyEquivalent: "") 81 | upToDateItem.isEnabled = false 82 | menu.addItem(upToDateItem) 83 | } 84 | 85 | menu.addItem(NSMenuItem.separator()) 86 | 87 | // Add check for updates item 88 | let checkItem = NSMenuItem(title: "Check for Updates...", action: #selector(checkForUpdates), keyEquivalent: "u") 89 | checkItem.target = self 90 | menu.addItem(checkItem) 91 | } 92 | 93 | func menuDidClose(_ menu: NSMenu) { 94 | // Optional: Handle menu closing 95 | } 96 | 97 | func numberOfItems(in menu: NSMenu) -> Int { 98 | // Let the menu build dynamically 99 | return menu.numberOfItems 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Quiebro/Resources/UpdateChecker.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct GitHubRelease: Codable { 4 | let tagName: String 5 | let name: String 6 | let body: String 7 | let htmlUrl: String 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case tagName = "tag_name" 11 | case name 12 | case body 13 | case htmlUrl = "html_url" 14 | } 15 | } 16 | 17 | class UpdateChecker: ObservableObject { 18 | @Published var updateAvailable = false 19 | @Published var latestVersion: String? 20 | @Published var releaseNotes: String? 21 | @Published var downloadURL: URL? 22 | @Published var isChecking = false 23 | @Published var error: String? 24 | @Published var statusIcon: String = "checkmark.circle" 25 | 26 | var onStatusChange: ((String) -> Void)? 27 | var onUpdateAvailable: (() -> Void)? 28 | 29 | private let currentVersion: String 30 | private let githubRepo: String 31 | private var updateCheckTimer: Timer? 32 | 33 | init() { 34 | self.currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0" 35 | self.githubRepo = "nuance-dev/Quiebro" 36 | setupTimer() 37 | updateStatusIcon() 38 | } 39 | 40 | private func setupTimer() { 41 | // Initial check after 2 seconds 42 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in 43 | self?.checkForUpdates() 44 | } 45 | 46 | // Periodic check every 24 hours 47 | updateCheckTimer = Timer.scheduledTimer(withTimeInterval: 24 * 60 * 60, repeats: true) { [weak self] _ in 48 | self?.checkForUpdates() 49 | } 50 | } 51 | 52 | private func updateStatusIcon() { 53 | DispatchQueue.main.async { [weak self] in 54 | guard let self = self else { return } 55 | if self.isChecking { 56 | self.statusIcon = "arrow.triangle.2.circlepath" 57 | } else { 58 | self.statusIcon = self.updateAvailable ? "exclamationmark.circle" : "checkmark.circle" 59 | } 60 | self.onStatusChange?(self.statusIcon) 61 | } 62 | } 63 | 64 | func checkForUpdates() { 65 | print("Checking for updates...") 66 | print("Current version: \(currentVersion)") 67 | 68 | isChecking = true 69 | updateStatusIcon() 70 | error = nil 71 | 72 | let baseURL = "https://api.github.com/repos/\(githubRepo)/releases/latest" 73 | guard let url = URL(string: baseURL) else { 74 | error = "Invalid GitHub repository URL" 75 | isChecking = false 76 | updateStatusIcon() 77 | return 78 | } 79 | 80 | var request = URLRequest(url: url) 81 | request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept") 82 | request.setValue("Quiebro-App/\(currentVersion)", forHTTPHeaderField: "User-Agent") 83 | 84 | URLSession.shared.dataTask(with: request) { [weak self] data, response, error in 85 | DispatchQueue.main.async { 86 | self?.handleUpdateResponse(data: data, response: response as? HTTPURLResponse, error: error) 87 | } 88 | }.resume() 89 | } 90 | 91 | private func handleUpdateResponse(data: Data?, response: HTTPURLResponse?, error: Error?) { 92 | defer { 93 | isChecking = false 94 | updateStatusIcon() 95 | } 96 | 97 | if let error = error { 98 | print("Network error: \(error)") 99 | self.error = "Network error: \(error.localizedDescription)" 100 | return 101 | } 102 | 103 | guard let response = response else { 104 | print("Invalid response") 105 | self.error = "Invalid response from server" 106 | return 107 | } 108 | 109 | print("Response status code: \(response.statusCode)") 110 | 111 | guard response.statusCode == 200 else { 112 | self.error = "Server error: \(response.statusCode)" 113 | return 114 | } 115 | 116 | guard let data = data else { 117 | self.error = "No data received" 118 | return 119 | } 120 | 121 | do { 122 | 123 | let decoder = JSONDecoder() 124 | let release = try decoder.decode(GitHubRelease.self, from: data) 125 | 126 | let cleanLatestVersion = release.tagName.replacingOccurrences(of: "v", with: "") 127 | print("Latest version: \(cleanLatestVersion)") 128 | print("Current version for comparison: \(currentVersion)") 129 | 130 | updateAvailable = compareVersions(current: currentVersion, latest: cleanLatestVersion) 131 | if updateAvailable { 132 | DispatchQueue.main.async { 133 | self.onUpdateAvailable?() 134 | } 135 | } 136 | 137 | latestVersion = cleanLatestVersion 138 | releaseNotes = release.body 139 | downloadURL = URL(string: release.htmlUrl) 140 | 141 | updateAvailable = compareVersions(current: currentVersion, latest: cleanLatestVersion) 142 | print("Update available: \(updateAvailable)") 143 | 144 | } catch { 145 | print("Parsing error: \(error)") 146 | self.error = "Failed to parse response: \(error.localizedDescription)" 147 | } 148 | } 149 | 150 | private func compareVersions(current: String, latest: String) -> Bool { 151 | // Clean and split versions 152 | let currentParts = current.replacingOccurrences(of: "v", with: "") 153 | .split(separator: ".") 154 | .compactMap { Int($0) } 155 | 156 | let latestParts = latest.replacingOccurrences(of: "v", with: "") 157 | .split(separator: ".") 158 | .compactMap { Int($0) } 159 | 160 | 161 | // Ensure we have at least 3 components (major.minor.patch) 162 | let paddedCurrent = currentParts + Array(repeating: 0, count: max(3 - currentParts.count, 0)) 163 | let paddedLatest = latestParts + Array(repeating: 0, count: max(3 - latestParts.count, 0)) 164 | 165 | 166 | // Compare each version component 167 | for i in 0.. paddedCurrent[i] { 169 | return true 170 | } else if paddedLatest[i] < paddedCurrent[i] { 171 | return false 172 | } 173 | } 174 | 175 | print("Versions are equal") 176 | return false 177 | } 178 | 179 | deinit { 180 | updateCheckTimer?.invalidate() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Quiebro/UI Components/ButtonGroup.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ToolbarButton: View { 4 | let title: String 5 | let icon: String 6 | let action: () -> Void 7 | let isFirst: Bool 8 | let isLast: Bool 9 | 10 | var body: some View { 11 | Button(action: action) { 12 | HStack(spacing: 6) { 13 | Image(systemName: icon) 14 | .font(.system(size: 14)) 15 | Text(title) 16 | .font(.system(size: 13, weight: .medium)) 17 | } 18 | .frame(height: 36) 19 | .padding(.horizontal, 16) 20 | .foregroundColor(.primary) 21 | .background(Color.clear) 22 | .contentShape(Rectangle()) 23 | } 24 | .buttonStyle(PlainButtonStyle()) 25 | } 26 | } 27 | 28 | struct ButtonDivider: View { 29 | var body: some View { 30 | Divider() 31 | .frame(height: 24) 32 | } 33 | } 34 | 35 | struct ButtonGroup: View { 36 | let buttons: [(title: String, icon: String, action: () -> Void)] 37 | 38 | var body: some View { 39 | HStack(spacing: 0) { 40 | ForEach(Array(buttons.enumerated()), id: \.offset) { index, button in 41 | if index > 0 { 42 | ButtonDivider() 43 | } 44 | 45 | ToolbarButton( 46 | title: button.title, 47 | icon: button.icon, 48 | action: button.action, 49 | isFirst: index == 0, 50 | isLast: index == buttons.count - 1 51 | ) 52 | } 53 | } 54 | .background(backgroundView) 55 | } 56 | 57 | private var backgroundView: some View { 58 | ZStack { 59 | // Base background 60 | RoundedRectangle(cornerRadius: 12) 61 | .fill(Color(NSColor.windowBackgroundColor).opacity(0.5)) 62 | 63 | // Subtle border 64 | RoundedRectangle(cornerRadius: 12) 65 | .strokeBorder(Color.primary.opacity(0.1), lineWidth: 1) 66 | 67 | // Glass effect overlay 68 | RoundedRectangle(cornerRadius: 12) 69 | .stroke(Color.white.opacity(0.1), lineWidth: 1) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Quiebro/UI Components/GlassButtonStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GlassButtonStyle: ButtonStyle { 4 | func makeBody(configuration: Configuration) -> some View { 5 | configuration.label 6 | .padding(.horizontal, 20) 7 | .padding(.vertical, 10) 8 | .background( 9 | RoundedRectangle(cornerRadius: 8) 10 | .fill(Color.primary.opacity(0.1)) 11 | .overlay( 12 | RoundedRectangle(cornerRadius: 8) 13 | .stroke(Color.primary.opacity(0.2), lineWidth: 1) 14 | ) 15 | ) 16 | .opacity(configuration.isPressed ? 0.8 : 1.0) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Quiebro/UI Components/TitleBarAccessory.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct TitleBarAccessory: View { 4 | @AppStorage("isDarkMode") private var isDarkMode = false 5 | 6 | var body: some View { 7 | Button(action: { 8 | isDarkMode.toggle() 9 | }) { 10 | Image(systemName: isDarkMode ? "sun.max.fill" : "moon.fill") 11 | .foregroundColor(.primary) 12 | } 13 | .buttonStyle(PlainButtonStyle()) 14 | .frame(width: 30, height: 30) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Quiebro/UI Components/UpdateView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct UpdateView: View { 4 | @Environment(\.dismiss) private var dismiss 5 | @ObservedObject var updater: UpdateChecker 6 | 7 | var body: some View { 8 | VStack(spacing: 20) { 9 | Image(systemName: "arrow.down.circle.fill") 10 | .font(.system(size: 48)) 11 | .foregroundStyle(.blue) 12 | 13 | Text(updater.updateAvailable ? "Update Available" : "No Updates Available") 14 | .font(.title2) 15 | .fontWeight(.semibold) 16 | 17 | Text(updater.updateAvailable ? 18 | "A new version of Quiebro is available. Would you like to download it now?" : 19 | "You're running the latest version of Quiebro.") 20 | .multilineTextAlignment(.center) 21 | .foregroundStyle(.secondary) 22 | 23 | if updater.updateAvailable { 24 | HStack(spacing: 12) { 25 | Button("Later") { 26 | dismiss() 27 | } 28 | .buttonStyle(.plain) 29 | .foregroundStyle(.secondary) 30 | 31 | Button { 32 | if let url = updater.downloadURL { 33 | NSWorkspace.shared.open(url) 34 | } 35 | dismiss() 36 | } label: { 37 | Text("Download") 38 | .frame(width: 100) 39 | } 40 | .buttonStyle(.borderedProminent) 41 | .tint(.blue) 42 | } 43 | } else { 44 | Button("OK") { 45 | dismiss() 46 | } 47 | .buttonStyle(.borderedProminent) 48 | .tint(.blue) 49 | } 50 | } 51 | .padding(30) 52 | .frame(width: 400) 53 | } 54 | } -------------------------------------------------------------------------------- /Quiebro/UI Components/VisualEffectBlur.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct VisualEffectBlur: NSViewRepresentable { 4 | var material: NSVisualEffectView.Material 5 | var blendingMode: NSVisualEffectView.BlendingMode 6 | 7 | func makeNSView(context: Context) -> NSVisualEffectView { 8 | let view = NSVisualEffectView() 9 | view.state = .active 10 | view.material = material 11 | view.blendingMode = blendingMode 12 | view.alphaValue = 0.9 13 | return view 14 | } 15 | 16 | func updateNSView(_ nsView: NSVisualEffectView, context: Context) {} 17 | } 18 | -------------------------------------------------------------------------------- /Quiebro/UI Components/WindowAccessor.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct WindowAccessor: NSViewRepresentable { 4 | func makeNSView(context: Context) -> NSView { 5 | let nsView = NSView() 6 | 7 | DispatchQueue.main.async { 8 | if let window = nsView.window { 9 | let titleBarAccessory = NSTitlebarAccessoryViewController() 10 | let hostingView = NSHostingView(rootView: TitleBarAccessory()) 11 | 12 | hostingView.frame.size = hostingView.fittingSize 13 | titleBarAccessory.view = hostingView 14 | titleBarAccessory.layoutAttribute = .trailing 15 | 16 | window.addTitlebarAccessoryViewController(titleBarAccessory) 17 | } 18 | } 19 | 20 | return nsView 21 | } 22 | 23 | func updateNSView(_ nsView: NSView, context: Context) {} 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quiebro - A macOS Native File Splitter & Joiner 2 | 3 | A sleek, free native macOS app that splits files into three encrypted pieces and lets you join them back together. Perfect for securely sharing sensitive files across different channels. 4 | 5 | Note: This app requires macOS version 14+ 6 | 7 | ![quiebro-banner](https://github.com/user-attachments/assets/117759b7-ebd0-4b04-b8bf-482169ea55ff) 8 | 9 | ## Features 10 | 11 | - **Split Files**: Break any file into 3 encrypted pieces 12 | - **Join Files**: Easily combine the pieces back into the original file 13 | - **Multiple Input Methods**: Drag & drop, paste (⌘V), or click to upload 14 | - **Native Performance**: Built with SwiftUI for optimal processing 15 | - **Dark and Light modes**: Automatically matches your system theme 16 | 17 | https://github.com/user-attachments/assets/ea628ef4-4e09-498a-ad5c-2a093028c669 18 | 19 | ## 💻 Get it 20 | 21 | Download from the [releases](https://github.com/nuance-dev/Quiebro/releases/) page. 22 | 23 | ## 🔒 Security 24 | 25 | - Each piece is individually encrypted using AES-GCM encryption 26 | - Unique salt and key derivation for each piece 27 | - Original file can only be recovered with all 3 pieces 28 | - Two modes available: 29 | - **Standard Mode**: Basic file splitting with LZFSE compression 30 | - **Secure Mode**: Adds military-grade encryption to each piece 31 | - Perfect for distributing sensitive files across different channels 32 | 33 | ### How Secure Mode Works 34 | 35 | - Each piece is independently encrypted using AES-GCM 36 | - Keys are derived using HKDF with SHA-256 37 | - Unique salt generated for each piece 38 | - Key material is derived from the original file's hash 39 | - Without all three pieces and the original file hash, decryption is computationally infeasible 40 | 41 | ## 🥑 Fun facts 42 | 43 | - Yes this app can be used to host the best treasure hunt of all time 44 | 45 | ## 🤝 Contributing 46 | 47 | We welcome contributions! Here's how you can help: 48 | 49 | 1. Fork the repository 50 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 51 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 52 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 53 | 5. Open a Pull Request 54 | 55 | Please ensure your PR: 56 | 57 | - Follows the existing code style 58 | - Includes appropriate tests 59 | - Updates documentation as needed 60 | 61 | ## 📝 License 62 | 63 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 64 | 65 | ## 🔗 Links 66 | 67 | - Website: [Nuanc.me](https://nuanc.me) 68 | - Report issues: [GitHub Issues](https://github.com/nuance-dev/Quiebro/issues) 69 | - Follow updates: [@Nuanced](https://twitter.com/Nuancedev) 70 | --------------------------------------------------------------------------------