├── .github └── FUNDING.yml ├── .gitignore ├── Canvas.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Canvas ├── App │ ├── AppUpdater.swift │ └── CanvasApp.swift ├── Enums │ ├── AppSidebar.swift │ └── AppSidebarItem.swift ├── Extensions │ └── Defaults+Keys.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon_1024x1024.png │ │ │ ├── AppIcon_128x128.png │ │ │ ├── AppIcon_16x16.png │ │ │ ├── AppIcon_256x256 1.png │ │ │ ├── AppIcon_256x256.png │ │ │ ├── AppIcon_32x32 1.png │ │ │ ├── AppIcon_32x32.png │ │ │ ├── AppIcon_512x512 1.png │ │ │ ├── AppIcon_512x512.png │ │ │ ├── AppIcon_64x64.png │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Canvas.entitlements │ ├── Info.plist │ └── Localizable.xcstrings └── Views │ ├── AppSidebarView.swift │ └── AppView.swift ├── CoreExtensions ├── .gitignore ├── Package.swift └── Sources │ └── CoreExtensions │ ├── NSImage+Write.swift │ └── Optional+Utils.swift ├── CoreModels ├── .gitignore ├── Package.swift └── Sources │ └── CoreModels │ ├── DalleModel.swift │ └── DalleModelInfo.swift ├── CoreViewModels ├── .gitignore ├── Package.swift └── Sources │ └── CoreViewModels │ ├── DalleModelInfoViewModel.swift │ └── DalleViewModel.swift ├── CoreViews ├── .gitignore ├── Package.swift └── Sources │ └── CoreViews │ ├── Buttons │ └── CircleButton.swift │ ├── Extensions │ └── URL+Preview.swift │ ├── ImageResult │ ├── ImageResultContextMenu.swift │ ├── ImageResultListItemView.swift │ └── ImageResultListView.swift │ ├── PromptField │ ├── PromptField.swift │ └── PromptFieldFooterText.swift │ └── Texts │ └── FootnoteText.swift ├── ImageEditModule ├── .gitignore ├── Package.swift └── Sources │ └── ImageEditModule │ └── ImageEditView.swift ├── ImageGenerationModule ├── .gitignore ├── Package.swift └── Sources │ └── ImageGenerationModule │ └── ImageGenerationView.swift ├── ImagePreferencesModule ├── .gitignore ├── Package.swift └── Sources │ └── ImagePreferencesModule │ ├── Extensions │ └── NSImage+Utils.swift │ ├── ImagePreferencesView.swift │ └── Subviews │ ├── ImagePickers │ ├── ImagePicker.swift │ ├── ImagePickerButton.swift │ ├── ImagePickerEmptyView.swift │ └── ImagePickerPreviewView.swift │ ├── Pickers │ ├── ModelPicker.swift │ ├── NumberPicker.swift │ ├── QualityPicker.swift │ ├── SizePicker.swift │ └── StylePicker.swift │ └── SectionHeader.swift ├── ImageVariationModule ├── .gitignore ├── Package.swift └── Sources │ └── ImageVariationModule │ └── ImageVariationView.swift ├── LICENSE ├── ModelPricingModule ├── .gitignore ├── Package.swift └── Sources │ └── ModelPricingModule │ └── ModelPricingView.swift ├── README.md ├── SettingsModule ├── .gitignore ├── Package.swift └── Sources │ └── SettingsModule │ ├── Extensions │ └── Defaults+Keys.swift │ ├── Managers │ └── SettingsManager.swift │ └── Views │ ├── GeneralView.swift │ ├── SettingsView.swift │ └── Subviews │ ├── APIKeyPicker.swift │ └── AutosavePicker.swift ├── appcast.xml ├── assets ├── banner-night.jpg ├── banner.jpg ├── press-kit.zip ├── screenshot-dark.png └── screenshot.png └── model-info.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ["kevinhermawan"] 2 | custom: 3 | [ 4 | "https://buymeacoffee.com/kevinhermawan", 5 | "https://trakteer.id/kevinhermawan", 6 | ] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## General 2 | .DS_Store 3 | *.moved-aside 4 | *.xccheckout 5 | *.xcscmblueprint 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## fastlane 23 | fastlane/report.xml 24 | fastlane/Preview.html 25 | fastlane/screenshots 26 | fastlane/test_output 27 | fastlane/gems -------------------------------------------------------------------------------- /Canvas.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0A03693D2B4D6FBB00D7DAD4 /* SettingsModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A03693C2B4D6FBB00D7DAD4 /* SettingsModule */; }; 11 | 0A20744D2B3CD3E5008516D1 /* ModelPricingModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A20744C2B3CD3E5008516D1 /* ModelPricingModule */; }; 12 | 0A211A2A2C42D1C900B9029F /* AppInfo in Frameworks */ = {isa = PBXBuildFile; productRef = 0A211A292C42D1C900B9029F /* AppInfo */; }; 13 | 0A809A402B39752200C9A015 /* CanvasApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A809A3F2B39752200C9A015 /* CanvasApp.swift */; }; 14 | 0A809A462B39752300C9A015 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A809A452B39752300C9A015 /* Assets.xcassets */; }; 15 | 0A809A492B39752300C9A015 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A809A482B39752300C9A015 /* Preview Assets.xcassets */; }; 16 | 0A809A582B39793700C9A015 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A809A572B39793700C9A015 /* AppView.swift */; }; 17 | 0A809A5B2B397B6100C9A015 /* CoreViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A5A2B397B6100C9A015 /* CoreViewModels */; }; 18 | 0A809A612B397C3B00C9A015 /* CoreViews in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A602B397C3B00C9A015 /* CoreViews */; }; 19 | 0A809A632B397C3B00C9A015 /* ImageEditModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A622B397C3B00C9A015 /* ImageEditModule */; }; 20 | 0A809A652B397C3B00C9A015 /* ImageGenerationModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A642B397C3B00C9A015 /* ImageGenerationModule */; }; 21 | 0A809A672B397C3B00C9A015 /* ImageVariationModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A662B397C3B00C9A015 /* ImageVariationModule */; }; 22 | 0A809A6A2B397DB100C9A015 /* AppSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A809A692B397DB100C9A015 /* AppSidebar.swift */; }; 23 | 0A809A6C2B397E1C00C9A015 /* AppSidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A809A6B2B397E1C00C9A015 /* AppSidebarItem.swift */; }; 24 | 0A809A6F2B39814F00C9A015 /* AppSidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A809A6E2B39814F00C9A015 /* AppSidebarView.swift */; }; 25 | 0A809A752B398AEE00C9A015 /* ImagePreferencesModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A742B398AEE00C9A015 /* ImagePreferencesModule */; }; 26 | 0A809A792B39986D00C9A015 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A782B39986D00C9A015 /* Defaults */; }; 27 | 0A809A7C2B39988000C9A015 /* Defaults+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A809A7B2B39988000C9A015 /* Defaults+Keys.swift */; }; 28 | 0A809A7F2B39DDB100C9A015 /* CoreExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A7E2B39DDB100C9A015 /* CoreExtensions */; }; 29 | 0A809A812B39DDBA00C9A015 /* CoreModels in Frameworks */ = {isa = PBXBuildFile; productRef = 0A809A802B39DDBA00C9A015 /* CoreModels */; }; 30 | 0AE400612B3A98E600307732 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 0AE400602B3A98E600307732 /* Localizable.xcstrings */; }; 31 | 0AE400642B3AD5F800307732 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 0AE400632B3AD5F800307732 /* Sparkle */; }; 32 | 0AE400662B3AD84700307732 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE400652B3AD84700307732 /* AppUpdater.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 0A03693B2B4D6F7600D7DAD4 /* SettingsModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SettingsModule; sourceTree = ""; }; 37 | 0A20744B2B3CD385008516D1 /* ModelPricingModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ModelPricingModule; sourceTree = ""; }; 38 | 0A809A3C2B39752200C9A015 /* Canvas.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Canvas.app; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 0A809A3F2B39752200C9A015 /* CanvasApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasApp.swift; sourceTree = ""; }; 40 | 0A809A452B39752300C9A015 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 0A809A482B39752300C9A015 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 42 | 0A809A4A2B39752300C9A015 /* Canvas.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Canvas.entitlements; sourceTree = ""; }; 43 | 0A809A512B39771F00C9A015 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 0A809A572B39793700C9A015 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; 45 | 0A809A592B397B2E00C9A015 /* CoreViewModels */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CoreViewModels; sourceTree = ""; }; 46 | 0A809A5C2B397B8800C9A015 /* ImageGenerationModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ImageGenerationModule; sourceTree = ""; }; 47 | 0A809A5D2B397BAF00C9A015 /* ImageEditModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ImageEditModule; sourceTree = ""; }; 48 | 0A809A5E2B397BE800C9A015 /* ImageVariationModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ImageVariationModule; sourceTree = ""; }; 49 | 0A809A5F2B397C2500C9A015 /* CoreViews */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CoreViews; sourceTree = ""; }; 50 | 0A809A692B397DB100C9A015 /* AppSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebar.swift; sourceTree = ""; }; 51 | 0A809A6B2B397E1C00C9A015 /* AppSidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarItem.swift; sourceTree = ""; }; 52 | 0A809A6E2B39814F00C9A015 /* AppSidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarView.swift; sourceTree = ""; }; 53 | 0A809A732B398AB200C9A015 /* ImagePreferencesModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ImagePreferencesModule; sourceTree = ""; }; 54 | 0A809A762B398F1600C9A015 /* CoreModels */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CoreModels; sourceTree = ""; }; 55 | 0A809A7B2B39988000C9A015 /* Defaults+Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults+Keys.swift"; sourceTree = ""; }; 56 | 0A809A7D2B39D65000C9A015 /* CoreExtensions */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CoreExtensions; sourceTree = ""; }; 57 | 0AE400602B3A98E600307732 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 58 | 0AE400652B3AD84700307732 /* AppUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdater.swift; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 0A809A392B39752200C9A015 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | 0A20744D2B3CD3E5008516D1 /* ModelPricingModule in Frameworks */, 67 | 0A809A632B397C3B00C9A015 /* ImageEditModule in Frameworks */, 68 | 0A809A612B397C3B00C9A015 /* CoreViews in Frameworks */, 69 | 0A809A652B397C3B00C9A015 /* ImageGenerationModule in Frameworks */, 70 | 0A03693D2B4D6FBB00D7DAD4 /* SettingsModule in Frameworks */, 71 | 0A809A5B2B397B6100C9A015 /* CoreViewModels in Frameworks */, 72 | 0A809A672B397C3B00C9A015 /* ImageVariationModule in Frameworks */, 73 | 0A809A752B398AEE00C9A015 /* ImagePreferencesModule in Frameworks */, 74 | 0AE400642B3AD5F800307732 /* Sparkle in Frameworks */, 75 | 0A809A812B39DDBA00C9A015 /* CoreModels in Frameworks */, 76 | 0A809A792B39986D00C9A015 /* Defaults in Frameworks */, 77 | 0A809A7F2B39DDB100C9A015 /* CoreExtensions in Frameworks */, 78 | 0A211A2A2C42D1C900B9029F /* AppInfo in Frameworks */, 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXFrameworksBuildPhase section */ 83 | 84 | /* Begin PBXGroup section */ 85 | 0A809A332B39752200C9A015 = { 86 | isa = PBXGroup; 87 | children = ( 88 | 0A809A7D2B39D65000C9A015 /* CoreExtensions */, 89 | 0A809A762B398F1600C9A015 /* CoreModels */, 90 | 0A809A592B397B2E00C9A015 /* CoreViewModels */, 91 | 0A809A5F2B397C2500C9A015 /* CoreViews */, 92 | 0A809A5D2B397BAF00C9A015 /* ImageEditModule */, 93 | 0A809A5C2B397B8800C9A015 /* ImageGenerationModule */, 94 | 0A809A732B398AB200C9A015 /* ImagePreferencesModule */, 95 | 0A809A5E2B397BE800C9A015 /* ImageVariationModule */, 96 | 0A20744B2B3CD385008516D1 /* ModelPricingModule */, 97 | 0A03693B2B4D6F7600D7DAD4 /* SettingsModule */, 98 | 0A809A3E2B39752200C9A015 /* Canvas */, 99 | 0A809A522B39772800C9A015 /* Frameworks */, 100 | 0A809A3D2B39752200C9A015 /* Products */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 0A809A3D2B39752200C9A015 /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 0A809A3C2B39752200C9A015 /* Canvas.app */, 108 | ); 109 | name = Products; 110 | sourceTree = ""; 111 | }; 112 | 0A809A3E2B39752200C9A015 /* Canvas */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 0A809A562B39785300C9A015 /* App */, 116 | 0A809A682B397DA600C9A015 /* Enums */, 117 | 0A809A7A2B39987400C9A015 /* Extensions */, 118 | 0A809A472B39752300C9A015 /* Preview Content */, 119 | 0A809A552B3977C900C9A015 /* Resources */, 120 | 0A809A6D2B39814500C9A015 /* Views */, 121 | ); 122 | path = Canvas; 123 | sourceTree = ""; 124 | }; 125 | 0A809A472B39752300C9A015 /* Preview Content */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 0A809A482B39752300C9A015 /* Preview Assets.xcassets */, 129 | ); 130 | path = "Preview Content"; 131 | sourceTree = ""; 132 | }; 133 | 0A809A522B39772800C9A015 /* Frameworks */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | ); 137 | name = Frameworks; 138 | sourceTree = ""; 139 | }; 140 | 0A809A552B3977C900C9A015 /* Resources */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 0A809A512B39771F00C9A015 /* Info.plist */, 144 | 0A809A4A2B39752300C9A015 /* Canvas.entitlements */, 145 | 0A809A452B39752300C9A015 /* Assets.xcassets */, 146 | 0AE400602B3A98E600307732 /* Localizable.xcstrings */, 147 | ); 148 | path = Resources; 149 | sourceTree = ""; 150 | }; 151 | 0A809A562B39785300C9A015 /* App */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 0A809A3F2B39752200C9A015 /* CanvasApp.swift */, 155 | 0AE400652B3AD84700307732 /* AppUpdater.swift */, 156 | ); 157 | path = App; 158 | sourceTree = ""; 159 | }; 160 | 0A809A682B397DA600C9A015 /* Enums */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 0A809A692B397DB100C9A015 /* AppSidebar.swift */, 164 | 0A809A6B2B397E1C00C9A015 /* AppSidebarItem.swift */, 165 | ); 166 | path = Enums; 167 | sourceTree = ""; 168 | }; 169 | 0A809A6D2B39814500C9A015 /* Views */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 0A809A572B39793700C9A015 /* AppView.swift */, 173 | 0A809A6E2B39814F00C9A015 /* AppSidebarView.swift */, 174 | ); 175 | path = Views; 176 | sourceTree = ""; 177 | }; 178 | 0A809A7A2B39987400C9A015 /* Extensions */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | 0A809A7B2B39988000C9A015 /* Defaults+Keys.swift */, 182 | ); 183 | path = Extensions; 184 | sourceTree = ""; 185 | }; 186 | /* End PBXGroup section */ 187 | 188 | /* Begin PBXNativeTarget section */ 189 | 0A809A3B2B39752200C9A015 /* Canvas */ = { 190 | isa = PBXNativeTarget; 191 | buildConfigurationList = 0A809A4D2B39752300C9A015 /* Build configuration list for PBXNativeTarget "Canvas" */; 192 | buildPhases = ( 193 | 0A809A382B39752200C9A015 /* Sources */, 194 | 0A809A392B39752200C9A015 /* Frameworks */, 195 | 0A809A3A2B39752200C9A015 /* Resources */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | ); 201 | name = Canvas; 202 | packageProductDependencies = ( 203 | 0A809A5A2B397B6100C9A015 /* CoreViewModels */, 204 | 0A809A602B397C3B00C9A015 /* CoreViews */, 205 | 0A809A622B397C3B00C9A015 /* ImageEditModule */, 206 | 0A809A642B397C3B00C9A015 /* ImageGenerationModule */, 207 | 0A809A662B397C3B00C9A015 /* ImageVariationModule */, 208 | 0A809A742B398AEE00C9A015 /* ImagePreferencesModule */, 209 | 0A809A782B39986D00C9A015 /* Defaults */, 210 | 0A809A7E2B39DDB100C9A015 /* CoreExtensions */, 211 | 0A809A802B39DDBA00C9A015 /* CoreModels */, 212 | 0AE400632B3AD5F800307732 /* Sparkle */, 213 | 0A20744C2B3CD3E5008516D1 /* ModelPricingModule */, 214 | 0A03693C2B4D6FBB00D7DAD4 /* SettingsModule */, 215 | 0A211A292C42D1C900B9029F /* AppInfo */, 216 | ); 217 | productName = Canvas; 218 | productReference = 0A809A3C2B39752200C9A015 /* Canvas.app */; 219 | productType = "com.apple.product-type.application"; 220 | }; 221 | /* End PBXNativeTarget section */ 222 | 223 | /* Begin PBXProject section */ 224 | 0A809A342B39752200C9A015 /* Project object */ = { 225 | isa = PBXProject; 226 | attributes = { 227 | BuildIndependentTargetsInParallel = 1; 228 | LastSwiftUpdateCheck = 1510; 229 | LastUpgradeCheck = 1520; 230 | TargetAttributes = { 231 | 0A809A3B2B39752200C9A015 = { 232 | CreatedOnToolsVersion = 15.1; 233 | }; 234 | }; 235 | }; 236 | buildConfigurationList = 0A809A372B39752200C9A015 /* Build configuration list for PBXProject "Canvas" */; 237 | compatibilityVersion = "Xcode 14.0"; 238 | developmentRegion = en; 239 | hasScannedForEncodings = 0; 240 | knownRegions = ( 241 | en, 242 | Base, 243 | ); 244 | mainGroup = 0A809A332B39752200C9A015; 245 | packageReferences = ( 246 | 0A809A772B39986D00C9A015 /* XCRemoteSwiftPackageReference "Defaults" */, 247 | 0AE400622B3AD5F800307732 /* XCRemoteSwiftPackageReference "Sparkle" */, 248 | 0A211A282C42D1C900B9029F /* XCRemoteSwiftPackageReference "AppInfo" */, 249 | ); 250 | productRefGroup = 0A809A3D2B39752200C9A015 /* Products */; 251 | projectDirPath = ""; 252 | projectRoot = ""; 253 | targets = ( 254 | 0A809A3B2B39752200C9A015 /* Canvas */, 255 | ); 256 | }; 257 | /* End PBXProject section */ 258 | 259 | /* Begin PBXResourcesBuildPhase section */ 260 | 0A809A3A2B39752200C9A015 /* Resources */ = { 261 | isa = PBXResourcesBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | 0A809A492B39752300C9A015 /* Preview Assets.xcassets in Resources */, 265 | 0A809A462B39752300C9A015 /* Assets.xcassets in Resources */, 266 | 0AE400612B3A98E600307732 /* Localizable.xcstrings in Resources */, 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | /* End PBXResourcesBuildPhase section */ 271 | 272 | /* Begin PBXSourcesBuildPhase section */ 273 | 0A809A382B39752200C9A015 /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | 0A809A6F2B39814F00C9A015 /* AppSidebarView.swift in Sources */, 278 | 0A809A582B39793700C9A015 /* AppView.swift in Sources */, 279 | 0A809A6C2B397E1C00C9A015 /* AppSidebarItem.swift in Sources */, 280 | 0AE400662B3AD84700307732 /* AppUpdater.swift in Sources */, 281 | 0A809A7C2B39988000C9A015 /* Defaults+Keys.swift in Sources */, 282 | 0A809A402B39752200C9A015 /* CanvasApp.swift in Sources */, 283 | 0A809A6A2B397DB100C9A015 /* AppSidebar.swift in Sources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | /* End PBXSourcesBuildPhase section */ 288 | 289 | /* Begin XCBuildConfiguration section */ 290 | 0A809A4B2B39752300C9A015 /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | ALWAYS_SEARCH_USER_PATHS = NO; 294 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 295 | CLANG_ANALYZER_NONNULL = YES; 296 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 297 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 298 | CLANG_ENABLE_MODULES = YES; 299 | CLANG_ENABLE_OBJC_ARC = YES; 300 | CLANG_ENABLE_OBJC_WEAK = YES; 301 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 302 | CLANG_WARN_BOOL_CONVERSION = YES; 303 | CLANG_WARN_COMMA = YES; 304 | CLANG_WARN_CONSTANT_CONVERSION = YES; 305 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 306 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 307 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 308 | CLANG_WARN_EMPTY_BODY = YES; 309 | CLANG_WARN_ENUM_CONVERSION = YES; 310 | CLANG_WARN_INFINITE_RECURSION = YES; 311 | CLANG_WARN_INT_CONVERSION = YES; 312 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 313 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 314 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 316 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 317 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 318 | CLANG_WARN_STRICT_PROTOTYPES = YES; 319 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 320 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 321 | CLANG_WARN_UNREACHABLE_CODE = YES; 322 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 323 | COPY_PHASE_STRIP = NO; 324 | DEAD_CODE_STRIPPING = YES; 325 | DEBUG_INFORMATION_FORMAT = dwarf; 326 | ENABLE_STRICT_OBJC_MSGSEND = YES; 327 | ENABLE_TESTABILITY = YES; 328 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu17; 330 | GCC_DYNAMIC_NO_PIC = NO; 331 | GCC_NO_COMMON_BLOCKS = YES; 332 | GCC_OPTIMIZATION_LEVEL = 0; 333 | GCC_PREPROCESSOR_DEFINITIONS = ( 334 | "DEBUG=1", 335 | "$(inherited)", 336 | ); 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Kevin Hermawan. All rights reserved."; 344 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 345 | MACOSX_DEPLOYMENT_TARGET = 14.2; 346 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 347 | MTL_FAST_MATH = YES; 348 | ONLY_ACTIVE_ARCH = YES; 349 | SDKROOT = macosx; 350 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 351 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 352 | }; 353 | name = Debug; 354 | }; 355 | 0A809A4C2B39752300C9A015 /* Release */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 360 | CLANG_ANALYZER_NONNULL = YES; 361 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 363 | CLANG_ENABLE_MODULES = YES; 364 | CLANG_ENABLE_OBJC_ARC = YES; 365 | CLANG_ENABLE_OBJC_WEAK = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 373 | CLANG_WARN_EMPTY_BODY = YES; 374 | CLANG_WARN_ENUM_CONVERSION = YES; 375 | CLANG_WARN_INFINITE_RECURSION = YES; 376 | CLANG_WARN_INT_CONVERSION = YES; 377 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 379 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | COPY_PHASE_STRIP = NO; 389 | DEAD_CODE_STRIPPING = YES; 390 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 391 | ENABLE_NS_ASSERTIONS = NO; 392 | ENABLE_STRICT_OBJC_MSGSEND = YES; 393 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 394 | GCC_C_LANGUAGE_STANDARD = gnu17; 395 | GCC_NO_COMMON_BLOCKS = YES; 396 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 397 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 398 | GCC_WARN_UNDECLARED_SELECTOR = YES; 399 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 400 | GCC_WARN_UNUSED_FUNCTION = YES; 401 | GCC_WARN_UNUSED_VARIABLE = YES; 402 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Kevin Hermawan. All rights reserved."; 403 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 404 | MACOSX_DEPLOYMENT_TARGET = 14.2; 405 | MTL_ENABLE_DEBUG_INFO = NO; 406 | MTL_FAST_MATH = YES; 407 | SDKROOT = macosx; 408 | SWIFT_COMPILATION_MODE = wholemodule; 409 | }; 410 | name = Release; 411 | }; 412 | 0A809A4E2B39752300C9A015 /* Debug */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 416 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 417 | CODE_SIGN_ENTITLEMENTS = Canvas/Resources/Canvas.entitlements; 418 | CODE_SIGN_STYLE = Automatic; 419 | COMBINE_HIDPI_IMAGES = YES; 420 | CURRENT_PROJECT_VERSION = 7; 421 | DEAD_CODE_STRIPPING = YES; 422 | DEVELOPMENT_ASSET_PATHS = "\"Canvas/Preview Content\""; 423 | DEVELOPMENT_TEAM = 84ZM7K56B5; 424 | ENABLE_HARDENED_RUNTIME = YES; 425 | ENABLE_PREVIEWS = YES; 426 | GENERATE_INFOPLIST_FILE = YES; 427 | INFOPLIST_FILE = Canvas/Resources/Info.plist; 428 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 429 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Kevin Hermawan. All rights reserved."; 430 | LD_RUNPATH_SEARCH_PATHS = ( 431 | "$(inherited)", 432 | "@executable_path/../Frameworks", 433 | ); 434 | MACOSX_DEPLOYMENT_TARGET = 14.0; 435 | MARKETING_VERSION = 1.0.6; 436 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinhermawan.Canvas; 437 | PRODUCT_NAME = "$(TARGET_NAME)"; 438 | SWIFT_EMIT_LOC_STRINGS = YES; 439 | SWIFT_VERSION = 5.0; 440 | }; 441 | name = Debug; 442 | }; 443 | 0A809A4F2B39752300C9A015 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 447 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 448 | CODE_SIGN_ENTITLEMENTS = Canvas/Resources/Canvas.entitlements; 449 | CODE_SIGN_STYLE = Automatic; 450 | COMBINE_HIDPI_IMAGES = YES; 451 | CURRENT_PROJECT_VERSION = 7; 452 | DEAD_CODE_STRIPPING = YES; 453 | DEVELOPMENT_ASSET_PATHS = "\"Canvas/Preview Content\""; 454 | DEVELOPMENT_TEAM = 84ZM7K56B5; 455 | ENABLE_HARDENED_RUNTIME = YES; 456 | ENABLE_PREVIEWS = YES; 457 | GENERATE_INFOPLIST_FILE = YES; 458 | INFOPLIST_FILE = Canvas/Resources/Info.plist; 459 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 460 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Kevin Hermawan. All rights reserved."; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@executable_path/../Frameworks", 464 | ); 465 | MACOSX_DEPLOYMENT_TARGET = 14.0; 466 | MARKETING_VERSION = 1.0.6; 467 | PRODUCT_BUNDLE_IDENTIFIER = com.kevinhermawan.Canvas; 468 | PRODUCT_NAME = "$(TARGET_NAME)"; 469 | SWIFT_EMIT_LOC_STRINGS = YES; 470 | SWIFT_VERSION = 5.0; 471 | }; 472 | name = Release; 473 | }; 474 | /* End XCBuildConfiguration section */ 475 | 476 | /* Begin XCConfigurationList section */ 477 | 0A809A372B39752200C9A015 /* Build configuration list for PBXProject "Canvas" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | 0A809A4B2B39752300C9A015 /* Debug */, 481 | 0A809A4C2B39752300C9A015 /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | 0A809A4D2B39752300C9A015 /* Build configuration list for PBXNativeTarget "Canvas" */ = { 487 | isa = XCConfigurationList; 488 | buildConfigurations = ( 489 | 0A809A4E2B39752300C9A015 /* Debug */, 490 | 0A809A4F2B39752300C9A015 /* Release */, 491 | ); 492 | defaultConfigurationIsVisible = 0; 493 | defaultConfigurationName = Release; 494 | }; 495 | /* End XCConfigurationList section */ 496 | 497 | /* Begin XCRemoteSwiftPackageReference section */ 498 | 0A211A282C42D1C900B9029F /* XCRemoteSwiftPackageReference "AppInfo" */ = { 499 | isa = XCRemoteSwiftPackageReference; 500 | repositoryURL = "https://github.com/kevinhermawan/AppInfo.git"; 501 | requirement = { 502 | kind = upToNextMajorVersion; 503 | minimumVersion = 1.0.2; 504 | }; 505 | }; 506 | 0A809A772B39986D00C9A015 /* XCRemoteSwiftPackageReference "Defaults" */ = { 507 | isa = XCRemoteSwiftPackageReference; 508 | repositoryURL = "https://github.com/sindresorhus/Defaults.git"; 509 | requirement = { 510 | kind = upToNextMajorVersion; 511 | minimumVersion = 8.2.0; 512 | }; 513 | }; 514 | 0AE400622B3AD5F800307732 /* XCRemoteSwiftPackageReference "Sparkle" */ = { 515 | isa = XCRemoteSwiftPackageReference; 516 | repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; 517 | requirement = { 518 | kind = upToNextMajorVersion; 519 | minimumVersion = 2.6.4; 520 | }; 521 | }; 522 | /* End XCRemoteSwiftPackageReference section */ 523 | 524 | /* Begin XCSwiftPackageProductDependency section */ 525 | 0A03693C2B4D6FBB00D7DAD4 /* SettingsModule */ = { 526 | isa = XCSwiftPackageProductDependency; 527 | productName = SettingsModule; 528 | }; 529 | 0A20744C2B3CD3E5008516D1 /* ModelPricingModule */ = { 530 | isa = XCSwiftPackageProductDependency; 531 | productName = ModelPricingModule; 532 | }; 533 | 0A211A292C42D1C900B9029F /* AppInfo */ = { 534 | isa = XCSwiftPackageProductDependency; 535 | package = 0A211A282C42D1C900B9029F /* XCRemoteSwiftPackageReference "AppInfo" */; 536 | productName = AppInfo; 537 | }; 538 | 0A809A5A2B397B6100C9A015 /* CoreViewModels */ = { 539 | isa = XCSwiftPackageProductDependency; 540 | productName = CoreViewModels; 541 | }; 542 | 0A809A602B397C3B00C9A015 /* CoreViews */ = { 543 | isa = XCSwiftPackageProductDependency; 544 | productName = CoreViews; 545 | }; 546 | 0A809A622B397C3B00C9A015 /* ImageEditModule */ = { 547 | isa = XCSwiftPackageProductDependency; 548 | productName = ImageEditModule; 549 | }; 550 | 0A809A642B397C3B00C9A015 /* ImageGenerationModule */ = { 551 | isa = XCSwiftPackageProductDependency; 552 | productName = ImageGenerationModule; 553 | }; 554 | 0A809A662B397C3B00C9A015 /* ImageVariationModule */ = { 555 | isa = XCSwiftPackageProductDependency; 556 | productName = ImageVariationModule; 557 | }; 558 | 0A809A742B398AEE00C9A015 /* ImagePreferencesModule */ = { 559 | isa = XCSwiftPackageProductDependency; 560 | productName = ImagePreferencesModule; 561 | }; 562 | 0A809A782B39986D00C9A015 /* Defaults */ = { 563 | isa = XCSwiftPackageProductDependency; 564 | package = 0A809A772B39986D00C9A015 /* XCRemoteSwiftPackageReference "Defaults" */; 565 | productName = Defaults; 566 | }; 567 | 0A809A7E2B39DDB100C9A015 /* CoreExtensions */ = { 568 | isa = XCSwiftPackageProductDependency; 569 | productName = CoreExtensions; 570 | }; 571 | 0A809A802B39DDBA00C9A015 /* CoreModels */ = { 572 | isa = XCSwiftPackageProductDependency; 573 | productName = CoreModels; 574 | }; 575 | 0AE400632B3AD5F800307732 /* Sparkle */ = { 576 | isa = XCSwiftPackageProductDependency; 577 | package = 0AE400622B3AD5F800307732 /* XCRemoteSwiftPackageReference "Sparkle" */; 578 | productName = Sparkle; 579 | }; 580 | /* End XCSwiftPackageProductDependency section */ 581 | }; 582 | rootObject = 0A809A342B39752200C9A015 /* Project object */; 583 | } 584 | -------------------------------------------------------------------------------- /Canvas.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Canvas.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Canvas.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "appinfo", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/kevinhermawan/AppInfo", 7 | "state" : { 8 | "revision" : "4b60b10c94c327083cb5fe5e73cdff21d1082b4c", 9 | "version" : "1.0.2" 10 | } 11 | }, 12 | { 13 | "identity" : "chatfield", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/kevinhermawan/ChatField.git", 16 | "state" : { 17 | "revision" : "82bfd96c793d95ee7e371afbe980ca80b9cfe295", 18 | "version" : "3.0.3" 19 | } 20 | }, 21 | { 22 | "identity" : "defaults", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/sindresorhus/Defaults.git", 25 | "state" : { 26 | "revision" : "38925e3cfacf3fb89a81a35b1cd44fd5a5b7e0fa", 27 | "version" : "8.2.0" 28 | } 29 | }, 30 | { 31 | "identity" : "keychainaccess", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", 34 | "state" : { 35 | "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", 36 | "version" : "4.2.2" 37 | } 38 | }, 39 | { 40 | "identity" : "nuke", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/kean/Nuke.git", 43 | "state" : { 44 | "revision" : "0ead44350d2737db384908569c012fe67c421e4d", 45 | "version" : "12.8.0" 46 | } 47 | }, 48 | { 49 | "identity" : "openai", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/MacPaw/OpenAI.git", 52 | "state" : { 53 | "revision" : "843e087929aa806adb611dbca93f9a4a7f28be04", 54 | "version" : "0.3.0" 55 | } 56 | }, 57 | { 58 | "identity" : "sparkle", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/sparkle-project/Sparkle.git", 61 | "state" : { 62 | "revision" : "0ef1ee0220239b3776f433314515fd849025673f", 63 | "version" : "2.6.4" 64 | } 65 | }, 66 | { 67 | "identity" : "swiftui-introspect", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/siteline/swiftui-introspect.git", 70 | "state" : { 71 | "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", 72 | "version" : "1.3.0" 73 | } 74 | }, 75 | { 76 | "identity" : "viewcondition", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/kevinhermawan/ViewCondition.git", 79 | "state" : { 80 | "revision" : "a3d0aaf943271247dfd4870a2a0696693165c884", 81 | "version" : "1.0.5" 82 | } 83 | }, 84 | { 85 | "identity" : "viewstate", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/kevinhermawan/ViewState.git", 88 | "state" : { 89 | "revision" : "6c85c207c23edf487007542651e3533a80dfa0bc", 90 | "version" : "1.2.2" 91 | } 92 | } 93 | ], 94 | "version" : 2 95 | } 96 | -------------------------------------------------------------------------------- /Canvas/App/AppUpdater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppUpdater.swift 3 | // Canvas 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import Combine 9 | import Sparkle 10 | import SwiftUI 11 | 12 | @Observable 13 | final class AppUpdater { 14 | private var cancellable: AnyCancellable? 15 | var canCheckForUpdates = false 16 | 17 | init(_ updater: SPUUpdater) { 18 | cancellable = updater.publisher(for: \.canCheckForUpdates) 19 | .assign(to: \.canCheckForUpdates, on: self) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Canvas/App/CanvasApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CanvasApp.swift 3 | // Canvas 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import AppInfo 9 | import CoreViewModels 10 | import KeychainAccess 11 | import SettingsModule 12 | import Sparkle 13 | import SwiftUI 14 | 15 | @main 16 | struct CanvasApp: App { 17 | @State private var appUpdater: AppUpdater 18 | private var updater: SPUUpdater 19 | 20 | @State private var settingsManager: SettingsManager 21 | @State private var dalleViewModel: DalleViewModel 22 | @State private var dalleModelInfoViewModel: DalleModelInfoViewModel 23 | 24 | init() { 25 | let keychain = Keychain() 26 | self._settingsManager = State(initialValue: SettingsManager(keychain: keychain)) 27 | self._dalleViewModel = State(initialValue: DalleViewModel()) 28 | self._dalleModelInfoViewModel = State(initialValue: DalleModelInfoViewModel()) 29 | 30 | let updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) 31 | updater = updaterController.updater 32 | 33 | let appUpdater = AppUpdater(updater) 34 | _appUpdater = State(initialValue: appUpdater) 35 | } 36 | 37 | var body: some Scene { 38 | WindowGroup { 39 | AppView() 40 | .environment(settingsManager) 41 | .environment(dalleViewModel) 42 | .environment(dalleModelInfoViewModel) 43 | } 44 | .commands { 45 | CommandGroup(after: .appInfo) { 46 | Button("Check for Updates...") { 47 | updater.checkForUpdates() 48 | } 49 | .disabled(appUpdater.canCheckForUpdates == false) 50 | } 51 | 52 | CommandGroup(replacing: .help) { 53 | if let helpURL = AppInfo.value(for: "HELP_URL"), let url = URL(string: helpURL) { 54 | Link("Canvas Help", destination: url) 55 | } 56 | } 57 | 58 | SidebarCommands() 59 | InspectorCommands() 60 | } 61 | 62 | Settings { 63 | SettingsView() 64 | .environment(settingsManager) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Canvas/Enums/AppSidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSidebar.swift 3 | // Canvas 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AppSidebar: String, CaseIterable { 11 | case dalle 12 | case dalle2 13 | case menu 14 | 15 | var title: String { 16 | switch self { 17 | case .dalle: 18 | return "DALL·E" 19 | case .dalle2: 20 | return "DALL·E 2" 21 | case .menu: 22 | return "Menu" 23 | } 24 | } 25 | } 26 | 27 | extension AppSidebar: Identifiable { 28 | var id: String { 29 | self.rawValue 30 | } 31 | } 32 | 33 | extension AppSidebar { 34 | var items: [AppSidebarItem] { 35 | switch self { 36 | case .dalle: 37 | return [.imageGeneration] 38 | case .dalle2: 39 | return [.imageEdit, .imageVariation] 40 | case .menu: 41 | return [.modelPricing] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Canvas/Enums/AppSidebarItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSidebarItem.swift 3 | // Canvas 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AppSidebarItem: String, CaseIterable { 11 | case imageGeneration 12 | case imageEdit 13 | case imageVariation 14 | case modelPricing 15 | 16 | var title: String { 17 | switch self { 18 | case .imageGeneration: 19 | return "Image Generation" 20 | case .imageEdit: 21 | return "Image Edit" 22 | case .imageVariation: 23 | return "Image Variation" 24 | case .modelPricing: 25 | return "Model Pricing" 26 | } 27 | } 28 | 29 | var systemImage: String { 30 | switch self { 31 | case .imageGeneration: 32 | return "paintbrush" 33 | case .imageEdit: 34 | return "theatermask.and.paintbrush" 35 | case .imageVariation: 36 | return "paintpalette" 37 | case .modelPricing: 38 | return "dollarsign.circle" 39 | } 40 | } 41 | } 42 | 43 | extension AppSidebarItem: Identifiable { 44 | var id: String { 45 | self.rawValue 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Canvas/Extensions/Defaults+Keys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults+Keys.swift 3 | // Canvas 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import CoreModels 9 | import Defaults 10 | import Foundation 11 | 12 | extension Defaults.Keys { 13 | static let ig_selectedModel = Key("ig_selectedModel", default: .dalle2) 14 | static let ig_selectedNumber = Key("ig_selectedNumber", default: DalleModel.dalle2.numbers[0]) 15 | static let ig_selectedSize = Key("ig_selectedSize", default: ._1024) 16 | static let ig_selectedQuality = Key("ig_selectedQuality", default: .standard) 17 | static let ig_selectedStyle = Key("ig_selectedStyle", default: .vivid) 18 | 19 | static let ie_selectedNumber = Key("ie_selectedNumber", default: DalleModel.dalle2.numbers[0]) 20 | static let ie_selectedSize = Key("ie_selectedSize", default: ._1024) 21 | 22 | static let iv_selectedNumber = Key("iv_selectedNumber", default: DalleModel.dalle2.numbers[0]) 23 | static let iv_selectedSize = Key("iv_selectedSize", default: ._1024) 24 | } 25 | -------------------------------------------------------------------------------- /Canvas/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemBlueColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_1024x1024.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_128x128.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_16x16.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_256x256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_256x256 1.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_256x256.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_32x32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_32x32 1.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_32x32.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_512x512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_512x512 1.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_512x512.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/Canvas/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon_64x64.png -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "AppIcon_32x32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "AppIcon_32x32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "AppIcon_64x64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "AppIcon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "AppIcon_256x256 1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "AppIcon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "AppIcon_512x512 1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "AppIcon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "AppIcon_1024x1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Canvas/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Canvas/Resources/Canvas.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.files.user-selected.read-write 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.temporary-exception.mach-lookup.global-name 10 | 11 | $(PRODUCT_BUNDLE_IDENTIFIER)-spks 12 | $(PRODUCT_BUNDLE_IDENTIFIER)-spki 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Canvas/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MODEL_INFO_URL 6 | https://raw.githubusercontent.com/kevinhermawan/Canvas/main/model-info.json 7 | HELP_URL 8 | https://github.com/kevinhermawan/Canvas/issues 9 | SUEnableInstallerLauncherService 10 | 11 | SUEnableDownloaderService 12 | 13 | SUFeedURL 14 | https://raw.githubusercontent.com/kevinhermawan/Canvas/main/appcast.xml 15 | SUPublicEDKey 16 | xEv1HCqq1iI29hbUSHP7BuExQBfIMupe2d1Xa5bvW2Y= 17 | 18 | 19 | -------------------------------------------------------------------------------- /Canvas/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "Canvas Help" : { 5 | 6 | }, 7 | "Check for Updates..." : { 8 | 9 | }, 10 | "FOOTNOTE_IMAGE_IMAGE_EDIT" : { 11 | "extractionState" : "manual", 12 | "localizations" : { 13 | "en" : { 14 | "stringUnit" : { 15 | "state" : "translated", 16 | "value" : "The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask." 17 | } 18 | } 19 | } 20 | }, 21 | "FOOTNOTE_IMAGE_IMAGE_VARIATION" : { 22 | "extractionState" : "manual", 23 | "localizations" : { 24 | "en" : { 25 | "stringUnit" : { 26 | "state" : "translated", 27 | "value" : "The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, and square." 28 | } 29 | } 30 | } 31 | }, 32 | "FOOTNOTE_MASK_IMAGE_EDIT" : { 33 | "extractionState" : "manual", 34 | "localizations" : { 35 | "en" : { 36 | "stringUnit" : { 37 | "state" : "translated", 38 | "value" : "An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions as image." 39 | } 40 | } 41 | } 42 | }, 43 | "FOOTNOTE_MODEL" : { 44 | "extractionState" : "manual", 45 | "localizations" : { 46 | "en" : { 47 | "stringUnit" : { 48 | "state" : "translated", 49 | "value" : "The model to use for image generation." 50 | } 51 | } 52 | } 53 | }, 54 | "FOOTNOTE_NUMBER" : { 55 | "extractionState" : "manual", 56 | "localizations" : { 57 | "en" : { 58 | "stringUnit" : { 59 | "state" : "translated", 60 | "value" : "The number of images to generate." 61 | } 62 | } 63 | } 64 | }, 65 | "FOOTNOTE_QUALITY_HD" : { 66 | "extractionState" : "manual", 67 | "localizations" : { 68 | "en" : { 69 | "stringUnit" : { 70 | "state" : "translated", 71 | "value" : "The quality of the image that will be generated. HD creates images with finer details and greater consistency across the image. " 72 | } 73 | } 74 | } 75 | }, 76 | "FOOTNOTE_QUALITY_STANDARD" : { 77 | "extractionState" : "manual", 78 | "localizations" : { 79 | "en" : { 80 | "stringUnit" : { 81 | "state" : "translated", 82 | "value" : "The quality of the image that will be generated." 83 | } 84 | } 85 | } 86 | }, 87 | "FOOTNOTE_SIZE" : { 88 | "extractionState" : "manual", 89 | "localizations" : { 90 | "en" : { 91 | "stringUnit" : { 92 | "state" : "translated", 93 | "value" : "The size of the generated images." 94 | } 95 | } 96 | } 97 | }, 98 | "FOOTNOTE_STYLE_NATURAL" : { 99 | "extractionState" : "manual", 100 | "localizations" : { 101 | "en" : { 102 | "stringUnit" : { 103 | "state" : "translated", 104 | "value" : "The style of the generated images. Natural causes the model to produce more natural, less hyper-real looking images." 105 | } 106 | } 107 | } 108 | }, 109 | "FOOTNOTE_STYLE_VIVID" : { 110 | "extractionState" : "manual", 111 | "localizations" : { 112 | "en" : { 113 | "stringUnit" : { 114 | "state" : "translated", 115 | "value" : "The style of the generated images. Vivid causes the model to lean towards generating hyper-real and dramatic images." 116 | } 117 | } 118 | } 119 | }, 120 | "Preferences" : { 121 | 122 | } 123 | }, 124 | "version" : "1.0" 125 | } -------------------------------------------------------------------------------- /Canvas/Views/AppSidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSidebarView.swift 3 | // Canvas 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppSidebarView: View { 11 | @Binding private var selection: AppSidebarItem 12 | 13 | init(selection: Binding) { 14 | self._selection = selection 15 | } 16 | 17 | var body: some View { 18 | List(AppSidebar.allCases, selection: $selection) { sidebar in 19 | Section(sidebar.title) { 20 | ForEach(sidebar.items) { item in 21 | Label(item.title, systemImage: item.systemImage) 22 | .tag(item) 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | #Preview { 30 | NavigationSplitView { 31 | AppSidebarView(selection: .constant(.imageGeneration)) 32 | } detail: { 33 | EmptyView() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Canvas/Views/AppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppView.swift 3 | // Canvas 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import CoreModels 9 | import Defaults 10 | import ImageEditModule 11 | import ImageGenerationModule 12 | import ImagePreferencesModule 13 | import ImageVariationModule 14 | import ModelPricingModule 15 | import SwiftUI 16 | 17 | struct AppView: View { 18 | @State private var selectedSidebar: AppSidebarItem = .imageGeneration 19 | @State private var imagePreferencesPresented: Bool = true 20 | 21 | @Default(.ig_selectedModel) private var ig_selectedModel 22 | @Default(.ig_selectedNumber) private var ig_selectedNumber 23 | @Default(.ig_selectedSize) private var ig_selectedSize 24 | @Default(.ig_selectedQuality) private var ig_selectedQuality 25 | @Default(.ig_selectedStyle) private var ig_selectedStyle 26 | 27 | @Default(.ie_selectedNumber) private var ie_selectedNumber 28 | @Default(.ie_selectedSize) private var ie_selectedSize 29 | @State private var ie_imageData: Data? = nil 30 | @State private var ie_maskData: Data? = nil 31 | 32 | @Default(.iv_selectedNumber) private var iv_selectedNumber 33 | @Default(.iv_selectedSize) private var iv_selectedSize 34 | @State private var iv_imageData: Data? = nil 35 | 36 | var body: some View { 37 | NavigationSplitView { 38 | AppSidebarView(selection: $selectedSidebar) 39 | .navigationSplitViewColumnWidth(min: 256, ideal: 256) 40 | } detail: { 41 | switch selectedSidebar { 42 | case .imageGeneration: 43 | ImageGenerationView( 44 | model: ig_selectedModel, 45 | number: ig_selectedNumber, 46 | size: ig_selectedSize, 47 | quality: ig_selectedQuality, 48 | style: ig_selectedStyle 49 | ) 50 | .navigationSubtitle(ig_selectedModel.title) 51 | .toolbar { PreferencesToolbar() } 52 | .inspector(isPresented: $imagePreferencesPresented) { 53 | ImagePreferencesView( 54 | modelSelection: $ig_selectedModel, 55 | numberSelection: $ig_selectedNumber, 56 | sizeSelection: $ig_selectedSize, 57 | qualitySelection: $ig_selectedQuality, 58 | styleSelection: $ig_selectedStyle, 59 | imageData: .constant(nil), 60 | maskData: .constant(nil) 61 | ) 62 | } 63 | case .imageEdit: 64 | ImageEditView( 65 | number: ie_selectedNumber, 66 | size: ie_selectedSize, 67 | imageData: ie_imageData, 68 | maskData: ie_maskData 69 | ) 70 | .navigationSubtitle(DalleModel.dalle2.title) 71 | .toolbar { PreferencesToolbar() } 72 | .inspector(isPresented: $imagePreferencesPresented) { 73 | ImagePreferencesView( 74 | modelSelection: .constant(.dalle2), 75 | numberSelection: $ie_selectedNumber, 76 | sizeSelection: $ie_selectedSize, 77 | qualitySelection: .constant(nil), 78 | styleSelection: .constant(nil), 79 | imageData: $ie_imageData, 80 | maskData: $ie_maskData 81 | ) 82 | .modelPickerHidden() 83 | .imagePickerVisible(text: "FOOTNOTE_IMAGE_IMAGE_EDIT") 84 | .maskPickerVisible(text: "FOOTNOTE_MASK_IMAGE_EDIT") 85 | } 86 | case .imageVariation: 87 | ImageVariationView( 88 | number: iv_selectedNumber, 89 | size: iv_selectedSize, 90 | imageData: iv_imageData 91 | ) 92 | .navigationSubtitle(DalleModel.dalle2.title) 93 | .toolbar { PreferencesToolbar() } 94 | .inspector(isPresented: $imagePreferencesPresented) { 95 | ImagePreferencesView( 96 | modelSelection: .constant(.dalle2), 97 | numberSelection: $iv_selectedNumber, 98 | sizeSelection: $iv_selectedSize, 99 | qualitySelection: .constant(nil), 100 | styleSelection: .constant(nil), 101 | imageData: $iv_imageData, 102 | maskData: .constant(nil) 103 | ) 104 | .modelPickerHidden() 105 | .imagePickerVisible(text: "FOOTNOTE_IMAGE_IMAGE_VARIATION") 106 | } 107 | case .modelPricing: 108 | ModelPricingView() 109 | } 110 | } 111 | } 112 | 113 | @ViewBuilder 114 | private func PreferencesToolbar() -> some View { 115 | Button("Preferences", systemImage: "sidebar.right") { 116 | imagePreferencesPresented.toggle() 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /CoreExtensions/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /CoreExtensions/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CoreExtensions", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "CoreExtensions", 12 | targets: ["CoreExtensions"]), 13 | ], 14 | targets: [ 15 | .target(name: "CoreExtensions") 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /CoreExtensions/Sources/CoreExtensions/NSImage+Write.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 09/01/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public extension NSImage { 12 | func write(to url: URL) throws -> Void { 13 | guard let tiff = self.tiffRepresentation else { return } 14 | guard let bitmap = NSBitmapImageRep(data: tiff) else { return } 15 | guard let data = bitmap.representation(using: .png, properties: [:]) else { return } 16 | 17 | try data.write(to: url) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CoreExtensions/Sources/CoreExtensions/Optional+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+Utils.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | public extension Optional { 9 | var isNil: Bool { 10 | self == nil 11 | } 12 | 13 | var isNotNil: Bool { 14 | self != nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CoreModels/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /CoreModels/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CoreModels", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "CoreModels", 12 | targets: ["CoreModels"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/MacPaw/OpenAI.git", .upToNextMajor(from: "0.3.0")), 16 | .package(url: "https://github.com/sindresorhus/Defaults.git", .upToNextMajor(from: "8.2.0")) 17 | ], 18 | targets: [ 19 | .target( 20 | name: "CoreModels", 21 | dependencies: ["OpenAI", "Defaults"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /CoreModels/Sources/CoreModels/DalleModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DalleModel.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import Defaults 9 | import Foundation 10 | import OpenAI 11 | 12 | public enum DalleModel: String, CaseIterable { 13 | case dalle2 = "dall-e-2" 14 | case dalle3 = "dall-e-3" 15 | 16 | public var title: String { 17 | switch self { 18 | case .dalle2: 19 | return "DALL·E 2" 20 | case .dalle3: 21 | return "DALL·E 3" 22 | } 23 | } 24 | 25 | public var numbers: [Int] { 26 | switch self { 27 | case .dalle2: 28 | return Array(1...10) 29 | case .dalle3: 30 | return Array(arrayLiteral: 1) 31 | } 32 | } 33 | } 34 | 35 | extension DalleModel: Identifiable { 36 | public var id: String { 37 | self.rawValue 38 | } 39 | } 40 | 41 | extension DalleModel: Codable, Defaults.Serializable {} 42 | 43 | extension DalleModel { 44 | public enum Size: String, CaseIterable, Identifiable, Codable, Defaults.Serializable { 45 | case _256, _512, _1024, _1024_1792, _1792_1024 46 | 47 | public var id: String { 48 | self.rawValue 49 | } 50 | 51 | public var title: String { 52 | switch self { 53 | case ._256: 54 | return "256x256 (Square)" 55 | case ._512: 56 | return "512x512 (Square)" 57 | case ._1024: 58 | return "1024x1024 (Square)" 59 | case ._1024_1792: 60 | return "1024x1792 (Portrait)" 61 | case ._1792_1024: 62 | return "1792x1024 (Landscape)" 63 | } 64 | } 65 | 66 | public var imagesQuery: ImagesQuery.Size { 67 | switch self { 68 | case ._256: 69 | return ._256 70 | case ._512: 71 | return ._512 72 | case ._1024: 73 | return ._1024 74 | case ._1024_1792: 75 | return ._1024_1792 76 | case ._1792_1024: 77 | return ._1792_1024 78 | } 79 | } 80 | } 81 | 82 | public var sizes: [Size] { 83 | switch self { 84 | case .dalle2: 85 | return [._256, ._512, ._1024] 86 | case .dalle3: 87 | return [._1024, ._1024_1792, ._1792_1024] 88 | } 89 | } 90 | } 91 | 92 | extension DalleModel { 93 | public enum Quality: String, CaseIterable, Identifiable, Codable, Defaults.Serializable { 94 | case standard, hd 95 | 96 | public var id: String { 97 | self.rawValue 98 | } 99 | 100 | public var title: String { 101 | switch self { 102 | case .standard: 103 | return "Standard" 104 | case .hd: 105 | return "HD" 106 | } 107 | } 108 | 109 | public var imagesQuery: ImagesQuery.Quality { 110 | switch self { 111 | case .standard: 112 | return .standard 113 | case .hd: 114 | return .hd 115 | } 116 | } 117 | } 118 | 119 | public var qualities: [Quality]? { 120 | switch self { 121 | case .dalle2: 122 | return nil 123 | case .dalle3: 124 | return [.standard, .hd] 125 | } 126 | } 127 | } 128 | 129 | extension DalleModel { 130 | public enum Style: String, CaseIterable, Identifiable, Defaults.Serializable { 131 | case vivid, natural 132 | 133 | public var id: String { 134 | self.rawValue 135 | } 136 | 137 | public var title: String { 138 | switch self { 139 | case .vivid: 140 | return "Vivid" 141 | case .natural: 142 | return "Natural" 143 | } 144 | } 145 | 146 | public var imagesQuery: ImagesQuery.Style { 147 | switch self { 148 | case .vivid: 149 | return .vivid 150 | case .natural: 151 | return .natural 152 | } 153 | } 154 | } 155 | 156 | public var styles: [Style]? { 157 | switch self { 158 | case .dalle2: 159 | return nil 160 | case .dalle3: 161 | return [.vivid, .natural] 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /CoreModels/Sources/CoreModels/DalleModelInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DalleModelInfo.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 28/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DalleModelInfo: Codable { 11 | public let pricing: [Pricing] 12 | public let pricingInfoUrl: String 13 | public let pricingUpdatedAt: Date 14 | } 15 | 16 | extension DalleModelInfo { 17 | public struct Pricing: Identifiable, Codable { 18 | public let model: DalleModel 19 | public let quality: DalleModel.Quality? 20 | public let resolution: String 21 | public let price: Double 22 | 23 | public var id: String { 24 | if let quality { 25 | return "\(model)-\(quality)-\(resolution)" 26 | } 27 | 28 | return "\(model)-\(resolution)" 29 | } 30 | 31 | public var qualityTitle: String { 32 | quality?.title ?? "-" 33 | } 34 | 35 | public var formattedPrice: String { 36 | String(format: "$%.3f", price) 37 | } 38 | 39 | public var attributedPrice: AttributedString { 40 | var attributedString = AttributedString(formattedPrice) 41 | var unitString = AttributedString(" / image") 42 | unitString.foregroundColor = .secondary 43 | 44 | attributedString.append(unitString) 45 | 46 | return attributedString 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /CoreViewModels/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /CoreViewModels/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CoreViewModels", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "CoreViewModels", 12 | targets: ["CoreViewModels"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "CoreModels", path: "../CoreModels"), 16 | .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.2.2")), 17 | .package(url: "https://github.com/kevinhermawan/AppInfo.git", .upToNextMajor(from: "1.0.2")), 18 | .package(url: "https://github.com/kevinhermawan/ViewState.git", .upToNextMajor(from: "1.2.2")), 19 | .package(url: "https://github.com/MacPaw/OpenAI.git", .upToNextMajor(from: "0.3.0")) 20 | ], 21 | targets: [ 22 | .target( 23 | name: "CoreViewModels", 24 | dependencies: ["CoreModels", "KeychainAccess", "AppInfo", "ViewState", "OpenAI"]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /CoreViewModels/Sources/CoreViewModels/DalleModelInfoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DalleModelInfoViewModel.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 28/12/23. 6 | // 7 | 8 | import AppInfo 9 | import CoreModels 10 | import Foundation 11 | import ViewState 12 | 13 | @MainActor 14 | @Observable 15 | public final class DalleModelInfoViewModel { 16 | private var session: URLSession 17 | 18 | public var pricing: [DalleModelInfo.Pricing] = [] 19 | public var pricingInfoUrl: String? = nil 20 | public var pricingUpdatedAt: Date? = nil 21 | public var viewState: ViewState? = nil 22 | 23 | public init() { 24 | let configuration = URLSessionConfiguration.default 25 | configuration.requestCachePolicy = .returnCacheDataElseLoad 26 | 27 | self.session = URLSession(configuration: configuration) 28 | self.session.configuration.urlCache?.removeAllCachedResponses() 29 | } 30 | 31 | private var decoder: JSONDecoder { 32 | let dateFormatter = DateFormatter() 33 | dateFormatter.dateFormat = "dd/MM/yyyy" 34 | 35 | let decoder = JSONDecoder() 36 | decoder.keyDecodingStrategy = .convertFromSnakeCase 37 | decoder.dateDecodingStrategy = .formatted(dateFormatter) 38 | 39 | return decoder 40 | } 41 | 42 | public func fetch() async { 43 | self.viewState = .loading 44 | 45 | do { 46 | guard let urlString = AppInfo.value(for: "MODEL_INFO_URL") else { 47 | throw URLError(.cannotFindHost) 48 | } 49 | 50 | guard let url = URL(string: urlString) else { 51 | throw URLError(.badURL) 52 | } 53 | 54 | let (data, response) = try await session.data(from: url) 55 | 56 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { 57 | throw URLError(.badServerResponse) 58 | } 59 | 60 | let result = try decoder.decode(DalleModelInfo.self, from: data) 61 | self.pricing = result.pricing 62 | self.pricingInfoUrl = result.pricingInfoUrl 63 | self.pricingUpdatedAt = result.pricingUpdatedAt 64 | 65 | self.viewState = nil 66 | } catch { 67 | guard error.localizedDescription != "cancelled" else { return } 68 | 69 | self.viewState = .error(message: error.localizedDescription) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CoreViewModels/Sources/CoreViewModels/DalleViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DalleViewModel.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import OpenAI 11 | import ViewState 12 | 13 | @MainActor 14 | @Observable 15 | public final class DalleViewModel { 16 | private var openAI: OpenAI? = nil 17 | private var cancellable: AnyCancellable? = nil 18 | 19 | public var results: [URL] = [] 20 | public var viewState: ViewState? = nil 21 | 22 | public init() {} 23 | 24 | public func setup(apiKey: String?) { 25 | guard let apiKey else { return } 26 | 27 | self.openAI = OpenAI(apiToken: apiKey) 28 | } 29 | 30 | public func imageGeneration(query: ImagesQuery) { 31 | guard let openAI else { return } 32 | 33 | self.viewState = .loading 34 | self.results = [] 35 | 36 | self.cancellable = openAI.images(query: query) 37 | .receive(on: DispatchQueue.main) 38 | .sink(receiveCompletion: self.handleCompletion, receiveValue: { [weak self] result in 39 | guard let self else { return } 40 | 41 | self.handleReceiveResult(result, with: query) 42 | }) 43 | } 44 | 45 | public func imageEdit(query: ImageEditsQuery) { 46 | guard let openAI else { return } 47 | 48 | self.viewState = .loading 49 | self.results = [] 50 | 51 | self.cancellable = openAI.imageEdits(query: query) 52 | .receive(on: DispatchQueue.main) 53 | .sink(receiveCompletion: self.handleCompletion, receiveValue: { [weak self] result in 54 | guard let self else { return } 55 | 56 | self.handleReceiveResult(result, with: query) 57 | }) 58 | } 59 | 60 | public func imageVariation(query: ImageVariationsQuery) { 61 | guard let openAI else { return } 62 | 63 | self.viewState = .loading 64 | self.results = [] 65 | 66 | self.cancellable = openAI.imageVariations(query: query) 67 | .receive(on: DispatchQueue.main) 68 | .sink(receiveCompletion: self.handleCompletion, receiveValue: { [weak self] result in 69 | guard let self else { return } 70 | 71 | self.handleReceiveResult(result, with: query) 72 | }) 73 | } 74 | 75 | public func cancel() { 76 | self.cancellable?.cancel() 77 | self.viewState = nil 78 | self.results = [] 79 | } 80 | 81 | private func handleCompletion(completion: Subscribers.Completion) { 82 | switch completion { 83 | case .finished: 84 | self.viewState = nil 85 | case .failure(let error): 86 | self.viewState = .error(message: error.localizedDescription) 87 | } 88 | } 89 | 90 | private func handleReceiveResult(_ result: ImagesResult, with query: ImagesQuery) { 91 | let urls = result.data 92 | .compactMap({ $0.url }) 93 | .compactMap({ URL(string: $0) }) 94 | 95 | for url in urls { 96 | self.results.append(url) 97 | } 98 | } 99 | 100 | private func handleReceiveResult(_ result: ImagesResult, with query: ImageEditsQuery) { 101 | let urls = result.data 102 | .compactMap({ $0.url }) 103 | .compactMap({ URL(string: $0) }) 104 | 105 | for url in urls { 106 | self.results.append(url) 107 | } 108 | } 109 | 110 | private func handleReceiveResult(_ result: ImagesResult, with query: ImageVariationsQuery) { 111 | let urls = result.data 112 | .compactMap({ $0.url }) 113 | .compactMap({ URL(string: $0) }) 114 | 115 | for url in urls { 116 | self.results.append(url) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /CoreViews/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /CoreViews/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CoreViews", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "CoreViews", 12 | targets: ["CoreViews"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/kean/Nuke.git", .upToNextMajor(from: "12.8.0")), 16 | .package(url: "https://github.com/kevinhermawan/ChatField.git", .upToNextMajor(from: "3.0.3")) 17 | ], 18 | targets: [ 19 | .target( 20 | name: "CoreViews", 21 | dependencies: [ 22 | .product(name: "Nuke", package: "Nuke"), 23 | .product(name: "NukeUI", package: "Nuke"), 24 | "ChatField" 25 | ]) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/Buttons/CircleButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleButton.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct CircleButton: View { 11 | private let systemImage: String 12 | private let action: () -> Void 13 | 14 | public init(systemImage: String, action: @escaping () -> Void) { 15 | self.systemImage = systemImage 16 | self.action = action 17 | } 18 | 19 | public var body: some View { 20 | Button(action: action) { 21 | Image(systemName: systemImage) 22 | .foregroundStyle(.foreground) 23 | .fontWeight(.bold) 24 | .padding(8) 25 | } 26 | .background(.background) 27 | .buttonStyle(.borderless) 28 | .clipShape(.circle) 29 | .colorInvert() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/Extensions/URL+Preview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Preview.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | #if DEBUG 11 | public extension URL { 12 | static let previewSquareURL = URL(string: "https://ik.imagekit.io/khermawan/kevinhermawan/canvas/square.png")! 13 | static let previewPortraitURL = URL(string: "https://ik.imagekit.io/khermawan/kevinhermawan/canvas/portrait.png")! 14 | static let previewLandscapeURL = URL(string: "https://ik.imagekit.io/khermawan/kevinhermawan/canvas/landscape.png")! 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/ImageResult/ImageResultContextMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageResultContextMenu.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreExtensions 9 | import Nuke 10 | import NukeUI 11 | import SwiftUI 12 | 13 | struct ImageResultContextMenu: View { 14 | private let name: String 15 | private var image: Image 16 | private var imageContainer: ImageContainer 17 | 18 | init(name: String, image: Image, imageContainer: ImageContainer) { 19 | self.name = name 20 | self.image = image 21 | self.imageContainer = imageContainer 22 | } 23 | 24 | var body: some View { 25 | Button("Copy", action: copy) 26 | Button("Save", action: save) 27 | ShareLink("Share", item: image, preview: SharePreview(name, image: image)) 28 | } 29 | 30 | private func copy() { 31 | let nsImage = imageContainer.image 32 | let pasteboard = NSPasteboard.general 33 | 34 | pasteboard.clearContents() 35 | pasteboard.writeObjects([nsImage]) 36 | } 37 | 38 | private func save() { 39 | let nsImage = imageContainer.image 40 | 41 | let savePanel = NSSavePanel() 42 | savePanel.allowedContentTypes = [.png] 43 | savePanel.canCreateDirectories = true 44 | savePanel.nameFieldStringValue = name 45 | 46 | savePanel.begin { response in 47 | guard response == .OK, let url = savePanel.url else { return } 48 | 49 | try? nsImage.write(to: url) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/ImageResult/ImageResultListItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageResultListItemView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import CoreExtensions 9 | import SettingsModule 10 | import NukeUI 11 | import SwiftUI 12 | 13 | public struct ImageResultListItemView: View { 14 | @Environment(SettingsManager.self) private var settingsManager 15 | 16 | private let url: URL 17 | 18 | public init(url: URL) { 19 | self.url = url 20 | } 21 | 22 | public var body: some View { 23 | LazyImage(url: url) { state in 24 | if state.isLoading { 25 | VStack { 26 | ProgressView() 27 | .controlSize(.small) 28 | } 29 | } else if let error = state.error { 30 | VStack { 31 | Text(error.localizedDescription) 32 | } 33 | } else if let image = state.image, let imageContainer = state.imageContainer { 34 | image.resizable() 35 | .aspectRatio(contentMode: .fit) 36 | .onAppear { 37 | autosaveAction(for: imageContainer.image) 38 | } 39 | .contextMenu { 40 | ImageResultContextMenu( 41 | name: url.lastPathComponent, 42 | image: image, 43 | imageContainer: imageContainer 44 | ) 45 | } 46 | } 47 | } 48 | .frame(maxWidth: .infinity) 49 | .frame(height: 302) 50 | .background(Color(nsColor: .secondarySystemFill)) 51 | .clipShape(.rect(cornerRadius: 8, style: .continuous)) 52 | } 53 | 54 | private func autosaveAction(for image: NSImage) { 55 | let fileManager = FileManager.default 56 | 57 | if settingsManager.autosaveEnabled { 58 | var isDir: ObjCBool = false 59 | var location = settingsManager.autosaveLocation 60 | 61 | if fileManager.fileExists(atPath: location.path, isDirectory: &isDir) { 62 | location.append(path: url.lastPathComponent) 63 | 64 | try? image.write(to: location) 65 | } else { 66 | try? fileManager.createDirectory(atPath: location.path, withIntermediateDirectories: true, attributes: nil) 67 | location.append(path: url.lastPathComponent) 68 | 69 | try? image.write(to: location) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/ImageResult/ImageResultListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageResultListView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct ImageResultListView: View { 11 | private let data: [URL] 12 | private let content: (_: URL) -> Content 13 | 14 | public init(_ data: [URL], @ViewBuilder content: @escaping (_: URL) -> Content) { 15 | self.data = data 16 | self.content = content 17 | } 18 | 19 | public var body: some View { 20 | ScrollView { 21 | LazyVGrid(columns: [.init(.adaptive(minimum: 256), spacing: 16)], spacing: 16) { 22 | ForEach(data, id: \.self) { url in 23 | content(url) 24 | } 25 | } 26 | .padding() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/PromptField/PromptField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromptField.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import ChatField 9 | import SwiftUI 10 | import ViewState 11 | 12 | public struct PromptField: View { 13 | @Binding private var prompt: String 14 | private var viewState: ViewState? 15 | private var generationAction: () -> Void 16 | private var cancellationAction: () -> Void 17 | 18 | public init( 19 | prompt: Binding, 20 | viewState: ViewState? = nil, 21 | generationAction: @escaping () -> Void, 22 | cancellationAction: @escaping () -> Void 23 | ) { 24 | self._prompt = prompt 25 | self.viewState = viewState 26 | self.generationAction = generationAction 27 | self.cancellationAction = cancellationAction 28 | } 29 | 30 | private var generating: Bool { 31 | viewState == .loading 32 | } 33 | 34 | public var body: some View { 35 | ChatField("Prompt", text: $prompt) { 36 | generationAction() 37 | } trailingAccessory: { 38 | if generating { 39 | CircleButton(systemImage: "stop.fill", action: cancellationAction) 40 | .keyboardShortcut("c", modifiers: .control) 41 | .help("Cancel generation") 42 | } else { 43 | CircleButton(systemImage: "arrow.up", action: generationAction) 44 | .help("Generate image") 45 | } 46 | } footer: { 47 | PromptFieldFooterText(viewState: viewState) 48 | } 49 | .chatFieldDisabled(generating) 50 | .chatFieldStyle(.capsule) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/PromptField/PromptFieldFooterText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromptFieldFooterText.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreExtensions 9 | import SettingsModule 10 | import SwiftUI 11 | import ViewState 12 | 13 | struct PromptFieldFooterText: View { 14 | @Environment(SettingsManager.self) private var settingsManager 15 | 16 | private var viewState: ViewState? 17 | 18 | init(viewState: ViewState? = nil) { 19 | self.viewState = viewState 20 | } 21 | 22 | private var message: AttributedString { 23 | guard settingsManager.autosaveEnabled else { 24 | var string = AttributedString("Auto-save is disabled. Remember to manually save your results.") 25 | string.foregroundColor = .orange 26 | 27 | return string 28 | } 29 | 30 | guard settingsManager.apiKey.isNotNil else { 31 | var string = AttributedString("Please set your API key in the settings to perform the action.") 32 | string.foregroundColor = .red 33 | 34 | return string 35 | } 36 | 37 | var string = AttributedString("A good prompt is essential for the best possible image generation results.") 38 | string.foregroundColor = .secondary 39 | 40 | return string 41 | } 42 | 43 | var body: some View { 44 | Text(message) 45 | .when(viewState, is: .loading) { 46 | Text("Generating image...") 47 | .foregroundStyle(.secondary) 48 | } 49 | .whenError(viewState) { message in 50 | Text(LocalizedStringKey(message)) 51 | .foregroundStyle(.red) 52 | } 53 | .font(.callout) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CoreViews/Sources/CoreViews/Texts/FootnoteText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FootnoteText.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct FootnoteText: View { 11 | private var titleKey: LocalizedStringKey 12 | 13 | public init(_ titleKey: LocalizedStringKey) { 14 | self.titleKey = titleKey 15 | } 16 | 17 | public var body: some View { 18 | VStack { 19 | Text(titleKey) 20 | .multilineTextAlignment(.leading) 21 | .foregroundStyle(.secondary) 22 | .font(.callout) 23 | } 24 | .frame(maxWidth: .infinity, alignment: .leading) 25 | } 26 | } 27 | 28 | #Preview { 29 | FootnoteText("Lorem ipsum dolor sit amet") 30 | } 31 | -------------------------------------------------------------------------------- /ImageEditModule/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /ImageEditModule/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ImageEditModule", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "ImageEditModule", 12 | targets: ["ImageEditModule"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "CoreModels", path: "../CoreModels"), 16 | .package(name: "CoreViewModels", path: "../CoreViewModels"), 17 | .package(name: "CoreViews", path: "../CoreViews"), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "ImageEditModule", 22 | dependencies: ["CoreModels", "CoreViewModels", "CoreViews"]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /ImageEditModule/Sources/ImageEditModule/ImageEditView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageEditView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import CoreExtensions 9 | import CoreModels 10 | import CoreViewModels 11 | import CoreViews 12 | import Defaults 13 | import OpenAI 14 | import SettingsModule 15 | import SwiftUI 16 | 17 | public struct ImageEditView: View { 18 | @Environment(SettingsManager.self) private var settingsManager 19 | @Environment(DalleViewModel.self) private var dalleViewModel 20 | 21 | @State private var prompt: String = "" 22 | 23 | private var number: Int 24 | private var size: DalleModel.Size? 25 | private var imageData: Data? 26 | private var maskData: Data? 27 | 28 | public init( 29 | number: Int, 30 | size: DalleModel.Size? = nil, 31 | imageData: Data? = nil, 32 | maskData: Data? = nil 33 | ) { 34 | self.number = number 35 | self.size = size 36 | self.imageData = imageData 37 | self.maskData = maskData 38 | } 39 | 40 | @MainActor 41 | private var generating: Bool { 42 | self.dalleViewModel.viewState == .loading 43 | } 44 | 45 | public var body: some View { 46 | NavigationStack { 47 | VStack { 48 | ImageResultListView(dalleViewModel.results) { url in 49 | ImageResultListItemView(url: url).tag(url) 50 | } 51 | 52 | PromptField(prompt: $prompt, viewState: dalleViewModel.viewState) { 53 | generationAction() 54 | } cancellationAction: { 55 | dalleViewModel.cancel() 56 | } 57 | .padding(.bottom, 8) 58 | .padding(.horizontal) 59 | } 60 | .navigationTitle("Image Edit") 61 | .onDisappear { 62 | dalleViewModel.cancel() 63 | } 64 | } 65 | } 66 | 67 | @MainActor 68 | func generationAction() { 69 | guard let image = imageData else { return } 70 | guard let mask = maskData else { return } 71 | let size = size?.imagesQuery 72 | let query = ImageEditsQuery(image: image, prompt: prompt, mask: mask, size: size) 73 | 74 | dalleViewModel.setup(apiKey: settingsManager.apiKey) 75 | dalleViewModel.imageEdit(query: query) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ImageGenerationModule/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /ImageGenerationModule/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ImageGenerationModule", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "ImageGenerationModule", 12 | targets: ["ImageGenerationModule"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "CoreModels", path: "../CoreModels"), 16 | .package(name: "CoreViewModels", path: "../CoreViewModels"), 17 | .package(name: "CoreViews", path: "../CoreViews") 18 | ], 19 | targets: [ 20 | .target( 21 | name: "ImageGenerationModule", 22 | dependencies: ["CoreModels", "CoreViewModels", "CoreViews"]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /ImageGenerationModule/Sources/ImageGenerationModule/ImageGenerationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageGenerationView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import CoreModels 9 | import CoreViewModels 10 | import CoreViews 11 | import OpenAI 12 | import SettingsModule 13 | import SwiftUI 14 | 15 | public struct ImageGenerationView: View { 16 | @Environment(SettingsManager.self) private var settingsManager 17 | @Environment(DalleViewModel.self) private var dalleViewModel 18 | 19 | @FocusState private var promptFocused: Bool 20 | @State private var prompt: String = "" 21 | 22 | private var model: DalleModel 23 | private var number: Int 24 | private var size: DalleModel.Size 25 | private var quality: DalleModel.Quality? 26 | private var style: DalleModel.Style? 27 | 28 | public init( 29 | model: DalleModel, 30 | number: Int, 31 | size: DalleModel.Size, 32 | quality: DalleModel.Quality? = nil, 33 | style: DalleModel.Style? = nil 34 | ) { 35 | self.model = model 36 | self.number = number 37 | self.size = size 38 | self.quality = quality 39 | self.style = style 40 | } 41 | 42 | @MainActor 43 | private var generating: Bool { 44 | self.dalleViewModel.viewState == .loading 45 | } 46 | 47 | public var body: some View { 48 | NavigationStack { 49 | VStack { 50 | ImageResultListView(dalleViewModel.results) { url in 51 | ImageResultListItemView(url: url).tag(url) 52 | } 53 | 54 | PromptField(prompt: $prompt, viewState: dalleViewModel.viewState) { 55 | generationAction() 56 | } cancellationAction: { 57 | dalleViewModel.cancel() 58 | } 59 | .padding(.bottom, 8) 60 | .padding(.horizontal) 61 | } 62 | .navigationTitle("Image Generation") 63 | .onDisappear { 64 | dalleViewModel.cancel() 65 | } 66 | } 67 | } 68 | 69 | @MainActor 70 | func generationAction() { 71 | let model = model.rawValue 72 | let size = size.imagesQuery 73 | let quality = quality?.imagesQuery 74 | let style = style?.imagesQuery 75 | let query = ImagesQuery(prompt: prompt, model: model, n: number, quality: quality, size: size, style: style) 76 | 77 | dalleViewModel.setup(apiKey: settingsManager.apiKey) 78 | dalleViewModel.imageGeneration(query: query) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ImagePreferencesModule/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ImagePreferencesModule", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "ImagePreferencesModule", 12 | targets: ["ImagePreferencesModule"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "CoreExtensions", path: "../CoreExtensions"), 16 | .package(name: "CoreModels", path: "../CoreModels"), 17 | .package(name: "CoreViewModels", path: "../CoreViewModels"), 18 | .package(name: "CoreViews", path: "../CoreViews"), 19 | .package(url: "https://github.com/kevinhermawan/ViewCondition.git", .upToNextMajor(from: "1.0.0")) 20 | ], 21 | targets: [ 22 | .target( 23 | name: "ImagePreferencesModule", 24 | dependencies: ["CoreExtensions", "CoreModels", "CoreViewModels", "CoreViews", "ViewCondition"]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Extensions/NSImage+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+Utils.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import Cocoa 9 | import UniformTypeIdentifiers 10 | 11 | extension NSImage { 12 | var isValidPNG: Bool { 13 | guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { 14 | return false 15 | } 16 | 17 | let data = NSMutableData() 18 | 19 | if let destination = CGImageDestinationCreateWithData(data, UTType.png.identifier as CFString, 1, nil) { 20 | CGImageDestinationAddImage(destination, cgImage, nil) 21 | 22 | if CGImageDestinationFinalize(destination) { 23 | let pngSignature: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] 24 | let imageDataBytes = [UInt8](data.prefix(8)) 25 | 26 | return imageDataBytes == pngSignature 27 | } 28 | } 29 | 30 | return false 31 | } 32 | 33 | var isSquare: Bool { 34 | guard let primaryRepresentation = self.representations.first else { return false } 35 | 36 | return primaryRepresentation.size.width == primaryRepresentation.size.height 37 | } 38 | 39 | func isLargerThan(maxSizeInMB: Double) -> Bool { 40 | guard let tiffRepresentation = self.tiffRepresentation, 41 | let bitmapImage = NSBitmapImageRep(data: tiffRepresentation) else { 42 | return false 43 | } 44 | 45 | guard let imageData = bitmapImage.representation(using: .png, properties: [:]) else { 46 | return false 47 | } 48 | 49 | let imageSizeInMB = Double(imageData.count) / (1024.0 * 1024.0) 50 | 51 | return imageSizeInMB > maxSizeInMB 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/ImagePreferencesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePreferencesView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import CoreExtensions 9 | import CoreModels 10 | import CoreViewModels 11 | import CoreViews 12 | import SwiftUI 13 | import ViewCondition 14 | 15 | public struct ImagePreferencesView: View { 16 | private var isModelPickerHidden: Bool = false 17 | private var isImagePickerVisible: Bool = false 18 | private var isMaskPickerVisible: Bool = false 19 | private var imageFootnote: LocalizedStringKey = "" 20 | private var maskFootnote: LocalizedStringKey = "" 21 | 22 | @Binding private var modelSelection: DalleModel 23 | @Binding private var numberSelection: Int 24 | @Binding private var sizeSelection: DalleModel.Size 25 | @Binding private var qualitySelection: DalleModel.Quality? 26 | @Binding private var styleSelection: DalleModel.Style? 27 | @Binding private var imageData: Data? 28 | @Binding private var maskData: Data? 29 | 30 | @State private var imageError: Error? = nil 31 | @State private var maskError: Error? = nil 32 | 33 | public init( 34 | modelSelection: Binding, 35 | numberSelection: Binding, 36 | sizeSelection: Binding, 37 | qualitySelection: Binding, 38 | styleSelection: Binding, 39 | imageData: Binding, 40 | maskData: Binding 41 | ) { 42 | self._modelSelection = modelSelection 43 | self._numberSelection = numberSelection 44 | self._sizeSelection = sizeSelection 45 | self._qualitySelection = qualitySelection 46 | self._styleSelection = styleSelection 47 | self._imageData = imageData 48 | self._maskData = maskData 49 | } 50 | 51 | public var body: some View { 52 | Form { 53 | ModelPicker(selection: $modelSelection) 54 | .hide(if: isModelPickerHidden, removeCompletely: true) 55 | 56 | ImagePicker("Image", data: $imageData, error: $imageError) 57 | .footnote(imageFootnote) 58 | .visible(if: isImagePickerVisible, removeCompletely: true) 59 | 60 | ImagePicker("Mask", data: $maskData, error: $maskError) 61 | .footnote(maskFootnote) 62 | .visible(if: isMaskPickerVisible, removeCompletely: true) 63 | 64 | NumberPicker(modelSelection.numbers, selection: $numberSelection) 65 | .visible(if: modelSelection.numbers.contains(numberSelection), removeCompletely: true) 66 | 67 | SizePicker(modelSelection.sizes, selection: $sizeSelection) 68 | .visible(if: modelSelection.sizes.contains(sizeSelection), removeCompletely: true) 69 | 70 | if let qualities = modelSelection.qualities, let selection = Binding($qualitySelection) { 71 | QualityPicker(qualities, selection: selection) 72 | } 73 | 74 | if let styles = modelSelection.styles, let selection = Binding($styleSelection) { 75 | StylePicker(styles, selection: selection) 76 | } 77 | } 78 | .inspectorColumnWidth(min: 320, ideal: 320) 79 | .onChange(of: modelSelection) { 80 | numberSelection = modelSelection.numbers[0] 81 | sizeSelection = modelSelection.sizes[0] 82 | qualitySelection = modelSelection.qualities?.first ?? .standard 83 | styleSelection = modelSelection.styles?.first ?? .vivid 84 | } 85 | } 86 | 87 | public func modelPickerHidden() -> ImagePreferencesView { 88 | var view = self 89 | view.isModelPickerHidden = true 90 | 91 | return view 92 | } 93 | 94 | public func imagePickerVisible(text: LocalizedStringKey) -> ImagePreferencesView { 95 | var view = self 96 | view.isImagePickerVisible = true 97 | view.imageFootnote = text 98 | 99 | return view 100 | } 101 | 102 | public func maskPickerVisible(text: LocalizedStringKey) -> ImagePreferencesView { 103 | var view = self 104 | view.isMaskPickerVisible = true 105 | view.maskFootnote = text 106 | 107 | return view 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/ImagePickers/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreViews 9 | import SwiftUI 10 | 11 | struct ImagePicker: View { 12 | private let title: String 13 | private var footnote: LocalizedStringKey? = nil 14 | 15 | @Binding private var data: Data? 16 | @Binding private var error: Error? 17 | 18 | init(_ title: String, data: Binding, error: Binding) { 19 | self.title = title 20 | self._data = data 21 | self._error = error 22 | } 23 | 24 | var body: some View { 25 | Section { 26 | if let data { 27 | ImagePickerPreviewView(data: data) 28 | } else { 29 | ImagePickerEmptyView("No \(title)") { data, error in 30 | self.data = data 31 | self.error = error 32 | } 33 | } 34 | } header: { 35 | SectionHeader(LocalizedStringKey(title)) 36 | .if(data.isNotNil) { view in 37 | view.action("Remove", action: { data = nil }) 38 | } 39 | } footer: { 40 | if let errorMessage = error?.localizedDescription { 41 | FootnoteText(LocalizedStringKey(errorMessage)) 42 | .foregroundStyle(.red) 43 | } else if let footnote { 44 | FootnoteText(footnote) 45 | } 46 | } 47 | } 48 | 49 | func footnote(_ note: LocalizedStringKey) -> ImagePicker { 50 | var view = self 51 | view.footnote = note 52 | 53 | return view 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/ImagePickers/ImagePickerButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerButton.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImagePickerButton: View { 11 | private var titleKey: LocalizedStringKey 12 | private var action: (_ data: Data?, _ error: Error?) -> Void 13 | 14 | @State private var presented: Bool = false 15 | 16 | init(_ titleKey: LocalizedStringKey, action: @escaping (_: Data?, _: Error?) -> Void) { 17 | self.titleKey = titleKey 18 | self.action = action 19 | } 20 | 21 | var body: some View { 22 | Button(action: { presented = true }) { 23 | Label(titleKey, systemImage: "photo.on.rectangle.angled") 24 | } 25 | .fileImporter(isPresented: $presented, allowedContentTypes: [.png]) { result in 26 | switch result { 27 | case .success(let url): 28 | if url.startAccessingSecurityScopedResource() { 29 | self.process(url: url) 30 | } else { 31 | self.action(nil, ImagePickerError.fileAccessError) 32 | } 33 | case .failure(let error): 34 | self.action(nil, error) 35 | } 36 | } 37 | } 38 | 39 | func process(url: URL) { 40 | do { 41 | let data = try Data(contentsOf: url) 42 | let validatedData = try self.validate(data: data) 43 | url.stopAccessingSecurityScopedResource() 44 | 45 | self.action(validatedData, nil) 46 | } catch { 47 | self.action(nil, error) 48 | } 49 | } 50 | 51 | func validate(data: Data) throws -> Data { 52 | guard let image = NSImage(data: data), image.isValidPNG else { 53 | throw ImagePickerError.unsupportedImageFormat 54 | } 55 | 56 | guard !image.isLargerThan(maxSizeInMB: 4.0) else { 57 | throw ImagePickerError.oversizedFile 58 | } 59 | 60 | guard image.isSquare else { 61 | throw ImagePickerError.nonUniformDimensions 62 | } 63 | 64 | return data 65 | } 66 | 67 | enum ImagePickerError: LocalizedError { 68 | case fileAccessError, unsupportedImageFormat, oversizedFile, nonUniformDimensions 69 | 70 | var errorDescription: String? { 71 | switch self { 72 | case .fileAccessError: 73 | return "There was an error accessing the file." 74 | case .unsupportedImageFormat: 75 | return "The image format is not supported." 76 | case .oversizedFile: 77 | return "The image exceeds the maximum file size limit." 78 | case .nonUniformDimensions: 79 | return "The image dimensions must be a square." 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/ImagePickers/ImagePickerEmptyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerEmptyView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImagePickerEmptyView: View { 11 | private var titleKey: LocalizedStringKey 12 | private var action: (_ data: Data?, _ error: Error?) -> Void 13 | 14 | init(_ titleKey: LocalizedStringKey, action: @escaping (_: Data?, _: Error?) -> Void) { 15 | self.titleKey = titleKey 16 | self.action = action 17 | } 18 | 19 | var body: some View { 20 | HStack { 21 | Text(titleKey) 22 | 23 | Spacer() 24 | 25 | ImagePickerButton("Choose", action: action) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/ImagePickers/ImagePickerPreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerPreviewView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImagePickerPreviewView: View { 11 | private var data: Data 12 | 13 | init(data: Data) { 14 | self.data = data 15 | } 16 | 17 | var body: some View { 18 | if let nsImage = NSImage(data: data) { 19 | Image(nsImage: nsImage) 20 | .resizable() 21 | .aspectRatio(contentMode: .fit) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/Pickers/ModelPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelPicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreModels 9 | import CoreViews 10 | import SwiftUI 11 | 12 | struct ModelPicker: View { 13 | @Binding var selection: DalleModel 14 | 15 | var body: some View { 16 | Section { 17 | Picker("Selected model", selection: $selection) { 18 | ForEach(DalleModel.allCases) { model in 19 | Text(model.title).tag(model) 20 | } 21 | } 22 | } header: { 23 | Text("Model") 24 | } footer: { 25 | FootnoteText("FOOTNOTE_MODEL") 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/Pickers/NumberPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberPicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreViews 9 | import SwiftUI 10 | 11 | struct NumberPicker: View { 12 | private let numbers: [Int] 13 | @Binding private var selection: Int 14 | 15 | init(_ numbers: [Int], selection: Binding) { 16 | self.numbers = numbers 17 | self._selection = selection 18 | } 19 | 20 | var body: some View { 21 | Section { 22 | Picker("Selected number", selection: $selection) { 23 | ForEach(numbers, id: \.self) { number in 24 | Text(String(number)).tag(number) 25 | } 26 | } 27 | } header: { 28 | Text("Number") 29 | } footer: { 30 | FootnoteText("FOOTNOTE_NUMBER") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/Pickers/QualityPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QualityPicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreModels 9 | import CoreViews 10 | import SwiftUI 11 | 12 | struct QualityPicker: View { 13 | private let qualities: [DalleModel.Quality] 14 | @Binding private var selection: DalleModel.Quality 15 | 16 | init(_ qualities: [DalleModel.Quality], selection: Binding) { 17 | self.qualities = qualities 18 | self._selection = selection 19 | } 20 | 21 | var body: some View { 22 | Section { 23 | Picker("Selected quality", selection: $selection) { 24 | ForEach(qualities) { quality in 25 | Text(quality.title).tag(quality) 26 | } 27 | } 28 | } header: { 29 | Text("Quality") 30 | } footer: { 31 | if selection == DalleModel.Quality.hd { 32 | FootnoteText("FOOTNOTE_QUALITY_HD") 33 | } else { 34 | FootnoteText("FOOTNOTE_QUALITY_STANDARD") 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/Pickers/SizePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SizePicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreModels 9 | import CoreViews 10 | import SwiftUI 11 | 12 | struct SizePicker: View { 13 | private let sizes: [DalleModel.Size] 14 | @Binding private var selection: DalleModel.Size 15 | 16 | init(_ sizes: [DalleModel.Size], selection: Binding) { 17 | self.sizes = sizes 18 | self._selection = selection 19 | } 20 | 21 | var body: some View { 22 | Section { 23 | Picker("Selected size", selection: $selection) { 24 | ForEach(sizes, id: \.self) { size in 25 | Text(size.title).tag(size) 26 | } 27 | } 28 | } header: { 29 | Text("Size") 30 | } footer: { 31 | FootnoteText("FOOTNOTE_SIZE") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/Pickers/StylePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StylePicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import CoreModels 9 | import CoreViews 10 | import SwiftUI 11 | 12 | struct StylePicker: View { 13 | private let styles: [DalleModel.Style] 14 | @Binding private var selection: DalleModel.Style 15 | 16 | init(_ styles: [DalleModel.Style], selection: Binding) { 17 | self.styles = styles 18 | self._selection = selection 19 | } 20 | 21 | var body: some View { 22 | Section { 23 | Picker("Selected style", selection: $selection) { 24 | ForEach(styles) { style in 25 | Text(style.title).tag(style) 26 | } 27 | } 28 | } header: { 29 | Text("Style") 30 | } footer: { 31 | if selection == DalleModel.Style.vivid { 32 | FootnoteText("FOOTNOTE_STYLE_VIVID") 33 | } else { 34 | FootnoteText("FOOTNOTE_STYLE_NATURAL") 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ImagePreferencesModule/Sources/ImagePreferencesModule/Subviews/SectionHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeader.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 26/12/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SectionHeader: View { 11 | private let titleKey: LocalizedStringKey 12 | private var actionTitleKey: LocalizedStringKey? 13 | private var action: (() -> Void)? 14 | 15 | init(_ titleKey: LocalizedStringKey) { 16 | self.titleKey = titleKey 17 | } 18 | 19 | var body: some View { 20 | HStack { 21 | Text(titleKey) 22 | 23 | if let actionTitleKey, let action { 24 | Spacer() 25 | 26 | Button(actionTitleKey, action: action) 27 | .fontWeight(.medium) 28 | .buttonStyle(.link) 29 | } 30 | } 31 | } 32 | 33 | func action(_ titleKey: LocalizedStringKey, action: @escaping () -> Void) -> SectionHeader { 34 | var view = self 35 | view.actionTitleKey = titleKey 36 | view.action = action 37 | 38 | return view 39 | } 40 | } 41 | 42 | #Preview { 43 | Form { 44 | Section { 45 | Text("Content") 46 | } header: { 47 | SectionHeader("Section Header") 48 | .action("Remove", action: {}) 49 | } 50 | } 51 | .padding() 52 | } 53 | -------------------------------------------------------------------------------- /ImageVariationModule/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /ImageVariationModule/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ImageVariationModule", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "ImageVariationModule", 12 | targets: ["ImageVariationModule"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "CoreModels", path: "../CoreModels"), 16 | .package(name: "CoreViewModels", path: "../CoreViewModels"), 17 | .package(name: "CoreViews", path: "../CoreViews"), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "ImageVariationModule", 22 | dependencies: ["CoreModels", "CoreViewModels", "CoreViews"]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /ImageVariationModule/Sources/ImageVariationModule/ImageVariationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageVariationView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 25/12/23. 6 | // 7 | 8 | import CoreModels 9 | import CoreModels 10 | import CoreViewModels 11 | import CoreViews 12 | import OpenAI 13 | import SettingsModule 14 | import SwiftUI 15 | import ViewCondition 16 | import ViewState 17 | 18 | public struct ImageVariationView: View { 19 | @Environment(SettingsManager.self) private var settingsManager 20 | @Environment(DalleViewModel.self) private var dalleViewModel 21 | 22 | private var number: Int 23 | private var size: DalleModel.Size? 24 | private var imageData: Data? 25 | 26 | public init( 27 | number: Int, 28 | size: DalleModel.Size? = nil, 29 | imageData: Data? = nil 30 | ) { 31 | self.number = number 32 | self.size = size 33 | self.imageData = imageData 34 | } 35 | 36 | @MainActor 37 | private var generating: Bool { 38 | self.dalleViewModel.viewState == .loading 39 | } 40 | 41 | public var body: some View { 42 | NavigationStack { 43 | VStack { 44 | ImageResultListView(dalleViewModel.results) { url in 45 | ImageResultListItemView(url: url).tag(url) 46 | } 47 | 48 | HStack { 49 | if let errorMessage = dalleViewModel.viewState?.errorMessage { 50 | Text(LocalizedStringKey(errorMessage)) 51 | .foregroundStyle(.red) 52 | .font(.callout) 53 | } 54 | 55 | Spacer() 56 | 57 | Button(action: { generationAction() }) { 58 | Label("Generate Variation", systemImage: "arrow.up.circle.fill") 59 | .padding(.vertical, 8) 60 | .padding(.horizontal) 61 | } 62 | .buttonStyle(.borderedProminent) 63 | .hide(if: generating, removeCompletely: true) 64 | 65 | Button(action: { dalleViewModel.cancel() }) { 66 | Label("Cancel Generation", systemImage: "stop.fill") 67 | .padding(.vertical, 8) 68 | .padding(.horizontal) 69 | } 70 | .buttonStyle(.bordered) 71 | .visible(if: generating, removeCompletely: true) 72 | } 73 | .padding() 74 | } 75 | .navigationTitle("Image Variation") 76 | .onDisappear { 77 | dalleViewModel.cancel() 78 | } 79 | } 80 | } 81 | 82 | @MainActor 83 | func generationAction() { 84 | guard let image = imageData else { return } 85 | let size = size?.rawValue 86 | let query = ImageVariationsQuery(image: image, n: number, size: size) 87 | 88 | dalleViewModel.setup(apiKey: settingsManager.apiKey) 89 | dalleViewModel.imageVariation(query: query) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Open Source License 2 | 3 | Canvas is licensed under the Apache License 2.0, with the following additional conditions: 4 | 5 | 1. Canvas may be utilized commercially, including as a standalone macOS application. Should the conditions below be met, a commercial license must be obtained from the producer: 6 | 7 | a. Redistribution Restriction: You may not redistribute Canvas or any derivative works under the name "Canvas" or any variation thereof, including but not limited to "Canvas X". Here, "X" refers to any suffix that implies a different version or variant of the original Canvas, such as "Canvas Pro", etc. You must change the name of the Work or Derivative Works when distributing. 8 | 9 | b. Branding and Copyright Information: You may not remove or modify the branding or copyright information in the Canvas application. This restriction applies to the visual elements and branding present in the application. 10 | 11 | Please contact kevin@hermawan.ai by email to inquire about licensing matters. 12 | 13 | 2. As a contributor, you should agree that: 14 | 15 | a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary. 16 | 17 | b. Your contributed code may be used for commercial purposes, including but not limited to its use in business operations. 18 | 19 | Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). 20 | 21 | The interactive design of this product is protected by an appearance patent. 22 | 23 | © 2024 Kevin Hermawan 24 | 25 | --- 26 | 27 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 28 | 29 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 30 | 31 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 32 | -------------------------------------------------------------------------------- /ModelPricingModule/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /ModelPricingModule/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ModelPricingModule", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "ModelPricingModule", 12 | targets: ["ModelPricingModule"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "CoreModels", path: "../CoreModels"), 16 | .package(name: "CoreViewModels", path: "../CoreViewModels"), 17 | .package(name: "CoreViews", path: "../CoreViews"), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "ModelPricingModule", 22 | dependencies: ["CoreModels", "CoreViewModels", "CoreViews"]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /ModelPricingModule/Sources/ModelPricingModule/ModelPricingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelPricingView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 28/12/23. 6 | // 7 | 8 | import CoreModels 9 | import CoreViewModels 10 | import SwiftUI 11 | import ViewCondition 12 | 13 | public struct ModelPricingView: View { 14 | @Environment(\.openURL) private var openURL 15 | @Environment(DalleModelInfoViewModel.self) private var dalleModelInfoViewModel 16 | 17 | public init() {} 18 | 19 | @MainActor 20 | private var navigationSubtitle: String { 21 | switch dalleModelInfoViewModel.viewState { 22 | case .loading: 23 | return "Fetching..." 24 | case .error(let message): 25 | return message 26 | default: 27 | guard let updatedAt = dalleModelInfoViewModel.pricingUpdatedAt else { return "" } 28 | let updatedAtFormatted = updatedAt.formatted(date: .numeric, time: .omitted) 29 | 30 | return "Last updated on \(updatedAtFormatted)" 31 | } 32 | } 33 | 34 | public var body: some View { 35 | NavigationStack { 36 | Table(dalleModelInfoViewModel.pricing ) { 37 | TableColumn("Model") { 38 | Text($0.model.title) 39 | .fontWeight(.semibold) 40 | } 41 | 42 | TableColumn("Quality", value: \.qualityTitle) 43 | TableColumn("Resolution", value: \.resolution) 44 | 45 | TableColumn("Price") { 46 | Text($0.attributedPrice) 47 | } 48 | .alignment(.trailing) 49 | } 50 | .navigationTitle("Model Pricing") 51 | .navigationSubtitle(navigationSubtitle) 52 | .task { 53 | await dalleModelInfoViewModel.fetch() 54 | } 55 | .toolbar { 56 | ToolbarItem(placement: .primaryAction) { 57 | Button("Pricing Info", systemImage: "info.circle") { 58 | pricingInfoAction() 59 | } 60 | .labelStyle(.titleAndIcon) 61 | } 62 | } 63 | } 64 | } 65 | 66 | @MainActor 67 | func pricingInfoAction() { 68 | guard let infoUrl = dalleModelInfoViewModel.pricingInfoUrl else { return } 69 | guard let url = URL(string: infoUrl) else { return } 70 | 71 | openURL(url) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | app icon 4 |

Canvas - DALL·E Playground for the Mac

5 | 6 | 7 | banner 8 | 9 |
10 |

Illustration of kids drawing in the Borobudur Temple, generated by DALL·E 3.

11 |
12 |
13 |
14 | 15 | ## Prerequisites 16 | 17 | - macOS 14.0 Sonoma or later. 18 | - OpenAI API key (You can get it [here](https://platform.openai.com/api-keys)). 19 | 20 | ## Download 21 | 22 | You can download the latest version of Canvas from the [releases page](https://github.com/kevinhermawan/Canvas/releases). 23 | 24 | ## Features 25 | 26 | - Image Generation with DALL·E 3 & DALL·E 2. 27 | - Image Editing (_currently only for DALL·E 2_). 28 | - Image Variations (_currently only for DALL·E 2_). 29 | - Copy, save, and share generated images. 30 | - Simple and easy to use. 31 | - Free and open source. 32 | - Native. 33 | 34 | And more... 35 | 36 | ## Screenshot 37 | 38 |
39 | 40 | 41 | screenshot 42 | 43 |
44 | 45 | ## Acknowledgements 46 | 47 | - [AppInfo by @kevinhermawan](https://github.com/kevinhermawan/AppInfo) 48 | - [ChatField by @kevinhermawan](https://github.com/kevinhermawan/ChatField) 49 | - [Defaults by @sindresorhus](https://github.com/sindresorhus/Defaults) 50 | - [KeychainAccess by @kishikawakatsumi](https://github.com/kishikawakatsumi/KeychainAccess) 51 | - [Nuke by @kean](https://github.com/kean/Nuke) 52 | - [OpenAI by @MacPaw](https://github.com/MacPaw/OpenAI) 53 | - [Sparkle by @sparkle-project](https://github.com/sparkle-project/Sparkle) 54 | - [ViewCondition by @kevinhermawan](https://github.com/kevinhermawan/ViewCondition) 55 | - [ViewState by @kevinhermawan](https://github.com/kevinhermawan/ViewState) 56 | 57 | ## License 58 | 59 | This repository is available under the [Apache License 2.0](./LICENSE), with a few additional restrictions. 60 | -------------------------------------------------------------------------------- /SettingsModule/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /SettingsModule/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SettingsModule", 8 | platforms: [.macOS(.v14)], 9 | products: [ 10 | .library( 11 | name: "SettingsModule", 12 | targets: ["SettingsModule"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "CoreExtensions", path: "../CoreExtensions"), 16 | .package(url: "https://github.com/sindresorhus/Defaults.git", .upToNextMajor(from: "8.2.0")) 17 | ], 18 | targets: [ 19 | .target( 20 | name: "SettingsModule", 21 | dependencies: ["CoreExtensions", "Defaults"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /SettingsModule/Sources/SettingsModule/Extensions/Defaults+Keys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults+Keys.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 09/01/24. 6 | // 7 | 8 | import Defaults 9 | import Foundation 10 | 11 | let fileManager = FileManager.default 12 | let homeDirectory = fileManager.homeDirectoryForCurrentUser 13 | 14 | public extension Defaults.Keys { 15 | static let autosaveEnabled = Key("autosaveEnabled", default: true) 16 | static let autosaveLocation = Key("autosaveLocation", default: homeDirectory.appending(path: "Canvas")) 17 | } 18 | -------------------------------------------------------------------------------- /SettingsModule/Sources/SettingsModule/Managers/SettingsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsManager.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 09/01/24. 6 | // 7 | 8 | import Defaults 9 | import Foundation 10 | import KeychainAccess 11 | 12 | @Observable 13 | public final class SettingsManager { 14 | private let keychainKey: String = "apikey" 15 | private var keychain: Keychain 16 | 17 | public var apiKey: String? = nil 18 | 19 | public var apiKeyMasked: String? { 20 | let prefixLength = 3 21 | let suffixLength = 4 22 | 23 | guard let apiKey, apiKey.count > prefixLength + suffixLength else { return nil } 24 | let prefix = apiKey.prefix(prefixLength) 25 | let suffix = apiKey.suffix(suffixLength) 26 | 27 | return "\(prefix)····················\(suffix)" 28 | } 29 | 30 | public var autosaveEnabled: Bool = Defaults[.autosaveEnabled] { 31 | didSet { 32 | Defaults[.autosaveEnabled] = autosaveEnabled 33 | } 34 | } 35 | 36 | public var autosaveLocation: URL = Defaults[.autosaveLocation] { 37 | didSet { 38 | Defaults[.autosaveLocation] = autosaveLocation 39 | } 40 | } 41 | 42 | public init(keychain: Keychain) { 43 | self.keychain = keychain 44 | self.fetchAPIKey() 45 | } 46 | } 47 | 48 | extension SettingsManager { 49 | public func fetchAPIKey() { 50 | self.apiKey = try? keychain.get(keychainKey) 51 | } 52 | 53 | public func setupAPIKey(apiKey: String) { 54 | try? keychain.set(apiKey, key: keychainKey) 55 | self.fetchAPIKey() 56 | } 57 | 58 | public func removeAPIKey() { 59 | try? keychain.remove(keychainKey) 60 | self.fetchAPIKey() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SettingsModule/Sources/SettingsModule/Views/GeneralView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 09/01/24. 6 | // 7 | 8 | import SwiftUI 9 | import ViewCondition 10 | 11 | struct GeneralView: View { 12 | @Environment(SettingsManager.self) private var manager 13 | 14 | var body: some View { 15 | @Bindable var manager = manager 16 | 17 | VStack(alignment: .leading, spacing: 16) { 18 | GroupBox { 19 | APIKeyPicker( 20 | apiKeyMasked: manager.apiKeyMasked, 21 | onSetup: { manager.setupAPIKey(apiKey: $0) }, 22 | onRemove: { manager.removeAPIKey() } 23 | ) 24 | } 25 | 26 | GroupBox { 27 | AutosavePicker( 28 | enabled: $manager.autosaveEnabled, 29 | location: $manager.autosaveLocation, 30 | action: chooseAutosaveDirectoryAction 31 | ) 32 | } 33 | } 34 | } 35 | 36 | private func chooseAutosaveDirectoryAction() { 37 | let openPanel = NSOpenPanel() 38 | openPanel.canChooseFiles = false 39 | openPanel.canChooseDirectories = true 40 | openPanel.canCreateDirectories = true 41 | openPanel.allowsMultipleSelection = false 42 | openPanel.prompt = "Choose" 43 | 44 | openPanel.begin { response in 45 | if response == .OK, let url = openPanel.urls.first { 46 | manager.autosaveLocation = url 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SettingsModule/Sources/SettingsModule/Views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 09/01/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct SettingsView: View { 11 | public init() {} 12 | 13 | public var body: some View { 14 | VStack { 15 | TabView { 16 | GeneralView() 17 | .tabItem { 18 | Label("General", systemImage: "gearshape") 19 | } 20 | } 21 | } 22 | .padding() 23 | .frame(width: 450) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SettingsModule/Sources/SettingsModule/Views/Subviews/APIKeyPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIKeyPicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 11/01/24. 6 | // 7 | 8 | import CoreExtensions 9 | import SwiftUI 10 | 11 | struct APIKeyPicker: View { 12 | private var apiKeyMasked: String? 13 | private var onSetup: (_: String) -> Void 14 | private var onRemove: () -> Void 15 | 16 | @FocusState private var apiKeyFocused: Bool 17 | @State private var apiKey: String = "" 18 | 19 | @State private var isEditing: Bool = false 20 | 21 | public init(apiKeyMasked: String?, onSetup: @escaping (_: String) -> Void, onRemove: @escaping () -> Void) { 22 | self.apiKeyMasked = apiKeyMasked 23 | self.onSetup = onSetup 24 | self.onRemove = onRemove 25 | } 26 | 27 | var body: some View { 28 | VStack(alignment: .leading, spacing: 16) { 29 | HStack { 30 | Text("API Key") 31 | .font(.headline.weight(.semibold)) 32 | 33 | Spacer() 34 | 35 | if isEditing { 36 | Button("Cancel") { isEditing = false } 37 | .buttonStyle(.link) 38 | } else { 39 | if apiKeyMasked.isNotNil { 40 | Button("Remove", action: onRemove) 41 | .buttonStyle(.link) 42 | } 43 | } 44 | } 45 | 46 | HStack { 47 | if isEditing { 48 | TextField("", text: $apiKey) 49 | .focused($apiKeyFocused) 50 | .onAppear { apiKeyFocused = true } 51 | .onSubmit { doneAction() } 52 | } else { 53 | if let apiKeyMasked { 54 | Text(apiKeyMasked) 55 | .foregroundStyle(.secondary) 56 | } else { 57 | Text("Please set the API key") 58 | .foregroundStyle(.red) 59 | } 60 | } 61 | 62 | Spacer() 63 | 64 | if isEditing { 65 | Button("Done", action: doneAction) 66 | } else { 67 | Button("Change") { isEditing = true } 68 | } 69 | } 70 | } 71 | .padding(4) 72 | } 73 | 74 | private func doneAction() { 75 | onSetup(apiKey) 76 | 77 | apiKey = "" 78 | isEditing = false 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SettingsModule/Sources/SettingsModule/Views/Subviews/AutosavePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutosavePicker.swift 3 | // 4 | // 5 | // Created by Kevin Hermawan on 09/01/24. 6 | // 7 | 8 | import SwiftUI 9 | import ViewCondition 10 | 11 | struct AutosavePicker: View { 12 | @Binding private var enabled: Bool 13 | @Binding private var location: URL 14 | private var action: () -> Void 15 | 16 | public init(enabled: Binding, location: Binding, action: @escaping () -> Void) { 17 | self._enabled = enabled 18 | self._location = location 19 | self.action = action 20 | } 21 | 22 | private var disabled: Bool { 23 | enabled == false 24 | } 25 | 26 | var body: some View { 27 | VStack(spacing: 16) { 28 | HStack { 29 | Text("Auto-save Results") 30 | .font(.headline.weight(.semibold)) 31 | 32 | Spacer() 33 | 34 | Toggle("", isOn: $enabled) 35 | .toggleStyle(.switch) 36 | .labelsHidden() 37 | } 38 | 39 | HStack { 40 | Text(location.path) 41 | .foregroundStyle(.secondary) 42 | .if(disabled) { view in 43 | view.foregroundStyle(.tertiary) 44 | } 45 | 46 | Spacer() 47 | 48 | Button(action: action) { 49 | Image(systemName: "folder") 50 | } 51 | .disabled(disabled) 52 | .help("Choose directory") 53 | } 54 | } 55 | .padding(4) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Canvas 5 | 6 | 1.0.6 7 | Mon, 05 Aug 2024 17:32:28 +0700 8 | 7 9 | 1.0.6 10 | 14.0 11 | 12 | Canvas v1.0.6 14 |

Bug Fixes

15 |
    16 |
  • Fixed: wrong orientation labels
  • 17 |
18 |

For a complete list of changes, visit our Full Changelog.

19 | ]]> 20 |
21 | 22 |
23 |
24 |
-------------------------------------------------------------------------------- /assets/banner-night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/assets/banner-night.jpg -------------------------------------------------------------------------------- /assets/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/assets/banner.jpg -------------------------------------------------------------------------------- /assets/press-kit.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/assets/press-kit.zip -------------------------------------------------------------------------------- /assets/screenshot-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/assets/screenshot-dark.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinhermawan/Canvas/5b1cbf5294ebdc39132cffbae26ba58083038a37/assets/screenshot.png -------------------------------------------------------------------------------- /model-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "pricing": [ 3 | { 4 | "model": "dall-e-3", 5 | "quality": "standard", 6 | "resolution": "1024x1024", 7 | "price": 0.04 8 | }, 9 | { 10 | "model": "dall-e-3", 11 | "quality": "standard", 12 | "resolution": "1024x1792", 13 | "price": 0.08 14 | }, 15 | { 16 | "model": "dall-e-3", 17 | "quality": "standard", 18 | "resolution": "1792x1024", 19 | "price": 0.08 20 | }, 21 | { 22 | "model": "dall-e-3", 23 | "quality": "hd", 24 | "resolution": "1024x1024", 25 | "price": 0.08 26 | }, 27 | { 28 | "model": "dall-e-3", 29 | "quality": "hd", 30 | "resolution": "1024x1792", 31 | "price": 0.12 32 | }, 33 | { 34 | "model": "dall-e-3", 35 | "quality": "hd", 36 | "resolution": "1792x1024", 37 | "price": 0.12 38 | }, 39 | { 40 | "model": "dall-e-2", 41 | "quality": null, 42 | "resolution": "256x256", 43 | "price": 0.016 44 | }, 45 | { 46 | "model": "dall-e-2", 47 | "quality": null, 48 | "resolution": "512x512", 49 | "price": 0.018 50 | }, 51 | { 52 | "model": "dall-e-2", 53 | "quality": null, 54 | "resolution": "1024x1024", 55 | "price": 0.02 56 | } 57 | ], 58 | "pricing_info_url": "https://openai.com/pricing", 59 | "pricing_updated_at": "25/12/2023" 60 | } 61 | --------------------------------------------------------------------------------