Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds auto-save results #7

Merged
merged 1 commit into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Canvas.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0A03693D2B4D6FBB00D7DAD4 /* SettingsModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A03693C2B4D6FBB00D7DAD4 /* SettingsModule */; };
0A20744D2B3CD3E5008516D1 /* ModelPricingModule in Frameworks */ = {isa = PBXBuildFile; productRef = 0A20744C2B3CD3E5008516D1 /* ModelPricingModule */; };
0A809A402B39752200C9A015 /* CanvasApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A809A3F2B39752200C9A015 /* CanvasApp.swift */; };
0A809A462B39752300C9A015 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A809A452B39752300C9A015 /* Assets.xcassets */; };
Expand All @@ -32,6 +33,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
0A03693B2B4D6F7600D7DAD4 /* SettingsModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SettingsModule; sourceTree = "<group>"; };
0A20744B2B3CD385008516D1 /* ModelPricingModule */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ModelPricingModule; sourceTree = "<group>"; };
0A809A3C2B39752200C9A015 /* Canvas.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Canvas.app; sourceTree = BUILT_PRODUCTS_DIR; };
0A809A3F2B39752200C9A015 /* CanvasApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasApp.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -66,6 +68,7 @@
0A809A632B397C3B00C9A015 /* ImageEditModule in Frameworks */,
0A809A612B397C3B00C9A015 /* CoreViews in Frameworks */,
0A809A652B397C3B00C9A015 /* ImageGenerationModule in Frameworks */,
0A03693D2B4D6FBB00D7DAD4 /* SettingsModule in Frameworks */,
0A809A5B2B397B6100C9A015 /* CoreViewModels in Frameworks */,
0A809A672B397C3B00C9A015 /* ImageVariationModule in Frameworks */,
0A809A752B398AEE00C9A015 /* ImagePreferencesModule in Frameworks */,
Expand All @@ -82,7 +85,6 @@
0A809A332B39752200C9A015 = {
isa = PBXGroup;
children = (
0A20744B2B3CD385008516D1 /* ModelPricingModule */,
0A809A7D2B39D65000C9A015 /* CoreExtensions */,
0A809A762B398F1600C9A015 /* CoreModels */,
0A809A592B397B2E00C9A015 /* CoreViewModels */,
Expand All @@ -91,6 +93,8 @@
0A809A5C2B397B8800C9A015 /* ImageGenerationModule */,
0A809A732B398AB200C9A015 /* ImagePreferencesModule */,
0A809A5E2B397BE800C9A015 /* ImageVariationModule */,
0A20744B2B3CD385008516D1 /* ModelPricingModule */,
0A03693B2B4D6F7600D7DAD4 /* SettingsModule */,
0A809A3E2B39752200C9A015 /* Canvas */,
0A809A522B39772800C9A015 /* Frameworks */,
0A809A3D2B39752200C9A015 /* Products */,
Expand Down Expand Up @@ -208,6 +212,7 @@
0A809A832B39EB0100C9A015 /* AppInfo */,
0AE400632B3AD5F800307732 /* Sparkle */,
0A20744C2B3CD3E5008516D1 /* ModelPricingModule */,
0A03693C2B4D6FBB00D7DAD4 /* SettingsModule */,
);
productName = Canvas;
productReference = 0A809A3C2B39752200C9A015 /* Canvas.app */;
Expand All @@ -221,7 +226,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1510;
LastUpgradeCheck = 1510;
LastUpgradeCheck = 1520;
TargetAttributes = {
0A809A3B2B39752200C9A015 = {
CreatedOnToolsVersion = 15.1;
Expand Down Expand Up @@ -316,6 +321,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
Expand Down Expand Up @@ -380,6 +386,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
Expand Down Expand Up @@ -411,6 +418,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Canvas/Preview Content\"";
DEVELOPMENT_TEAM = 84ZM7K56B5;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down Expand Up @@ -441,6 +449,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Canvas/Preview Content\"";
DEVELOPMENT_TEAM = 84ZM7K56B5;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down Expand Up @@ -513,6 +522,10 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
0A03693C2B4D6FBB00D7DAD4 /* SettingsModule */ = {
isa = XCSwiftPackageProductDependency;
productName = SettingsModule;
};
0A20744C2B3CD3E5008516D1 /* ModelPricingModule */ = {
isa = XCSwiftPackageProductDependency;
productName = ModelPricingModule;
Expand Down
13 changes: 12 additions & 1 deletion Canvas/App/CanvasApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@

import AppInfo
import CoreViewModels
import SettingsModule
import Sparkle
import SwiftUI

@main
struct CanvasApp: App {
@State private var appUpdater: AppUpdater
private var updater: SPUUpdater

@State private var settingsManager: SettingsManager

@State private var apiKeyViewModel: APIKeyViewModel
@State private var dalleViewModel: DalleViewModel
@State private var dalleModelInfoViewModel: DalleModelInfoViewModel

init() {
self._settingsManager = State(initialValue: SettingsManager())

self._apiKeyViewModel = State(initialValue: APIKeyViewModel())
self._dalleViewModel = State(initialValue: DalleViewModel())
self._dalleModelInfoViewModel = State(initialValue: DalleModelInfoViewModel())
Expand All @@ -34,6 +39,7 @@ struct CanvasApp: App {
var body: some Scene {
WindowGroup {
AppView()
.environment(settingsManager)
.environment(apiKeyViewModel)
.environment(dalleViewModel)
.environment(dalleModelInfoViewModel)
Expand All @@ -52,5 +58,10 @@ struct CanvasApp: App {
}
}
}

Settings {
SettingsView()
.environment(settingsManager)
}
}
}
2 changes: 0 additions & 2 deletions Canvas/Resources/Canvas.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
Expand Down
19 changes: 19 additions & 0 deletions CoreExtensions/Sources/CoreExtensions/NSImage+Write.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// NSImage.swift
//
//
// Created by Kevin Hermawan on 09/01/24.
//

import Foundation
import SwiftUI

public extension NSImage {
func write(to url: URL) throws -> Void {
guard let tiff = self.tiffRepresentation else { return }
guard let bitmap = NSBitmapImageRep(data: tiff) else { return }
guard let data = bitmap.representation(using: .png, properties: [:]) else { return }

try data.write(to: url)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Kevin Hermawan on 26/12/23.
//

import CoreExtensions
import Nuke
import NukeUI
import SwiftUI
Expand Down Expand Up @@ -44,11 +45,8 @@ struct ImageResultContextMenu: View {

savePanel.begin { response in
guard response == .OK, let url = savePanel.url else { return }
guard let tiffData = nsImage.tiffRepresentation else { return }
guard let bitmapImage = NSBitmapImageRep(data: tiffData) else { return }
guard let pngData = bitmapImage.representation(using: .png, properties: [:]) else { return }

try? pngData.write(to: url)

try? nsImage.write(to: url)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
// Created by Kevin Hermawan on 25/12/23.
//

import CoreExtensions
import SettingsModule
import NukeUI
import SwiftUI

public struct ImageResultListItemView: View {
@Environment(SettingsManager.self) private var settingsManager

private let url: URL

public init(url: URL) {
Expand All @@ -29,6 +33,9 @@ public struct ImageResultListItemView: View {
} else if let image = state.image, let imageContainer = state.imageContainer {
image.resizable()
.aspectRatio(contentMode: .fit)
.onAppear {
autosaveAction(for: imageContainer.image)
}
.contextMenu {
ImageResultContextMenu(
name: url.lastPathComponent,
Expand All @@ -43,4 +50,24 @@ public struct ImageResultListItemView: View {
.background(Color(nsColor: .secondarySystemFill))
.clipShape(.rect(cornerRadius: 8, style: .continuous))
}

private func autosaveAction(for image: NSImage) {
let fileManager = FileManager.default

if settingsManager.autosaveEnabled {
var isDir: ObjCBool = false
var location = settingsManager.autosaveLocation

if fileManager.fileExists(atPath: location.path, isDirectory: &isDir) {
location.append(path: url.lastPathComponent)

try? image.write(to: location)
} else {
try? fileManager.createDirectory(atPath: location.path, withIntermediateDirectories: true, attributes: nil)
location.append(path: url.lastPathComponent)

try? image.write(to: location)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@
// Created by Kevin Hermawan on 26/12/23.
//

import SettingsModule
import SwiftUI
import ViewState

struct PromptFieldFooterText: View {
@Environment(SettingsManager.self) private var settingsManager

private var viewState: ViewState?

init(viewState: ViewState? = nil) {
self.viewState = viewState
}

private var message: String {
if settingsManager.autosaveEnabled {
return "A good prompt is essential for the best possible image generation results."
}

return "Auto-save is not enabled; remember to manually save the results."
}

var body: some View {
Text("A good prompt is essential for the best possible image generation results.")
Text(message)
.when(viewState, is: .loading) {
Text("Generating image...")
}
Expand Down
8 changes: 8 additions & 0 deletions SettingsModule/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
25 changes: 25 additions & 0 deletions SettingsModule/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SettingsModule",
platforms: [.macOS(.v14)],
products: [
.library(
name: "SettingsModule",
targets: ["SettingsModule"]),
],
dependencies: [
.package(url: "https://github.com/sindresorhus/Defaults.git", .upToNextMajor(from: "7.3.1"))
],
targets: [
.target(
name: "SettingsModule",
dependencies: ["Defaults"]),
.testTarget(
name: "SettingsModuleTests",
dependencies: ["SettingsModule"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Defaults+Keys.swift
//
//
// Created by Kevin Hermawan on 09/01/24.
//

import Defaults
import Foundation

let fileManager = FileManager.default
let homeDirectory = fileManager.homeDirectoryForCurrentUser

public extension Defaults.Keys {
static let autosaveEnabled = Key<Bool>("autosaveEnabled", default: true)
static let autosaveLocation = Key<URL>("autosaveLocation", default: homeDirectory.appending(path: "Canvas"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// SettingsManager.swift
//
//
// Created by Kevin Hermawan on 09/01/24.
//

import Defaults
import Foundation

@Observable
public final class SettingsManager {
public var autosaveEnabled: Bool = Defaults[.autosaveEnabled] {
didSet {
Defaults[.autosaveEnabled] = autosaveEnabled
}
}

public var autosaveLocation: URL = Defaults[.autosaveLocation] {
didSet {
Defaults[.autosaveLocation] = autosaveLocation
}
}

public init() {}
}
Loading