├── file.txt
├── SwiftDiffusion
├── Assets.xcassets
│ ├── Contents.json
│ ├── PreviewData
│ │ ├── Contents.json
│ │ └── ImageData
│ │ │ ├── Contents.json
│ │ │ ├── boat-preview.imageset
│ │ │ ├── 16.jpeg
│ │ │ └── Contents.json
│ │ │ ├── jelly-preview.imageset
│ │ │ ├── 3.jpeg
│ │ │ └── Contents.json
│ │ │ ├── boat-thumbnail.imageset
│ │ │ ├── 16.jpeg
│ │ │ └── Contents.json
│ │ │ ├── jelly-thumbnail.imageset
│ │ │ ├── 3.jpeg
│ │ │ └── Contents.json
│ │ │ ├── pastel-preview.imageset
│ │ │ ├── 4.jpeg
│ │ │ └── Contents.json
│ │ │ └── pastel-thumbnail.imageset
│ │ │ ├── 4.jpeg
│ │ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon_16x16.png
│ │ ├── icon_32x32.png
│ │ ├── icon_128x128.png
│ │ ├── icon_16x16@2x.png
│ │ ├── icon_256x256.png
│ │ ├── icon_32x32@2x.png
│ │ ├── icon_512x512.png
│ │ ├── icon_128x128@2x.png
│ │ ├── icon_256x256@2x.png
│ │ ├── icon_512x512@2x.png
│ │ └── Contents.json
│ ├── Logo.imageset
│ │ ├── SwiftDiffusionLogo.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── App
│ ├── KeyCodes.swift
│ ├── Constants.swift
│ ├── AppStructure
│ │ ├── AppDirectory.swift
│ │ ├── App+Setup.swift
│ │ ├── AppDocuments.swift
│ │ └── AppFileStructure.swift
│ └── Debug.swift
├── Views
│ ├── Symbols
│ │ ├── SFSymbol+SwiftUI.swift
│ │ ├── SFSymbol.swift
│ │ └── SymbolButtons.swift
│ ├── WindowViews
│ │ ├── SettingsView
│ │ │ ├── SettingsSections
│ │ │ │ ├── GeneralSection.swift
│ │ │ │ ├── CustomViews
│ │ │ │ │ ├── ToolbarTabButtonStyle.swift
│ │ │ │ │ ├── BrowseFileRow.swift
│ │ │ │ │ ├── SettingsSectionHeader.swift
│ │ │ │ │ └── ToggleWithHeader.swift
│ │ │ │ ├── PromptSection.swift
│ │ │ │ ├── DeveloperSection.swift
│ │ │ │ ├── EngineSection.swift
│ │ │ │ └── FilesSection.swift
│ │ │ ├── SettingsTab.swift
│ │ │ ├── RequiredInputPaths
│ │ │ │ └── RequiredInputPathsPulsatingButton.swift
│ │ │ └── SettingsView.swift
│ │ └── UpdateView
│ │ │ └── UpdateViewState.swift
│ ├── OtherViews
│ │ └── Custom
│ │ │ ├── CustomViews
│ │ │ ├── HalfMaxWidthView.swift
│ │ │ └── VisualEffectBlurView.swift
│ │ │ └── ToolbarViews
│ │ │ └── ToolbarButton.swift
│ ├── MainViews
│ │ ├── Sidebar
│ │ │ ├── SidebarModel
│ │ │ │ ├── SidebarModel+Queue.swift
│ │ │ │ ├── SidebarModel+Copy.swift
│ │ │ │ ├── SidebarModel+Clean.swift
│ │ │ │ ├── SidebarModel+Create.swift
│ │ │ │ ├── SidebarModel+Move.swift
│ │ │ │ └── SidebarModel+Save.swift
│ │ │ ├── Components
│ │ │ │ ├── VisualEffectView.swift
│ │ │ │ ├── FilterSortingSection.swift
│ │ │ │ └── DisplayOptionsBar
│ │ │ │ │ ├── DisplayOptionsBar.swift
│ │ │ │ │ └── HoverToggleButton.swift
│ │ │ ├── PreviewImageProcessing
│ │ │ │ ├── ImageCache.swift
│ │ │ │ ├── CachedThumbnailImageView.swift
│ │ │ │ ├── CachedPreviewImageView.swift
│ │ │ │ └── NSImage+Extensions.swift
│ │ │ ├── SidebarFolderView
│ │ │ │ ├── ParentFolderListItem.swift
│ │ │ │ ├── DropHandlerModifier.swift
│ │ │ │ ├── SidebarFolderItem.swift
│ │ │ │ ├── SidebarItemView.swift
│ │ │ │ ├── SidebarStoredItemView.swift
│ │ │ │ └── FolderTitleControl.swift
│ │ │ ├── SidebarExtensions
│ │ │ │ ├── Sidebar+Preload.swift
│ │ │ │ └── Sidebar+Move.swift
│ │ │ └── WorkspaceFolderView
│ │ │ │ └── WorkspaceFolderView.swift
│ │ ├── ContentView
│ │ │ ├── ContentToolbar
│ │ │ │ ├── WindowHeader.swift
│ │ │ │ ├── DeveloperItems
│ │ │ │ │ ├── SegmentedViewPicker.swift
│ │ │ │ │ └── DeveloperToolbarItems.swift
│ │ │ │ └── ToolbarProgressView.swift
│ │ │ └── CustomViews
│ │ │ │ ├── BlueButton.swift
│ │ │ │ └── ContentProgressBar.swift
│ │ ├── DetailView
│ │ │ ├── FullscreenImage
│ │ │ │ ├── FullscreenImageView.swift
│ │ │ │ └── ImageWindowManager.swift
│ │ │ ├── FileHierarchy
│ │ │ │ ├── FileNode.swift
│ │ │ │ ├── FileHierarchy+Control.swift
│ │ │ │ └── FileHierarchy.swift
│ │ │ ├── Views
│ │ │ │ ├── DetailToolbarSymbolButton.swift
│ │ │ │ ├── DetailImageView.swift
│ │ │ │ ├── Dividers.swift
│ │ │ │ └── ShareButton.swift
│ │ │ ├── Extensions
│ │ │ │ └── NSImageExtensions.swift
│ │ │ ├── FileRow
│ │ │ │ ├── FileRowView.swift
│ │ │ │ └── ThumbnailLoader.swift
│ │ │ └── FileOutlineView.swift
│ │ └── PromptView
│ │ │ ├── PromptBars
│ │ │ ├── PromptBarButton.swift
│ │ │ └── PasteGenerationDataStatusBar.swift
│ │ │ ├── PromptMenus
│ │ │ ├── Components
│ │ │ │ └── ExpandableSectionHeader.swift
│ │ │ ├── SamplingMethodMenu.swift
│ │ │ ├── VaeModelMenu.swift
│ │ │ └── CheckpointMenu.swift
│ │ │ ├── DebugPromptViews
│ │ │ ├── DebugPromptStatusView.swift
│ │ │ └── DebugPromptActionView.swift
│ │ │ ├── PromptRows
│ │ │ └── PromptRows.swift
│ │ │ └── PromptView.swift
│ └── CommonPreviews
│ │ └── CommonPreviews.swift
├── SwiftDiffusion.entitlements
├── Utilities
│ ├── CopyPasteUtility.swift
│ ├── Delay.swift
│ ├── SoundUtility.swift
│ ├── NotificationUtility.swift
│ ├── FileUtility.swift
│ ├── URLParserUtility.swift
│ ├── AppRelauncherUtility.swift
│ └── ImageSaver
│ │ └── ImageSaver+Composite.swift
├── Models
│ ├── StoredModels
│ │ ├── MapModelData.swift
│ │ ├── StoredVaeModel.swift
│ │ ├── StoredCheckpointApiModel.swift
│ │ └── StoredCheckpointModel.swift
│ ├── SidebarModels
│ │ ├── ImageInfo.swift
│ │ ├── SidebarItem.swift
│ │ └── SidebarFolder.swift
│ └── AutomaticModels
│ │ ├── Custom
│ │ └── Checkpoints
│ │ │ └── Models
│ │ │ ├── CheckpointApiModel.swift
│ │ │ ├── CheckpointModel.swift
│ │ │ └── CheckpointModelPreferences.swift
│ │ └── Generic
│ │ ├── Models
│ │ ├── LoraModel.swift
│ │ └── VaeModel.swift
│ │ └── ModelManager.swift
├── Info.plist
├── ScriptManager
│ ├── ScriptManager
│ │ ├── ScriptSetupHelper.swift
│ │ ├── Extensions
│ │ │ └── ScriptManager+ServiceAvailability.swift
│ │ ├── StateDebugInfo.swift
│ │ ├── GenerationStatus.swift
│ │ └── ScriptState.swift
│ └── PythonProcess
│ │ ├── ScriptManager+TerminateAll.swift
│ │ ├── ScriptManager+PythonProcess.swift
│ │ └── PythonProcess.swift
├── SwiftDiffusionAppDelegate.swift
├── Observers
│ ├── DirectoryObserver.swift
│ └── ScriptManagerObserver.swift
├── Services
│ ├── PastableService
│ │ ├── PasteGenerationDataButton.swift
│ │ └── PastableService.swift
│ ├── AutomaticServices
│ │ ├── Txt2ImgService.swift
│ │ └── AutomaticApiService.swift
│ └── FilePickerService.swift
└── SwiftDiffusionApp.swift
├── SwiftDiffusion.xcodeproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
└── .gitignore
/file.txt:
--------------------------------------------------------------------------------
1 | Update from 2023-10-12 08:32:39
2 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/Logo.imageset/SwiftDiffusionLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/Logo.imageset/SwiftDiffusionLogo.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/boat-preview.imageset/16.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/boat-preview.imageset/16.jpeg
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/jelly-preview.imageset/3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/jelly-preview.imageset/3.jpeg
--------------------------------------------------------------------------------
/SwiftDiffusion.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/boat-thumbnail.imageset/16.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/boat-thumbnail.imageset/16.jpeg
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/jelly-thumbnail.imageset/3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/jelly-thumbnail.imageset/3.jpeg
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/pastel-preview.imageset/4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/pastel-preview.imageset/4.jpeg
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/pastel-thumbnail.imageset/4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/superstar3222/SwiftDiffusion/HEAD/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/pastel-thumbnail.imageset/4.jpeg
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/Logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "SwiftDiffusionLogo.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/boat-preview.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "16.jpeg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/jelly-preview.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "3.jpeg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/pastel-preview.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "4.jpeg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/boat-thumbnail.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "16.jpeg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/jelly-thumbnail.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "3.jpeg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/PreviewData/ImageData/pastel-thumbnail.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "4.jpeg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftDiffusion/App/KeyCodes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyCodes.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/26/24.
6 | //
7 |
8 | import Foundation
9 |
10 | enum KeyCodes {
11 | case deleteKey
12 |
13 | var code: UInt16 {
14 | switch self {
15 | case .deleteKey: return 51
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/SwiftDiffusion.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/Symbols/SFSymbol+SwiftUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SFSymbol+SwiftUI.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension SFSymbol {
11 | var name: String {
12 | return self.rawValue
13 | }
14 |
15 | var image: some View {
16 | Image(systemName: self.name)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/SwiftDiffusion/SwiftDiffusion.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/SwiftDiffusion.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "compactslider",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/buh/CompactSlider.git",
7 | "state" : {
8 | "revision" : "6d591a76caecd583ad69fbcd06f2fb83135318c0",
9 | "version" : "1.1.5"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/CopyPasteUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CopyPasteUtility.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import Foundation
9 | import AppKit
10 |
11 | struct CopyPasteUtility {
12 | static func copyToClipboard(_ string: String) {
13 | let pasteboard = NSPasteboard.general
14 | pasteboard.clearContents()
15 | pasteboard.setString(string, forType: .string)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/GeneralSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeneralSection.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct GeneralSection: View {
11 | @ObservedObject var userSettings: UserSettings
12 |
13 | var body: some View {
14 | Text("Coming soon")
15 | }
16 | }
17 |
18 | #Preview {
19 | SettingsView(selectedTab: .general)
20 | }
21 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/StoredModels/MapModelData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapModelData.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/13/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct MapModelData {
11 | @MainActor
12 | func toStored(promptModel: PromptModel) -> StoredPromptModel? {
13 | return toStoredPromptModel(from: promptModel)
14 | }
15 |
16 | @MainActor
17 | func fromStored(storedPromptModel: StoredPromptModel) -> PromptModel {
18 | return toPromptModel(from: storedPromptModel)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/SidebarModels/ImageInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageInfo.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/27/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | @Model
12 | class ImageInfo: Identifiable {
13 | @Attribute var id: UUID = UUID()
14 | @Attribute var url: URL
15 | @Attribute var width: CGFloat
16 | @Attribute var height: CGFloat
17 |
18 | init(url: URL, width: CGFloat, height: CGFloat) {
19 | self.url = url
20 | self.width = width
21 | self.height = height
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/OtherViews/Custom/CustomViews/HalfMaxWidthView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HalfMaxWidthView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HalfMaxWidthView: View {
11 | let content: Content
12 |
13 | init(@ViewBuilder content: () -> Content) {
14 | self.content = content()
15 | }
16 |
17 | var body: some View {
18 | GeometryReader { geometry in
19 | content
20 | .frame(width: geometry.size.width / 2)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/Delay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Delay.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Delay {
11 | /// Perform an action after a set amount of seconds.
12 | static func by(_ seconds: Double, closure: @escaping () -> Void) {
13 | Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { _ in
14 | closure()
15 | }
16 | }
17 |
18 | static func repeatEvery(_ seconds: Double, closure: @escaping () -> Void) {
19 | Timer.scheduledTimer(withTimeInterval: seconds, repeats: true) { _ in
20 | closure()
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/SwiftDiffusion/App/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Constants {
11 |
12 | struct Api {}
13 | struct App {}
14 | struct CommandLine {}
15 | struct Debug {}
16 | struct Delays {}
17 | struct FileStructure {}
18 | struct FileTypes {}
19 | struct Keys {}
20 | struct Layout {}
21 | struct Parsing {}
22 | struct PromptOptions {}
23 | struct Sidebar {}
24 | struct WindowSize {}
25 |
26 | }
27 |
28 | extension Constants.App {
29 | static let name = "SwiftDiffusion"
30 | }
31 |
32 | extension Constants.CommandLine {
33 | static let zshPath = "/bin/zsh"
34 | static let zshUrl = URL(fileURLWithPath: zshPath)
35 | }
36 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/CustomViews/ToolbarTabButtonStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToolbarTabButtonStyle.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToolbarTabButtonStyle: ButtonStyle {
11 | var isSelected: Bool
12 |
13 | func makeBody(configuration: Configuration) -> some View {
14 | configuration.label
15 | .foregroundColor(isSelected ? Color.accentColor : Color.primary)
16 | .padding(4)
17 | .frame(minWidth: 56)
18 | .background(self.isSelected ? Color.gray.opacity(0.2) : Color.clear)
19 | .clipShape(RoundedRectangle(cornerRadius: 8))
20 | }
21 | }
22 |
23 | #Preview {
24 | SettingsView(selectedTab: .general)
25 | }
26 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarModel/SidebarModel+Queue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarModel+Queue.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SidebarModel {
11 | func addToStorableSidebarItems(sidebarItem: SidebarItem, withImageUrls imageUrls: [URL]) {
12 | sidebarItem.imageUrls = imageUrls
13 | storableSidebarItems.append(sidebarItem)
14 | }
15 |
16 | func generatingSidebarItemFinished(withImageUrls imageUrls: [URL]) {
17 | guard let sidebarItem = currentlyGeneratingSidebarItem else {
18 | return
19 | }
20 |
21 | addToStorableSidebarItems(sidebarItem: sidebarItem, withImageUrls: imageUrls)
22 | currentlyGeneratingSidebarItem = nil
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/Components/VisualEffectView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VisualEffectView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/16/24.
6 | //
7 |
8 | import SwiftUI
9 | import AppKit
10 |
11 | struct VisualEffectView: NSViewRepresentable {
12 | var material: NSVisualEffectView.Material
13 | var blendingMode: NSVisualEffectView.BlendingMode
14 |
15 | func makeNSView(context: Context) -> NSVisualEffectView {
16 | let view = NSVisualEffectView()
17 | view.material = material
18 | view.blendingMode = blendingMode
19 | view.state = .active
20 | return view
21 | }
22 |
23 | func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
24 | nsView.material = material
25 | nsView.blendingMode = blendingMode
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/OtherViews/Custom/CustomViews/VisualEffectBlurView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VisualEffectBlurView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import SwiftUI
9 | import AppKit
10 |
11 | struct VisualEffectBlurView: NSViewRepresentable {
12 | var material: NSVisualEffectView.Material
13 | var blendingMode: NSVisualEffectView.BlendingMode
14 |
15 | func makeNSView(context: Context) -> NSVisualEffectView {
16 | let view = NSVisualEffectView()
17 | view.material = material
18 | view.blendingMode = blendingMode
19 | view.state = .active
20 | return view
21 | }
22 |
23 | func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
24 | nsView.material = material
25 | nsView.blendingMode = blendingMode
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSExceptionDomains
8 |
9 | localhost
10 |
11 | NSExceptionAllowsInsecureHTTPLoads
12 |
13 | NSIncludesSubdomains
14 |
15 |
16 | 127.0.0.1
17 |
18 | NSExceptionAllowsInsecureHTTPLoads
19 |
20 | NSIncludesSubdomains
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/ContentView/ContentToolbar/WindowHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WindowHeader.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct WindowToolbarTitle: View {
11 | let text: String
12 |
13 | var body: some View {
14 | Text(text)
15 | .font(.system(size: 15, weight: .semibold, design: .default))
16 | }
17 | }
18 |
19 | struct ContentViewToolbarTitle: View {
20 | let text: String
21 | @ObservedObject private var pastableService = PastableService.shared
22 | @ObservedObject private var userSettings = UserSettings.shared
23 |
24 | var body: some View {
25 | if pastableService.canPasteData == false && userSettings.showDeveloperInterface == false {
26 | WindowToolbarTitle(text: text)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FullscreenImage/FullscreenImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FullscreenImageView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FullscreenImageView: View {
11 | var image: NSImage
12 | var onClose: () -> Void
13 |
14 | var body: some View {
15 | ZStack(alignment: .topLeading) {
16 | Image(nsImage: image)
17 | .resizable()
18 | .aspectRatio(contentMode: .fit)
19 | .edgesIgnoringSafeArea(.all)
20 | Button(action: onClose) {
21 | SFSymbol.closeFullscreen.image
22 | .font(.largeTitle)
23 | .padding()
24 | }
25 | .buttonStyle(BorderlessButtonStyle())
26 | .shadow(color: .black, radius: 10, x: 0, y: 4)
27 | .padding()
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/AutomaticModels/Custom/Checkpoints/Models/CheckpointApiModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckpointApiModel.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/21/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CheckpointApiModel: Decodable {
11 | let title: String
12 | let modelName: String
13 | let modelHash: String?
14 | let sha256: String?
15 | let filename: String
16 | let config: String?
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case title
20 | case modelName = "model_name"
21 | case modelHash = "hash"
22 | case sha256
23 | case filename
24 | case config
25 | }
26 | }
27 |
28 | struct ClientConfig: Encodable, Decodable {
29 | let sdModelCheckpoint: String
30 | enum CodingKeys: String, CodingKey {
31 | case sdModelCheckpoint = "sd_model_checkpoint"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/PreviewImageProcessing/ImageCache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageCache.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/26/24.
6 | //
7 |
8 | import AppKit
9 |
10 | /// A singleton class responsible for caching `NSImage` objects to improve performance and reduce network usage.
11 | /// This class uses `NSCache` to store images keyed by their URL path, providing a simple API for accessing and storing images.
12 | class ImageCache {
13 | static let shared = ImageCache()
14 | private var cache = NSCache()
15 |
16 | private init() {}
17 |
18 | func image(forKey key: String) -> NSImage? {
19 | return cache.object(forKey: key as NSString)
20 | }
21 |
22 | func setImage(_ image: NSImage, forKey key: String) {
23 | cache.setObject(image, forKey: key as NSString)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/AutomaticModels/Generic/Models/LoraModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoraModel.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | extension Constants.API.Endpoint {
12 | struct Loras {
13 | static let get = "/sdapi/v1/loras"
14 | static let postRefresh = "/sdapi/v1/refresh-loras"
15 | }
16 | }
17 |
18 | struct LoraModel: Identifiable, Decodable {
19 | var id = UUID()
20 | let name: String
21 | let alias: String
22 | let path: String
23 |
24 | enum CodingKeys: String, CodingKey {
25 | case name, alias, path
26 | }
27 | }
28 |
29 | extension LoraModel: EndpointRepresentable {
30 | static var fetchEndpoint: String? {
31 | Constants.API.Endpoint.Loras.get
32 | }
33 |
34 | static var refreshEndpoint: String? {
35 | Constants.API.Endpoint.Loras.postRefresh
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/SwiftDiffusion/ScriptManager/ScriptManager/ScriptSetupHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScriptSetupHelper.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Helps with setting up script paths and directories.
11 | struct ScriptSetupHelper {
12 | /// Calculates and returns script path components.
13 | /// - Parameter scriptPath: The full path to the script.
14 | /// - Returns: A tuple containing the script path, directory, and name, or nil if the path is empty.
15 | static func setupScriptPath(_ scriptPath: String?) -> (String, String)? {
16 | guard let scriptPath = scriptPath, !scriptPath.isEmpty else { return nil }
17 | let scriptDirectory = URL(fileURLWithPath: scriptPath).deletingLastPathComponent().path
18 | let scriptName = URL(fileURLWithPath: scriptPath).lastPathComponent
19 | return (scriptDirectory, scriptName)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarModel/SidebarModel+Copy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarModel+Copy.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SidebarModel {
11 | func copySelectedSidebarItemToWorkspace() {
12 | guard let sidebarItem = selectedSidebarItem else { return }
13 | copyItemToWorkspace(sidebarItem)
14 | }
15 |
16 | private func copyItemToWorkspace(_ sidebarItem: SidebarItem) {
17 | if let clonedPrompt = sidebarItem.prompt {
18 | let clonedTitle = String(sidebarItem.title.prefix(Constants.Sidebar.titleLength))
19 | let clonedItem = SidebarItem(title: clonedTitle, imageUrls: [])
20 | clonedItem.prompt = clonedPrompt
21 | workspaceFolder.add(item: clonedItem)
22 | saveData(in: modelContext)
23 | cleanUpEmptyWorkspaceItems()
24 | setSelectedSidebarItem(to: clonedItem)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Packages #
2 | ############
3 | # it's better to unpack these files and commit the raw source
4 | # git has its own built in compression methods
5 | *.7z
6 | *.dmg
7 | *.gz
8 | *.iso
9 | *.jar
10 | *.rar
11 | *.tar
12 | *.zip
13 |
14 | # Logs and databases #
15 | ######################
16 | *.log
17 | *.sql
18 | *.sqlite
19 |
20 | # OS generated files #
21 | ######################
22 | .DS_Store
23 | .DS_Store?
24 | ._*
25 | .AppleDouble
26 | .LSOverride
27 | .Spotlight-V100
28 | .Trashes
29 | Icon
30 | Thumbs.db
31 | ehthumbs.db
32 |
33 | # Xcode
34 | #
35 | build/
36 | *.pbxuser
37 | !default.pbxuser
38 | *.mode1v3
39 | !default.mode1v3
40 | *.mode2v3
41 | !default.mode2v3
42 | *.perspectivev3
43 | !default.perspectivev3
44 | xcuserdata
45 | *.xccheckout
46 | *.moved-aside
47 | DerivedData
48 | *.hmap
49 | *.ipa
50 | *.xcuserstate
51 | .idea/
52 |
53 | # CocoaPods
54 | Pods
55 |
56 | # Carthage
57 | Carthage
58 |
59 | # SPM
60 | .build/
61 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarModel/SidebarModel+Clean.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarModel+Clean.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SidebarModel {
11 | func cleanUpEmptyWorkspaceItems() {
12 | let emptyWorkspaceItems = workspaceFolder.items.filter { $0.prompt?.isEmptyPrompt ?? false }
13 | for item in emptyWorkspaceItems {
14 | deleteWorkspaceItem(item)
15 | }
16 | }
17 | }
18 |
19 | extension StoredPromptModel {
20 | var isEmptyPrompt: Bool {
21 | return selectedModel == nil &&
22 | samplingMethod == nil &&
23 | positivePrompt.isEmpty &&
24 | negativePrompt.isEmpty &&
25 | width == 512 &&
26 | height == 512 &&
27 | cfgScale == 7 &&
28 | samplingSteps == 20 &&
29 | seed == "-1" &&
30 | batchCount == 1 &&
31 | batchSize == 1 &&
32 | clipSkip == 1 &&
33 | vaeModel == nil
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/AutomaticModels/Custom/Checkpoints/Models/CheckpointModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckpointModel.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/18/24.
6 | //
7 |
8 | import Foundation
9 |
10 | enum CheckpointModelType {
11 | case coreMl
12 | case python
13 | }
14 |
15 | class CheckpointModel: ObservableObject, Identifiable {
16 | let id = UUID()
17 | let name: String
18 | let path: String
19 | let type: CheckpointModelType
20 | var checkpointApiModel: CheckpointApiModel?
21 |
22 | init(name: String, path: String, type: CheckpointModelType, checkpointApiModel: CheckpointApiModel? = nil) {
23 | self.name = name
24 | self.path = path
25 | self.type = type
26 | self.checkpointApiModel = checkpointApiModel
27 | }
28 | }
29 |
30 | extension CheckpointModel: Equatable {
31 | static func == (lhs: CheckpointModel, rhs: CheckpointModel) -> Bool {
32 | return lhs.id == rhs.id
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FileHierarchy/FileNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileNode.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct FileNode: Identifiable {
11 | let id: UUID = UUID()
12 | let name: String
13 | let fullPath: String
14 | var children: [FileNode]? = nil
15 | let lastModified: Date
16 |
17 | var isLeaf: Bool {
18 | return children == nil
19 | }
20 | }
21 |
22 |
23 | extension FileNode: Equatable {
24 | static func == (lhs: FileNode, rhs: FileNode) -> Bool {
25 | return lhs.id == rhs.id
26 | }
27 | }
28 |
29 | extension FileNode {
30 | var isImage: Bool {
31 | let imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff"]
32 | return imageExtensions.contains((fullPath as NSString).pathExtension.lowercased())
33 | }
34 |
35 | var iconName: String {
36 | isLeaf ? (isImage ? "" : "doc") : "folder"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/Views/DetailToolbarSymbolButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailToolbarSymbolButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Constants.Layout {
11 | struct Toolbar {
12 | static let itemHeight: CGFloat = 30
13 | static let itemWidth: CGFloat = 30
14 | }
15 | }
16 |
17 | struct DetailToolbarSymbolButton: View {
18 | var hint: String = ""
19 | let symbol: SFSymbol
20 | let action: () -> Void
21 |
22 | private let itemHeight: CGFloat = 30
23 | private let itemWidth: CGFloat = 30
24 |
25 | var body: some View {
26 | Button(action: action) {
27 | symbol.image
28 | }
29 | .buttonStyle(BorderlessButtonStyle())
30 | .frame(width: Constants.Layout.Toolbar.itemWidth, height: Constants.Layout.Toolbar.itemHeight)
31 | .help(hint)
32 | }
33 | }
34 |
35 | #Preview {
36 | CommonPreviews.detailView
37 | }
38 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/ContentView/ContentToolbar/DeveloperItems/SegmentedViewPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SegmentedViewPicker.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum ViewManager {
11 | case prompt, console, split
12 |
13 | var title: String {
14 | switch self {
15 | case .prompt: return "Prompt"
16 | case .console: return "Console"
17 | case .split: return "Split"
18 | }
19 | }
20 | }
21 |
22 | extension ViewManager: Hashable, Identifiable {
23 | var id: Self { self }
24 | }
25 |
26 | struct SegmentedViewPicker: View {
27 | @Binding var selectedView: ViewManager
28 |
29 | var body: some View {
30 | Picker("Options", selection: $selectedView) {
31 | Text("Prompt").tag(ViewManager.prompt)
32 | Text("Console").tag(ViewManager.console)
33 | Text("Split").tag(ViewManager.split)
34 | }
35 | .pickerStyle(SegmentedPickerStyle())
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptBars/PromptBarButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PromptBarButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum AlignSymbol {
11 | case leading, trailing
12 | }
13 |
14 | struct PromptBarButton: View {
15 | let title: String
16 | let symbol: SFSymbol
17 | var align: AlignSymbol = .leading
18 | let action: () -> Void
19 |
20 | var body: some View {
21 | Button(action: action) {
22 | if align == .leading { symbol.image }
23 | Text(title)
24 | if align == .trailing { symbol.image }
25 | }
26 | .buttonStyle(.accessoryBar)
27 | }
28 | }
29 |
30 | #Preview {
31 | return HStack {
32 | PromptBarButton(title: "Close", symbol: .close, align: .leading, action: {
33 |
34 | })
35 |
36 | Spacer()
37 |
38 | PromptBarButton(title: "Save", symbol: .save, align: .trailing, action: {
39 |
40 | })
41 | }
42 | .padding()
43 | .frame(width: 400)
44 | }
45 |
--------------------------------------------------------------------------------
/SwiftDiffusion/ScriptManager/ScriptManager/Extensions/ScriptManager+ServiceAvailability.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScriptManager+ServiceAvailability.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension ScriptManager {
11 |
12 | func checkScriptServiceAvailability(completion: @escaping (Bool) -> Void) {
13 | guard let url = serviceUrl else {
14 | Debug.log("Service URL not available.")
15 | completion(false)
16 | return
17 | }
18 |
19 | let task = URLSession.shared.dataTask(with: url) { _, response, error in
20 | DispatchQueue.main.async {
21 | if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
22 | // page loaded successfully, script is likely still running
23 | completion(true)
24 | } else {
25 | // request failed, script is likely terminated
26 | completion(false)
27 | }
28 | }
29 | }
30 |
31 | task.resume()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/SwiftDiffusion/SwiftDiffusionAppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftDiffusionAppDelegate.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import Cocoa
9 |
10 | class AppDelegate: NSObject, NSApplicationDelegate {
11 |
12 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
13 | ScriptManager.shared.terminateImmediately()
14 |
15 | return .terminateNow
16 | }
17 |
18 |
19 | func applicationDidFinishLaunching(_ notification: Notification) {
20 | Debug.log("applicationDidFinishLaunching")
21 | // TODO: Python PID tracker
22 |
23 | // NSUserDefaults Python PID
24 |
25 | // on launch, killall programs with PID saved to Defaults
26 | // for PIDs that were killed, remove from Defaults list
27 |
28 | // check all running programs for name with "Python" (except those in Defaults)
29 | // disinclude from the tracking list henceforth
30 |
31 | // on terminate, kill all tracked PIDs
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/Views/DetailImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailImageView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 | import AppKit
10 |
11 | struct DetailImageView: View {
12 | @Binding var image: NSImage?
13 |
14 | var body: some View {
15 | VStack {
16 | if let image = image {
17 | Image(nsImage: image)
18 | .resizable()
19 | .aspectRatio(contentMode: .fit)
20 | .frame(maxWidth: .infinity, maxHeight: .infinity)
21 | .shadow(color: .black.opacity(0.5), radius: 5, x: 0, y: 2)
22 | } else {
23 | Spacer()
24 | HStack {
25 | Spacer()
26 | SFSymbol.photo.image
27 | .font(.system(size: 30, weight: .light))
28 | .foregroundColor(Color.secondary)
29 | .opacity(0.5)
30 | Spacer()
31 | }
32 | Spacer()
33 | }
34 | }
35 | }
36 | }
37 |
38 | #Preview {
39 | CommonPreviews.detailView
40 | }
41 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/AutomaticModels/Generic/Models/VaeModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VaeModel.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/25/24.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | extension Constants.API.Endpoint {
12 | struct Vae {
13 | static let get = "/sdapi/v1/sd-vae"
14 | static let refresh = "/sdapi/v1/refresh-vae"
15 | }
16 | }
17 |
18 | struct VaeModel: Identifiable, Decodable {
19 | var id = UUID()
20 | let name: String
21 | let path: String
22 |
23 | enum CodingKeys: String, CodingKey {
24 | case name = "model_name"
25 | case path = "filename"
26 | }
27 | }
28 |
29 | extension VaeModel: EndpointRepresentable {
30 | static var fetchEndpoint: String? {
31 | Constants.API.Endpoint.Vae.get
32 | }
33 |
34 | static var refreshEndpoint: String? {
35 | Constants.API.Endpoint.Vae.refresh
36 | }
37 | }
38 |
39 | extension VaeModel: Equatable {
40 | static func == (lhs: VaeModel, rhs: VaeModel) -> Bool {
41 | return lhs.id == rhs.id
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/StoredModels/StoredVaeModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoredVaeModel.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/27/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | @Model
12 | class StoredVaeModel {
13 | @Attribute var name: String
14 | @Attribute var path: String
15 |
16 | init(name: String, path: String) {
17 | self.name = name
18 | self.path = path
19 | }
20 | }
21 |
22 | extension MapModelData {
23 |
24 | @MainActor
25 | func toStoredVaeModel(from vaeModel: VaeModel?) -> StoredVaeModel? {
26 | guard let vaeModel = vaeModel else { return nil }
27 | return StoredVaeModel(name: vaeModel.name,
28 | path: vaeModel.path
29 | )
30 | }
31 |
32 | @MainActor
33 | func toVaeModel(from storedVaeModel: StoredVaeModel?) -> VaeModel? {
34 | guard let storedVaeModel = storedVaeModel else { return nil }
35 |
36 | return VaeModel(name: storedVaeModel.name,
37 | path: storedVaeModel.path
38 | )
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/SwiftDiffusion/App/AppStructure/AppDirectory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDirectory.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/8/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Constants.FileStructure {
11 | // Can custom set this variable if user wants custom application directory path
12 | static let AppSupportUrl: URL? = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
13 | static let AppSupportFolderName = "SwiftDiffusion"
14 | static let AppSwiftDataFileName = "LocalUserDatabase.store" // "StoredUserData.store"
15 | }
16 |
17 | /// App directories and their associated URLs
18 | enum AppDirectory: String, CaseIterable {
19 | // UserFiles: models, loras, embeddings, etc.
20 | case userFiles = "UserFiles"
21 | case models = "UserFiles/Models"
22 | case coreMl = "UserFiles/Models/CoreML"
23 | case python = "UserFiles/Models/Python"
24 | // UserData: local database, saved prompt media, etc.
25 | case userData = "UserData"
26 | case promptMedia = "UserData/PromptMedia"
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/Extensions/NSImageExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSImageExtensions.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension NSImage {
11 | /// Resizes the NSImage while maintaining its original aspect ratio to match the target height.
12 | ///
13 | /// - Parameters:
14 | /// - targetHeight: The desired height for the resized image.
15 | /// - Returns: A new NSImage instance that has been resized while maintaining the original aspect ratio.
16 | func resizedToMaintainAspectRatio(targetHeight: CGFloat) -> NSImage {
17 | let imageSize = self.size
18 | let heightRatio = targetHeight / imageSize.height
19 | let newSize = NSSize(width: imageSize.width * heightRatio, height: targetHeight)
20 |
21 | let img = NSImage(size: newSize)
22 | img.lockFocus()
23 | self.draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: NSRect.zero, operation: .copy, fraction: 1.0)
24 | img.unlockFocus()
25 | return img
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/OtherViews/Custom/ToolbarViews/ToolbarButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToolbarButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToolbarButton: View {
11 | let text: String?
12 | let symbol: String?
13 | @Binding var isDisabled: Bool
14 | let action: () -> Void
15 |
16 | init(text: String? = nil, symbol: String? = nil, action: @escaping () -> Void) {
17 | self.text = text
18 | self.symbol = symbol
19 | self.action = action
20 | self._isDisabled = .constant(false)
21 | }
22 |
23 | var body: some View {
24 | Button(action: action) {
25 | Group {
26 | if let symbol = symbol {
27 | Image(systemName: symbol)
28 | } else if let text = text {
29 | Text(text)
30 | } else {
31 | EmptyView()
32 | }
33 | }
34 | }
35 | .disabled(isDisabled)
36 | }
37 | }
38 |
39 | #Preview {
40 | ToolbarButton(symbol: "arkit") {
41 | Debug.log("Hello")
42 | }
43 | .frame(width: 300, height: 40)
44 | }
45 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptMenus/Components/ExpandableSectionHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExpandableSectionHeader.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/24/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ExpandableSectionHeader: View {
11 | let title: String
12 | @Binding var isExpanded: Bool
13 |
14 | var body: some View {
15 | Button(action: {
16 | if isExpanded {
17 | withAnimation(.easeOut(duration: 0.2)) {
18 | self.isExpanded.toggle()
19 | }
20 | } else {
21 | withAnimation(.default) {
22 | self.isExpanded.toggle()
23 | }
24 | }
25 | }) {
26 | HStack(spacing: 0) {
27 | Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
28 | .font(.system(size: 10, weight: .heavy))
29 | .frame(minWidth: 12)
30 | .foregroundStyle(.secondary)
31 | PromptRowHeading(title: title)
32 | Spacer()
33 | }
34 | }
35 | .buttonStyle(.plain)
36 | }
37 | }
38 |
39 | #Preview {
40 | CommonPreviews.promptView
41 | }
42 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarModel/SidebarModel+Create.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarModel+Create.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/4/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SidebarModel {
11 | func createNewWorkspaceItem() {
12 | let newWorkspaceItem = SidebarItem(title: "", imageUrls: [])
13 | newWorkspaceItem.prompt = StoredPromptModel()
14 | create(sidebarItem: newWorkspaceItem, in: workspaceFolder)
15 | }
16 |
17 | func createNewWorkspaceItem(withPrompt prompt: StoredPromptModel) {
18 | let title = prompt.positivePrompt.truncatingToLength(Constants.Sidebar.titleLength)
19 | let newWorkspaceItem = SidebarItem(title: title, imageUrls: [])
20 | newWorkspaceItem.prompt = prompt
21 | cleanUpEmptyWorkspaceItems()
22 | create(sidebarItem: newWorkspaceItem, in: workspaceFolder)
23 | }
24 |
25 | private func create(sidebarItem: SidebarItem, in folder: SidebarFolder) {
26 | folder.add(item: sidebarItem)
27 | saveData(in: modelContext)
28 | setSelectedSidebarItem(to: sidebarItem)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SwiftDiffusion/App/AppStructure/App+Setup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // App+Setup.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SwiftDiffusionApp {
11 | /// Initialize app file-folder structure setup with error handling.
12 | func setupAppFileStructure() {
13 | AppFileStructure.setup { error, failedUrl in
14 | if let error = error, let failedUrl = failedUrl {
15 | Debug.log("Failed to create directory at \(failedUrl): \(error)")
16 | } else if let error = error {
17 | Debug.log("Error: \(error)")
18 | } else {
19 | Debug.log("Successfully initialized application file structure.")
20 | }
21 | }
22 | AppFileStructure.setupDocuments { error, failedUrl in
23 | if let error = error, let failedUrl = failedUrl {
24 | Debug.log("Failed to create directory at \(failedUrl): \(error)")
25 | } else if let error = error {
26 | Debug.log("Error: \(error)")
27 | } else {
28 | Debug.log("Successfully initialized application documents structure.")
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptMenus/SamplingMethodMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SamplingMethodMenu.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SamplingMethodMenu: View {
11 | @EnvironmentObject var currentPrompt: PromptModel
12 |
13 | var body: some View {
14 | VStack(alignment: .leading, spacing: 0) {
15 | PromptRowHeading(title: "Sampling")
16 | .padding(.bottom, 6)
17 | Menu {
18 | let samplingMethods = currentPrompt.selectedModel?.type == .coreMl
19 | ? Constants.coreMLSamplingMethods
20 | : Constants.pythonSamplingMethods
21 | ForEach(samplingMethods, id: \.self) { method in
22 | Button(method) {
23 | currentPrompt.samplingMethod = method
24 | Debug.log("Selected Sampling Method: \(method)")
25 | }
26 | }
27 | } label: {
28 | Label(currentPrompt.samplingMethod ?? "Choose Sampler", systemImage: "square.stack.3d.forward.dottedline")
29 | }
30 | }
31 | }
32 | }
33 |
34 | #Preview {
35 | CommonPreviews.promptView
36 | }
37 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/PromptSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PromptSection.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PromptSection: View {
11 | @ObservedObject var userSettings: UserSettings
12 |
13 | var body: some View {
14 | VStack(alignment: .leading) {
15 | ToggleWithHeader(isToggled: $userSettings.disablePasteboardParsingForGenerationData, header: "Disable automatic generation data parsing", description: "When you copy generation data from sites like Civit.ai, this will automatically format it and show a button to paste it.", showAllDescriptions: userSettings.alwaysShowSettingsHelp)
16 |
17 | ToggleWithHeader(isToggled: $userSettings.alwaysShowPasteboardGenerationDataButton, header: "Always show Paste Generation Data button", description: "This will cause the 'Paste Generation Data' button to always show, even if copied data is incompatible and cannot be pasted.", showAllDescriptions: userSettings.alwaysShowSettingsHelp)
18 |
19 | }
20 | }
21 | }
22 |
23 | #Preview {
24 | SettingsView(selectedTab: .prompt)
25 | }
26 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/SoundUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoundUtility.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/18/24.
6 | //
7 |
8 | import AppKit
9 | import AVFoundation
10 |
11 | struct SoundUtility {
12 |
13 | static func play(systemSound: SystemSound) {
14 | if let sound = NSSound(contentsOfFile: systemSound.path, byReference: true) {
15 | sound.play()
16 | } else {
17 | Debug.log("Failed to play system sound")
18 | }
19 | }
20 |
21 | }
22 |
23 | enum SystemSound {
24 | case trash, poof, mount, unmount
25 |
26 | var path: String {
27 | switch self {
28 | case .trash: return "/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/dock/drag to trash.aif"
29 | case .poof: return "/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/dock/poof item off dock.aif"
30 | case .mount: return "/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/system/Volume Mount.aif"
31 | case .unmount: return "/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/system/Volume Mount.aif"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/NotificationUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserNotifications.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/11/24.
6 | //
7 |
8 | import Foundation
9 | import UserNotifications
10 |
11 | struct NotificationUtility {
12 |
13 | static func showCompletionNotification(imageCount: Int = 1) {
14 | var bodyText = "Your image has finished generating"
15 |
16 | if imageCount > 1 {
17 | bodyText = "\(imageCount) images have finished generating"
18 | }
19 |
20 | let center = UNUserNotificationCenter.current()
21 | center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
22 | if granted {
23 | let content = UNMutableNotificationContent()
24 | content.title = "Swift Diffusion" // "Generation Complete"
25 | content.body = bodyText
26 | content.sound = UNNotificationSound.default
27 |
28 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
29 | let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
30 |
31 | center.add(request)
32 | }
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/ContentView/CustomViews/BlueButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlueButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/2/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BlueButton: View {
11 | let title: String
12 | let action: () -> Void
13 |
14 | var body: some View {
15 | Button(action: action) {
16 | Text(title)
17 | .font(.system(size: 13, weight: .semibold))
18 | }
19 | .buttonStyle(BlueBackgroundButtonStyle())
20 | }
21 | }
22 |
23 | struct BlueSymbolButton: View {
24 | let title: String
25 | let symbol: SFSymbol
26 | let action: () -> Void
27 |
28 | var body: some View {
29 | Button(action: action) {
30 | HStack {
31 | Text(title)
32 | Image(systemName: symbol.name)
33 | }
34 | .font(.system(size: 13, weight: .medium))
35 | .padding(.horizontal, 3)
36 | }
37 | .buttonStyle(BlueBackgroundSmallButtonStyle())
38 | }
39 | }
40 |
41 | struct OutlineButton: View {
42 | let title: String
43 | let action: () -> Void
44 |
45 | var body: some View {
46 | Button(action: action) {
47 | Text(title)
48 | }
49 | .buttonStyle(BorderBackgroundButtonStyle())
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarFolderView/ParentFolderListItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParentFolderListItem.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/29/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ParentFolderListItem: View {
11 | @EnvironmentObject var sidebarModel: SidebarModel
12 | var parentFolder: SidebarFolder
13 | @State private var isHovering = false
14 |
15 | var body: some View {
16 | HStack {
17 | SFSymbol.upDirectory.image
18 | .foregroundStyle(isHovering ? .white : .secondary)
19 | .frame(width: 20)
20 | Text(parentFolder.name)
21 | .foregroundColor(isHovering ? .white : .primary)
22 | Spacer()
23 | }
24 | .padding(.vertical, 8).padding(.horizontal, 4)
25 | .contentShape(Rectangle())
26 | .onHover { hovering in
27 | if DragState.shared.isDragging {
28 | isHovering = hovering
29 | }
30 | }
31 | .cornerRadius(8)
32 | .background(Group {
33 | if isHovering {
34 | RoundedRectangle(cornerRadius: 8)
35 | .fill(Color.blue.opacity(0.9))
36 | }
37 | })
38 | .onDropHandling(isHovering: $isHovering, folderId: parentFolder.id, sidebarModel: sidebarModel)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/Views/Dividers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dividers.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HorizontalDivider: View {
11 | var lightColor: Color = .gray.opacity(0.25)
12 | var darkColor: Color = .black
13 | var thickness: CGFloat = 1
14 |
15 | @Environment(\.colorScheme) var colorScheme
16 |
17 | var body: some View {
18 | Rectangle()
19 | .fill(currentColor)
20 | .frame(height: thickness)
21 | .edgesIgnoringSafeArea(.all)
22 | }
23 |
24 | private var currentColor: Color {
25 | colorScheme == .dark ? darkColor : lightColor
26 | }
27 | }
28 |
29 | struct VerticalDivider: View {
30 | var lightColor: Color = .gray.opacity(0.25)
31 | var darkColor: Color = .black
32 | var thickness: CGFloat = 1
33 |
34 | @Environment(\.colorScheme) var colorScheme
35 |
36 | var body: some View {
37 | Rectangle()
38 | .fill(currentColor)
39 | .frame(width: thickness)
40 | .edgesIgnoringSafeArea(.all)
41 | }
42 |
43 | private var currentColor: Color {
44 | colorScheme == .dark ? darkColor : lightColor
45 | }
46 | }
47 |
48 | #Preview {
49 | CommonPreviews.detailView
50 | }
51 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FileRow/FileRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileRowView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FileRowView: View {
11 | let node: FileNode
12 | @StateObject private var thumbnailLoader = ThumbnailLoader()
13 |
14 | var body: some View {
15 | HStack {
16 | if let thumbnailImage = thumbnailLoader.thumbnailImage {
17 | Image(nsImage: thumbnailImage)
18 | .resizable()
19 | .scaledToFit()
20 | .frame(width: 20, height: 20)
21 | } else {
22 | ProgressView()
23 | .frame(width: 20, height: 20)
24 | }
25 |
26 | Text(node.name)
27 |
28 | Spacer()
29 |
30 | if let size = thumbnailLoader.imageSize {
31 | let width = Int(size.width)
32 | let height = Int(size.height)
33 | let dimensionString = "\(width)x\(height)"
34 | let fileSizeString = thumbnailLoader.fileSize
35 |
36 | Text("\(fileSizeString) • \(dimensionString)")
37 | .font(.caption)
38 | .foregroundColor(.gray)
39 | }
40 | }
41 | .task {
42 | await thumbnailLoader.loadThumbnail(for: node)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/SwiftDiffusion/ScriptManager/ScriptManager/StateDebugInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StateDebugInfo.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension ScriptState {
11 | var debugInfo: String {
12 | switch self {
13 | case .readyToStart: return ".readyToStart"
14 | case .launching: return ".launching"
15 | case .active: return ".active"
16 | case .isTerminating: return ".isTerminating"
17 | case .terminated: return ".terminated"
18 | case .unableToLocateScript: return ".unableToLocateScript"
19 | }
20 | }
21 | }
22 |
23 | extension GenerationStatus {
24 | var debugInfo: String {
25 | switch self {
26 | case .idle: return ".idle"
27 | case .preparingToGenerate: return ".preparingToGenerate"
28 | case .generating: return ".generating"
29 | case .finishingUp: return ".finishingUp"
30 | case .done: return ".done"
31 | }
32 | }
33 | }
34 |
35 | extension ModelLoadState {
36 | var debugInfo: String {
37 | switch self {
38 | case .launching: return ".launching"
39 | case .done: return ".done"
40 | case .isLoading: return ".isLoading"
41 | case .failed: return ".failed"
42 | case .idle: return ".idle"
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/FileUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileUtility.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import Foundation
9 |
10 | enum FileUtilityError: Error {
11 | case directoryCreationFailed(url: URL, underlyingError: Error)
12 | case urlConstructionFailed
13 | }
14 |
15 | struct FileUtility {
16 | /// Ensures a directory exists at the specified URL, throwing an error if creation fails.
17 | ///
18 | /// - If the directory exists: return the URL to said directory
19 | /// - If the directory does not exist: create the directory and return the URL to the newly created directory
20 | ///
21 | /// ## Usage
22 | /// ```swift
23 | /// do {
24 | /// try FileUtility.ensureDirectoryExists(at: directoryUrl)
25 | /// } catch {
26 | /// completion(error, directoryUrl)
27 | /// }
28 | /// ```
29 | static func ensureDirectoryExists(at url: URL) throws {
30 | let fileManager = FileManager.default
31 | if !fileManager.fileExists(atPath: url.path) {
32 | do {
33 | try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
34 | } catch {
35 | throw FileUtilityError.directoryCreationFailed(url: url, underlyingError: error)
36 | }
37 | }
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/CustomViews/BrowseFileRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowseFileRow.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BrowseFileRow: View {
11 | var labelText: String?
12 | var placeholderText: String
13 | @Binding var textValue: String
14 | var browseAction: () async -> String?
15 |
16 | var body: some View {
17 | VStack(alignment: .leading) {
18 | if let label = labelText {
19 | Text(label)
20 | .font(.system(size: 14, weight: .semibold, design: .rounded))
21 | .padding(.vertical, 2)
22 | .padding(.horizontal, 14)
23 | }
24 | HStack {
25 | TextField(placeholderText, text: $textValue)
26 | .truncationMode(.middle)
27 | .textFieldStyle(RoundedBorderTextFieldStyle())
28 | .font(.system(size: 11, design: .monospaced))
29 | .disabled(true)
30 | Button("Browse...") {
31 | Task {
32 | if let path = await browseAction() {
33 | textValue = path
34 | }
35 | }
36 | }
37 | }
38 | .padding(.bottom, 14)
39 | }
40 | }
41 | }
42 |
43 |
44 | #Preview {
45 | SettingsView(selectedTab: .files)
46 | }
47 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/SidebarModels/SidebarItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarItem.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/13/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | @Model class SidebarItem: Identifiable {
12 | @Attribute(.unique) var id: UUID = UUID()
13 | @Attribute var title: String
14 | @Attribute var timestamp: Date
15 | @Attribute var imageUrls: [URL]
16 | @Relationship(deleteRule: .cascade) var imageThumbnails: [ImageInfo]
17 | @Relationship(deleteRule: .cascade) var imagePreviews: [ImageInfo]
18 | @Relationship var prompt: StoredPromptModel?
19 | @Relationship var parent: SidebarFolder?
20 |
21 | init(title: String, timestamp: Date = Date(), imageUrls: [URL], prompt: StoredPromptModel? = nil, parent: SidebarFolder? = nil) {
22 | self.title = title
23 | self.timestamp = timestamp
24 | self.imageUrls = imageUrls
25 | self.imageThumbnails = []
26 | self.imagePreviews = []
27 | self.prompt = prompt
28 | self.parent = parent
29 | }
30 | }
31 |
32 | extension SidebarItem {
33 | func set(prompt: StoredPromptModel) {
34 | self.prompt = prompt
35 | }
36 | }
37 |
38 | extension SidebarItem: Equatable {
39 | static func == (lhs: SidebarItem, rhs: SidebarItem) -> Bool {
40 | lhs.id == rhs.id
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsTab.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsTab.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum SettingsTab: String {
11 | case general = "General"
12 | case files = "Files"
13 | case prompt = "Prompt"
14 | case engine = "Engine"
15 | case developer = "Developer"
16 |
17 | var symbol: String {
18 | switch self {
19 | case .general: return "gearshape"
20 | case .files: return "doc.on.doc"
21 | case .prompt: return "text.bubble"
22 | case .engine: return "arkit"
23 | case .developer: return "hammer"
24 | }
25 | }
26 |
27 | var hasHelpIndicators: Bool {
28 | switch self {
29 | case .general: return true
30 | case .files: return false
31 | case .prompt: return true
32 | case .engine: return true
33 | case .developer: return true
34 | }
35 | }
36 |
37 | var sectionHeaderText: String {
38 | switch self {
39 | case .general: return "General"
40 | case .files: return "Automatic Paths"
41 | case .prompt: return "Prompt"
42 | case .engine: return "Engine"
43 | case .developer: return "Developer"
44 | }
45 | }
46 | }
47 |
48 |
49 | extension SettingsTab: CaseIterable, Identifiable {
50 | var id: Self { self }
51 | }
52 |
53 |
54 | #Preview {
55 | SettingsView()
56 | }
57 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/CustomViews/SettingsSectionHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsSectionHeader.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SettingsSectionHeader: View {
11 | @ObservedObject var userSettings: UserSettings
12 | var selectedTab: SettingsTab = .general
13 |
14 | var body: some View {
15 |
16 | HStack {
17 | Text(selectedTab.sectionHeaderText)
18 | .font(.system(size: 20, weight: .semibold, design: .rounded))
19 | .padding(.leading, 12)
20 | .padding(.bottom, 10)
21 | .padding(.top, 20)
22 | Spacer()
23 |
24 | if selectedTab.hasHelpIndicators {
25 | Button(action: {
26 | userSettings.alwaysShowSettingsHelp.toggle()
27 | }) {
28 | HStack {
29 | Text(userSettings.alwaysShowSettingsHelp ? "Hide Help" : "Always Show Help")
30 | .font(.system(size: 11))
31 | }
32 | .padding(.horizontal, 2)
33 | }
34 | }
35 | }
36 | .padding(.horizontal, 14)
37 | .padding(.top)
38 | }
39 | }
40 |
41 |
42 | #Preview("Prompt Tab") {
43 | SettingsView(selectedTab: .prompt)
44 | }
45 |
46 | #Preview("Files Tab") {
47 | SettingsView(selectedTab: .files)
48 | }
49 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/Components/FilterSortingSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilterSortingSection.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/29/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FilterSortingSection: View {
11 | @Binding var sortingOrder: SidebarModel.SortingOrder
12 | @Binding var selectedModelName: String?
13 | @Binding var filterToolsButtonToggled: Bool
14 | let uniqueModelNames: [String]
15 |
16 | var body: some View {
17 | if filterToolsButtonToggled {
18 | VStack {
19 | List {
20 | Section(header: Text("Sorting")) {
21 | Menu(sortingOrder.rawValue) {
22 | Button("Most Recent") { sortingOrder = .mostRecent }
23 | Button("Least Recent") { sortingOrder = .leastRecent }
24 | }
25 | }
26 |
27 | Section(header: Text("Filters")) {
28 | Menu(selectedModelName ?? "Filter by Model") {
29 | Button("Show All") { selectedModelName = nil }
30 | Divider()
31 | ForEach(uniqueModelNames, id: \.self) { modelName in
32 | Button(modelName) { selectedModelName = modelName }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | .frame(height: 110)
39 | }
40 | }
41 | }
42 |
43 | /*
44 | #Preview {
45 | FilterSortingSection()
46 | }
47 | */
48 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/ContentView/ContentToolbar/ToolbarProgressView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToolbarProgressView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToolbarProgressView: View {
11 | @ObservedObject private var scriptManager = ScriptManager.shared
12 |
13 | var body: some View {
14 | if scriptManager.modelLoadState == .done && scriptManager.modelLoadTime > 0 {
15 | Text("\(String(format: "%.1f", scriptManager.modelLoadTime))s")
16 | .font(.system(size: 11, design: .monospaced))
17 | .padding(.trailing, 6)
18 | }
19 |
20 | if scriptManager.genStatus == .generating {
21 | Text("\(Int(scriptManager.genProgress * 100))%")
22 | .font(.system(.body, design: .monospaced))
23 | } else if scriptManager.genStatus == .finishingUp {
24 | Text("Saving")
25 | .font(.system(.body, design: .monospaced))
26 | } else if scriptManager.genStatus == .preparingToGenerate {
27 | ProgressView()
28 | .progressViewStyle(CircularProgressViewStyle())
29 | .scaleEffect(0.5)
30 | } else if scriptManager.genStatus == .done {
31 | Image(systemName: SFSymbol.checkmark.name)
32 | }
33 |
34 | if scriptManager.genStatus != .idle || scriptManager.scriptState == .launching {
35 | ContentProgressBar(scriptManager: scriptManager)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/CustomViews/ToggleWithHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToggleWithHeader.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToggleWithHeader: View {
11 | @Binding var isToggled: Bool
12 | var header: String
13 | var description: String = ""
14 | @State private var isHovering = false
15 | var showAllDescriptions: Bool
16 |
17 | var body: some View {
18 | HStack(alignment: .top) {
19 | Toggle("", isOn: $isToggled)
20 | .padding(.trailing, 6)
21 | .frame(width: 60)
22 | .toggleStyle(SwitchToggleStyle(tint: .blue))
23 |
24 | VStack(alignment: .leading) {
25 | HStack {
26 | Text(header)
27 | .font(.system(size: 14, weight: .semibold, design: .default))
28 | .underline()
29 | .padding(.vertical, 2)
30 | SFSymbol.help.image
31 | .onHover { isHovering in
32 | self.isHovering = isHovering
33 | }
34 | }
35 | Text(description)
36 | .font(.system(size: 12))
37 | .foregroundStyle(Color.secondary)
38 | .opacity(showAllDescriptions || isHovering ? 1 : 0)
39 | }
40 |
41 | Spacer()
42 | }
43 | .padding(.bottom, 8)
44 | }
45 | }
46 |
47 | #Preview {
48 | SettingsView(selectedTab: .prompt)
49 | }
50 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Observers/DirectoryObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DirectoryObserver.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import Foundation
9 |
10 | class DirectoryObserver {
11 | private var fileDescriptor: Int32 = -1
12 | private var source: DispatchSourceFileSystemObject?
13 | private var observationTask: Task<(), Never>?
14 |
15 | func startObserving(url: URL, onChange: @escaping () async -> Void) {
16 | stopObserving()
17 |
18 | fileDescriptor = open(url.path, O_EVTONLY)
19 | guard fileDescriptor != -1 else {
20 | Debug.log("Unable to open the directory.")
21 | return
22 | }
23 |
24 | let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: .global())
25 | self.source = source
26 |
27 | observationTask = Task {
28 | source.setEventHandler { [weak self] in
29 | guard self != nil else { return }
30 | Task { await onChange() }
31 | }
32 |
33 | source.setCancelHandler { [weak self] in
34 | guard let self = self else { return }
35 | close(self.fileDescriptor)
36 | self.fileDescriptor = -1
37 | }
38 |
39 | source.resume()
40 | }
41 | }
42 |
43 | func stopObserving() {
44 | source?.cancel()
45 | observationTask?.cancel()
46 | observationTask = nil
47 | source = nil
48 | }
49 |
50 | deinit {
51 | stopObserving()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/URLParserUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLParserUtility.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/2/24.
6 | //
7 |
8 | import Foundation
9 |
10 | class URLParserUtility {
11 | struct URLParsingConfig {
12 | let pattern: String
13 | let messageContains: String
14 | }
15 |
16 | func parseURL(from output: String, withConfigs configs: [URLParsingConfig], completion: @escaping (URL?) -> Void) {
17 | for config in configs {
18 | if let urlStr = extractURL(from: output, using: config), let url = URL(string: urlStr) {
19 | DispatchQueue.main.async {
20 | completion(url)
21 | }
22 | return
23 | }
24 | }
25 | DispatchQueue.main.async {
26 | completion(nil)
27 | }
28 | }
29 |
30 | private func extractURL(from output: String, using config: URLParsingConfig) -> String? {
31 | guard output.contains(config.messageContains) else { return nil }
32 |
33 | do {
34 | let regex = try NSRegularExpression(pattern: config.pattern, options: .caseInsensitive)
35 | let nsRange = NSRange(output.startIndex.. Void)? = nil) {
18 | let process = Process()
19 | let pipe = Pipe()
20 |
21 | process.executableURL = Constants.CommandLine.zshUrl
22 | process.arguments = ["-c", Constants.CommandLine.killallPython]
23 | process.standardOutput = pipe
24 | process.standardError = pipe
25 |
26 | do {
27 | try process.run()
28 | process.waitUntilExit() // wait for process to exit
29 |
30 | // read and log the output
31 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
32 | let output = String(data: data, encoding: .utf8) ?? ""
33 | DispatchQueue.main.async {
34 | Debug.log("Terminate Python Output: \(output)")
35 | self.updateConsoleOutput(with: "All Python-related processes have been killed.")
36 | completion?()
37 | self.terminateImmediately()
38 | }
39 | } catch {
40 | DispatchQueue.main.async {
41 | Debug.log("Failed to terminate Python processes: \(error)")
42 | completion?()
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FileHierarchy/FileHierarchy+Control.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileHierarchy+Control.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension FileHierarchy {
11 | func nextImage(currentPath: String) -> FileNode? {
12 | // Assuming a method getAllImageFiles() returns sorted image files by modification date
13 | let files = getAllImageFiles()
14 | guard let currentIndex = files.firstIndex(where: { $0.fullPath == currentPath }) else { return nil }
15 | return currentIndex + 1 < files.count ? files[currentIndex + 1] : nil
16 | }
17 |
18 | func previousImage(currentPath: String) -> FileNode? {
19 | let files = getAllImageFiles()
20 | guard let currentIndex = files.firstIndex(where: { $0.fullPath == currentPath }) else { return nil }
21 | return currentIndex - 1 >= 0 ? files[currentIndex - 1] : nil
22 | }
23 | }
24 |
25 | extension FileHierarchy {
26 | private func flattenImageNodes(node: FileNode) -> [FileNode] {
27 | var nodes = [FileNode]()
28 | if let children = node.children {
29 | for child in children {
30 | nodes += flattenImageNodes(node: child)
31 | }
32 | } else if node.isImage {
33 | nodes.append(node)
34 | }
35 | return nodes
36 | }
37 |
38 | func getAllImageFiles() -> [FileNode] {
39 | var allImageNodes = [FileNode]()
40 | for rootNode in rootNodes {
41 | allImageNodes += flattenImageNodes(node: rootNode)
42 | }
43 | return allImageNodes.sorted(by: { $0.lastModified > $1.lastModified })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarExtensions/Sidebar+Move.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sidebar+Move.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/29/24.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftData
10 |
11 | extension Sidebar {
12 | func moveItemInItemQueue() {
13 | if sidebarModel.beginMovableSidebarItemQueue,
14 | let sidebarId = sidebarModel.queueMovableSidebarItemID,
15 | let folderId = sidebarModel.queueDestinationFolderID {
16 | moveItem(sidebarId, toFolderWithId: folderId)
17 | }
18 | sidebarModel.queueMovableSidebarItemID = nil
19 | sidebarModel.queueDestinationFolderID = nil
20 | sidebarModel.beginMovableSidebarItemQueue = false
21 | }
22 | }
23 |
24 | extension Sidebar {
25 | func moveItem(_ itemId: UUID, toFolderWithId targetFolderId: UUID) {
26 | Debug.log("[Sidebar] moveItem - Moving item with ID: \(itemId) to folder with ID: \(targetFolderId)")
27 |
28 | guard let sourceFolder = findFolderForItem(itemId),
29 | let targetFolder = findSidebarFolder(by: targetFolderId, in: sidebarFolders),
30 | let itemIndex = sourceFolder.items.firstIndex(where: { $0.id == itemId }) else {
31 | Debug.log("[Sidebar] Error: Unable to find source or target folder for item ID: \(itemId)")
32 | return
33 | }
34 |
35 | let itemToMove = sourceFolder.items.remove(at: itemIndex)
36 | targetFolder.items.append(itemToMove)
37 | Debug.log("[Sidebar] Successfully moved item \(itemToMove.title) to \(targetFolder.name)")
38 | sidebarModel.saveData(in: modelContext)
39 | SoundUtility.play(systemSound: .mount)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/SwiftDiffusion/App/AppStructure/AppDocuments.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDocuments.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Constants.FileStructure {
11 | static let AppDocumentsUrl: URL? = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
12 | static let AppDocumentsFolderName = "SwiftDiffusion"
13 | }
14 |
15 | /// App directories and their associated URLs
16 | enum AppDocuments: String, CaseIterable {
17 | // output images
18 | case documents = "SwiftDiffusion"
19 | case txt2img = "SwiftDiffusion/txt2img"
20 | }
21 |
22 | extension AppDocuments {
23 | /// `URL` to the AppDirectory case (if it exists)
24 | var url: URL? {
25 | guard let appDocumentsUrl = Constants.FileStructure.AppDocumentsUrl else {
26 | Debug.log("Error: Unable to find Documents directory.")
27 | return nil
28 | }
29 | return appDocumentsUrl.appendingPathComponent(self.rawValue)
30 | }
31 | }
32 |
33 | extension AppFileStructure {
34 | static func setupDocuments(completion: @escaping (Error?, URL?) -> Void) {
35 | for directoryPath in AppDocuments.allCases {
36 | guard let directoryUrl = directoryPath.url else {
37 | completion(FileUtilityError.urlConstructionFailed, nil)
38 | return
39 | }
40 | do {
41 | try FileUtility.ensureDirectoryExists(at: directoryUrl)
42 | } catch {
43 | completion(error, directoryUrl)
44 | return
45 | }
46 | }
47 | // indicate success if all directories were ensured without errors
48 | completion(nil, nil)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarFolderView/DropHandlerModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DropHandlerModifier.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/29/24.
6 | //
7 |
8 | import SwiftUI
9 | import UniformTypeIdentifiers
10 |
11 | struct DropHandlerModifier: ViewModifier {
12 | var isHovering: Binding?
13 | var folderId: UUID
14 | var sidebarModel: SidebarModel
15 |
16 | func body(content: Content) -> some View {
17 | content
18 | .onDrop(of: [UTType.plainText], isTargeted: isHovering) { providers in
19 | Debug.log("[DD] Attempting to drop on folder with ID: \(folderId)")
20 | DragState.shared.isDragging = false
21 | return providers.first?.loadObject(ofClass: NSString.self) { (nsItem, error) in
22 | guard let itemIDStr = nsItem as? String else {
23 | Debug.log("[DD] Failed to load the dropped item ID string")
24 | return
25 | }
26 | DispatchQueue.main.async {
27 | if let itemId = UUID(uuidString: itemIDStr) {
28 | Debug.log("[DD] Successfully identified item with ID: \(itemId) for dropping into folder ID: \(folderId)")
29 | sidebarModel.moveSidebarItem(withId: itemId, toFolderWithId: folderId)
30 | DragState.shared.isDragging = false
31 | }
32 | }
33 | } != nil
34 | }
35 | }
36 | }
37 |
38 | extension View {
39 | func onDropHandling(isHovering: Binding? = nil, folderId: UUID, sidebarModel: SidebarModel) -> some View {
40 | modifier(DropHandlerModifier(isHovering: isHovering, folderId: folderId, sidebarModel: sidebarModel))
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Services/PastableService/PasteGenerationDataButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PasteGenerationDataButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/7/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PasteGenerationDataButton: View {
11 | @ObservedObject var pastableService = PastableService.shared
12 | @EnvironmentObject var sidebarModel: SidebarModel
13 | @EnvironmentObject var checkpointsManager: CheckpointsManager
14 | @EnvironmentObject var vaeModelsManager: ModelManager
15 |
16 | @State var showButtonWithAnimation: Bool = false
17 |
18 | var body: some View {
19 | HStack {
20 | if showButtonWithAnimation {
21 | BlueSymbolButton(title: "Paste", symbol: .paste) {
22 | if let pastablePromptData = pastableService.parsePasteboard(checkpoints: checkpointsManager.models, vaeModels: vaeModelsManager.models) {
23 | sidebarModel.createNewWorkspaceItem(withPrompt: pastablePromptData)
24 | }
25 | withAnimation {
26 | pastableService.clearPasteboard()
27 | pastableService.canPasteData = false
28 | }
29 | }
30 | }
31 | }
32 | .onChange(of: pastableService.canPasteData) {
33 | withAnimation {
34 | showButtonWithAnimation = pastableService.canPasteData
35 | }
36 | }
37 | .onReceive(NotificationCenter.default.publisher(for: NSApplication.willBecomeActiveNotification)) { _ in
38 | Task {
39 | await pastableService.checkForPastableData()
40 | }
41 | }
42 | }
43 | }
44 |
45 |
46 | #Preview {
47 | PasteGenerationDataButton()
48 | .frame(width: 200, height: 80)
49 | }
50 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarModel/SidebarModel+Move.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarModel+Move.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: User Gesture Move
11 | extension SidebarModel {
12 | func moveSidebarItem(withId sidebarItemId: UUID, toFolderWithId folderId: UUID) {
13 | queueMovableSidebarItemID = sidebarItemId
14 | queueDestinationFolderID = folderId
15 | beginMovableSidebarItemQueue = true
16 | }
17 | }
18 |
19 | // MARK: Move on Action
20 | extension SidebarModel {
21 | func moveWorkspaceItemToCurrentFolder() {
22 | guard let selectedSidebarItem = selectedSidebarItem,
23 | let currentFolder = currentFolder
24 | else { return }
25 |
26 | storableSidebarItems.removeAll(where: { $0 == selectedSidebarItem })
27 | PreviewImageProcessingManager.shared.createImagePreviewsAndThumbnails(for: selectedSidebarItem, in: modelContext)
28 | selectedSidebarItem.timestamp = Date()
29 | move(sidebarItem: selectedSidebarItem, from: workspaceFolder, to: currentFolder)
30 | }
31 |
32 | func moveSelectedSidebarItem(to folder: SidebarFolder) {
33 | guard let selectedSidebarItem = selectedSidebarItem,
34 | let currentFolder = currentFolder
35 | else { return }
36 |
37 | move(sidebarItem: selectedSidebarItem, from: currentFolder, to: folder)
38 | }
39 |
40 | private func move(sidebarItem: SidebarItem, from currentParent: SidebarFolder, to targetParent: SidebarFolder) {
41 | targetParent.add(item: sidebarItem)
42 | currentParent.remove(item: sidebarItem)
43 | saveData(in: modelContext)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/Symbols/SymbolButtons.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SymbolButtons.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MenuButton: View {
11 | let title: String
12 | var symbol: SFSymbol = .none
13 | let action: () -> Void
14 |
15 | var body: some View {
16 | Button(action: action) {
17 | HStack {
18 | if symbol != .none {
19 | symbol.image
20 | }
21 | Text(title)
22 | }
23 | }
24 | }
25 | }
26 |
27 | struct SymbolButton: View {
28 | let symbol: SFSymbol
29 | let action: () -> Void
30 |
31 | var body: some View {
32 | Button(action: {
33 | action()
34 | }) {
35 | symbol.image
36 | }
37 | .buttonStyle(BorderlessButtonStyle())
38 | }
39 | }
40 |
41 | struct AccessorySymbolButton: View {
42 | let symbol: SFSymbol
43 | let action: () -> Void
44 |
45 | var body: some View {
46 | Button(action: {
47 | action()
48 | }) {
49 | symbol.image
50 | }
51 | .buttonStyle(.accessoryBar)
52 | }
53 | }
54 |
55 | struct ToolbarSymbolButton: View {
56 | let title: String
57 | let symbol: SFSymbol
58 | let action: () -> Void
59 |
60 | var body: some View {
61 | Button(action: {
62 | action()
63 | }) {
64 | Label(title, systemImage: symbol.name)
65 | }
66 | }
67 | }
68 |
69 |
70 | struct SymbolButtons: View {
71 | var body: some View {
72 | VStack(spacing: 10) {
73 | MenuButton(title: "Test", symbol: .python, action: {})
74 | SymbolButton(symbol: .python, action: {})
75 | }
76 | .padding()
77 | }
78 | }
79 |
80 | #Preview {
81 | SymbolButtons()
82 | }
83 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Services/AutomaticServices/Txt2ImgService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Txt2ImgService.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import Foundation
9 |
10 | class Txt2ImgService {
11 | static let shared = Txt2ImgService()
12 |
13 | private init() {}
14 |
15 | func sendImageGenerationRequest(to endpoint: Constants.API.Endpoint, with payload: [String: Any], baseAPI: URL) async -> [String]? {
16 | guard let url = endpoint.url(relativeTo: baseAPI) else {
17 | Debug.log("[Txt2ImgService] Invalid URL for endpoint: \(endpoint)")
18 | return nil
19 | }
20 |
21 | do {
22 | var request = URLRequest(url: url)
23 | request.httpMethod = "POST"
24 | request.addValue("application/json", forHTTPHeaderField: "Content-Type")
25 | request.httpBody = try JSONSerialization.data(withJSONObject: payload, options: [])
26 |
27 | let session = URLSession(configuration: self.customSessionConfiguration())
28 | let (data, _) = try await session.data(for: request)
29 |
30 | if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
31 | let images = json["images"] as? [String] {
32 | return images
33 | }
34 | } catch {
35 | Debug.log("[Txt2ImgService] API request failed with error: \(error)")
36 | }
37 | return nil
38 | }
39 |
40 | private func customSessionConfiguration() -> URLSessionConfiguration {
41 | let configuration = URLSessionConfiguration.default
42 | configuration.timeoutIntervalForRequest = Constants.API.timeoutInterval
43 | configuration.timeoutIntervalForResource = Constants.API.timeoutInterval
44 | return configuration
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FileRow/ThumbnailLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThumbnailLoader.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import Cocoa
9 | import SwiftUI
10 |
11 | class ThumbnailLoader: ObservableObject {
12 | @Published var thumbnailImage: NSImage?
13 | @Published var imageSize: CGSize?
14 | @Published var fileSize: String = ""
15 |
16 | func loadThumbnail(for node: FileNode) async {
17 | await processImage(from: node.fullPath)
18 | await fetchAndSetFileSize(for: node.fullPath)
19 | }
20 |
21 | @MainActor
22 | private func processImage(from path: String) {
23 | guard let image = NSImage(contentsOfFile: path) else {
24 | self.thumbnailImage = nil
25 | return
26 | }
27 |
28 | let thumbnail = image.resizedToMaintainAspectRatio(targetHeight: 40)
29 | self.thumbnailImage = thumbnail
30 | self.imageSize = image.size
31 | }
32 |
33 | private func fetchAndSetFileSize(for path: String) async {
34 | do {
35 | let fileSizeAttributes = try FileManager.default.attributesOfItem(atPath: path)
36 | if let fileSize = fileSizeAttributes[.size] as? NSNumber {
37 | await MainActor.run {
38 | self.fileSize = formatFileSize(fileSize.intValue)
39 | }
40 | }
41 | } catch {
42 | Debug.log("[ThumbnailLoader] fetchAndSetFileSize(for: \(path))\n > Error fetching file size: \(error)")
43 | }
44 | }
45 |
46 | private func formatFileSize(_ size: Int) -> String {
47 | let formatter = ByteCountFormatter()
48 | formatter.allowedUnits = [.useKB, .useMB]
49 | formatter.countStyle = .file
50 | return formatter.string(fromByteCount: Int64(size))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptBars/PasteGenerationDataStatusBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PasteGenerationDataStatusBar.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/16/24.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftData
10 |
11 | struct PasteGenerationDataStatusBar: View {
12 | @Environment(\.modelContext) private var modelContext
13 | @EnvironmentObject var sidebarModel: SidebarModel
14 | @EnvironmentObject var currentPrompt: PromptModel
15 | @ObservedObject var userSettings = UserSettings.shared
16 |
17 | var generationDataInPasteboard: Bool
18 | var onPaste: (String) -> Void
19 |
20 | var body: some View {
21 | if sidebarModel.workspaceFolderContainsSelectedSidebarItem() {
22 |
23 |
24 | if generationDataInPasteboard || userSettings.alwaysShowPasteboardGenerationDataButton {
25 | HStack(alignment: .center) {
26 |
27 | Spacer()
28 |
29 | Button(action: {
30 | if let pasteboardContent = getPasteboardString() {
31 | onPaste(pasteboardContent)
32 | }
33 | sidebarModel.storeChangesOfSelectedSidebarItem(with: currentPrompt)
34 | }) {
35 | Text("Paste Generation Data")
36 | Image(systemName: SFSymbol.paste.name)
37 | }
38 | .buttonStyle(.accessoryBar)
39 |
40 |
41 | }
42 | .padding(.horizontal, 12)
43 | .frame(height: 30)
44 | .background(VisualEffectBlurView(material: .sheet, blendingMode: .behindWindow))
45 | }
46 | }
47 |
48 | }
49 |
50 | func getPasteboardString() -> String? {
51 | return NSPasteboard.general.string(forType: .string)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptMenus/VaeModelMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VaeModelMenu.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/25/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct VaeModelMenu: View {
11 | @EnvironmentObject var currentPrompt: PromptModel
12 | @EnvironmentObject var vaeModelsManager: ModelManager
13 |
14 | @State private var isExpanded: Bool = false
15 |
16 | var body: some View {
17 | VStack {
18 | HStack {
19 | ExpandableSectionHeader(title: "VAE Model", isExpanded: $isExpanded)
20 |
21 | Spacer()
22 |
23 | if !isExpanded {
24 | Text(currentPrompt.vaeModel?.name ?? "None")
25 | .foregroundStyle(.secondary)
26 | .opacity(0.8)
27 | .lineLimit(1)
28 | .truncationMode(.tail)
29 | }
30 | }
31 |
32 | if isExpanded {
33 | VStack(alignment: .leading, spacing: 0) {
34 | HStack {
35 | Menu {
36 | Button("None") {
37 | currentPrompt.vaeModel = nil
38 | }
39 | Divider()
40 |
41 | ForEach(vaeModelsManager.models.sorted(by: { $0.name.lowercased() < $1.name.lowercased() }), id: \.id) { vae in
42 | Button(vae.name) {
43 | currentPrompt.vaeModel = vae
44 | }
45 | }
46 | } label: {
47 | Label(currentPrompt.vaeModel?.name ?? "None", systemImage: "line.3.crossed.swirl.circle")
48 | }
49 | }
50 | }
51 | }
52 |
53 | }
54 | .padding(.vertical, 10)
55 | }
56 | }
57 |
58 | #Preview {
59 | CommonPreviews.promptView
60 | }
61 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/AppRelauncherUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppRelauncherUtility.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/21/24.
6 | //
7 |
8 | import Foundation
9 | import AppKit
10 |
11 | class AppRelauncherUtility {
12 | private let scriptPath: String = NSTemporaryDirectory() + "relaunch.sh"
13 |
14 | init() {
15 | let scriptContent = """
16 | #!/bin/bash
17 |
18 | # Wait until the application quits
19 | while ps -p $1 > /dev/null; do sleep 1; done
20 |
21 | sleep 2 # Increase this delay if needed
22 |
23 | # Launch the application again
24 | open -a "$2"
25 | """
26 |
27 | do {
28 | try scriptContent.write(toFile: scriptPath, atomically: true, encoding: .utf8)
29 | try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath)
30 | } catch {
31 | Debug.log("Failed to create or set permissions for script: \(error)")
32 | }
33 | }
34 |
35 | func relaunchApplication() {
36 | let process = Process()
37 | let bundleID = Bundle.main.bundleIdentifier ?? "com.buzsh.SwiftDiffusion"
38 |
39 | Debug.log("relaunchApplication with bundleID: \(bundleID)")
40 |
41 | let scriptURL = URL(fileURLWithPath: scriptPath) // Corrected method to form URL
42 |
43 | process.launchPath = "/bin/bash"
44 | process.arguments = [scriptURL.path, String(ProcessInfo.processInfo.processIdentifier), bundleID]
45 |
46 | do {
47 | try process.run()
48 | } catch {
49 | Debug.log("Failed to launch script: \(error)")
50 | }
51 |
52 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
53 | NSApplication.shared.terminate(nil)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/PreviewImageProcessing/CachedThumbnailImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CachedThumbnailImageView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/26/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A view that displays a thumbnail image from a URL, caching it for performance.
11 | struct CachedThumbnailImageView: View {
12 | let imageUrl: URL
13 | let width: CGFloat
14 | let height: CGFloat
15 | @State private var displayedImage: NSImage?
16 |
17 | init(imageUrl: URL, width: CGFloat = 50, height: CGFloat = 65) {
18 | self.imageUrl = imageUrl
19 | self.width = width
20 | self.height = height
21 | }
22 |
23 | var body: some View {
24 | Group {
25 | if let displayedImage = displayedImage {
26 | Image(nsImage: displayedImage)
27 | .resizable()
28 | .scaledToFill()
29 | .frame(width: width, height: height)
30 | .clipped()
31 | } else {
32 | Rectangle()
33 | .foregroundColor(Color.gray.opacity(0.3))
34 | .frame(width: width, height: height)
35 | }
36 | }
37 | .onAppear(perform: loadImage)
38 | }
39 |
40 | /// Attempts to load the image from cache or fetches it from the URL if not cached.
41 | private func loadImage() {
42 | // Attempt to retrieve the image from cache
43 | if let cachedImage = ImageCache.shared.image(forKey: imageUrl.path) {
44 | displayedImage = cachedImage
45 | return
46 | }
47 |
48 | DispatchQueue.global(qos: .userInitiated).async {
49 | if let image = NSImage(contentsOf: imageUrl) {
50 | DispatchQueue.main.async {
51 | ImageCache.shared.setImage(image, forKey: imageUrl.path)
52 | self.displayedImage = image
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/Components/DisplayOptionsBar/DisplayOptionsBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DisplayOptionsBar.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/15/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Constants.Layout {
11 | struct SidebarToolbar {
12 | static let itemHeight: CGFloat = 20
13 | static let itemWidth: CGFloat = 30
14 |
15 | static let bottomBarHeight: CGFloat = 50
16 | }
17 | }
18 |
19 | struct DisplayOptionsBar: View {
20 | @EnvironmentObject var sidebarModel: SidebarModel
21 |
22 | var body: some View {
23 | VisualEffectView(material: .sidebar, blendingMode: .withinWindow)
24 | .frame(height: Constants.Layout.SidebarToolbar.bottomBarHeight)
25 | .edgesIgnoringSafeArea(.bottom)
26 | .overlay(
27 | HStack {
28 |
29 | Spacer()
30 |
31 | HoverToggleButton(buttonToggled: $sidebarModel.modelNameButtonToggled, symbol: "arkit")
32 |
33 | Spacer()
34 |
35 | SegmentedDisplayOptions(
36 | noPreviewsItemButtonToggled: $sidebarModel.noPreviewsItemButtonToggled,
37 | smallPreviewsButtonToggled: $sidebarModel.smallPreviewsButtonToggled,
38 | largePreviewsButtonToggled: $sidebarModel.largePreviewsButtonToggled)
39 | .padding(.trailing, 4)
40 |
41 | Spacer()
42 |
43 | }
44 | )
45 | }
46 | }
47 |
48 | /*
49 | #Preview {
50 | @State var toggle0: Bool = true
51 | @State var toggle1: Bool = false
52 | @State var toggle2: Bool = true
53 | @State var toggle3: Bool = false
54 | return DisplayOptionsBar(modelNameButtonToggled: $toggle0, noPreviewsItemButtonToggled: $toggle1, smallPreviewsButtonToggled: $toggle2, largePreviewsButtonToggled: $toggle3)
55 | .frame(width: 250)
56 | }
57 | */
58 |
--------------------------------------------------------------------------------
/SwiftDiffusion/ScriptManager/PythonProcess/ScriptManager+PythonProcess.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScriptManager+PythonProcess.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension ScriptManager: PythonProcessDelegate {
11 | func setupPythonProcess() {
12 | let pythonProcess = PythonProcess()
13 | pythonProcess.delegate = self
14 |
15 | guard userSettings.webuiShellPath.isEmpty || userSettings.stableDiffusionModelsPath.isEmpty else {
16 | updateScriptState(.unableToLocateScript)
17 | return
18 | }
19 |
20 | if let (scriptDirectory, scriptName) = ScriptSetupHelper.setupScriptPath(userSettings.webuiShellPath) {
21 | pythonProcess.runScript(at: scriptDirectory, scriptName: scriptName)
22 | }
23 | }
24 |
25 | func pythonProcessDidUpdateOutput(output: String) {
26 | let filteredOutput = shouldTrimOutput ? output.trimmingCharacters(in: .whitespacesAndNewlines) : output
27 | DispatchQueue.main.async {
28 | self.consoleOutput += "\n\(filteredOutput)"
29 | self.detectExistingPythonProcesses(from: filteredOutput)
30 | self.parseServiceUrl(from: filteredOutput)
31 | self.updateProgressBasedOnOutput(output: output)
32 | self.updateModelLoadStateBasedOnOutput(output: output)
33 | }
34 | }
35 |
36 | func pythonProcessDidFinishRunning(with result: ScriptResult) {
37 | switch result {
38 | case .success(let message):
39 | DispatchQueue.main.async {
40 | self.updateScriptState(.terminated)
41 | self.updateConsoleOutput(with: message)
42 | }
43 | case .failure(let error):
44 | DispatchQueue.main.async {
45 | self.updateConsoleOutput(with: "Script failed: \(error.localizedDescription)")
46 | self.updateScriptState(.readyToStart)
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/ContentView/CustomViews/ContentProgressBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentProgressBar.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/13/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | class ProgressViewModel: ObservableObject {
11 | @Published var progress: Double = 0 {
12 | didSet {
13 | Debug.log("Progress updated to \(progress)")
14 | }
15 | }
16 | }
17 |
18 | struct CustomLinearProgressViewStyle: ProgressViewStyle {
19 | @Environment(\.colorScheme) var colorScheme
20 |
21 | func makeBody(configuration: Configuration) -> some View {
22 | ProgressView(configuration)
23 | .shadow(radius: colorScheme == .dark ? 2 : 2)
24 | }
25 | }
26 |
27 | struct ContentProgressBar: View {
28 | @ObservedObject var scriptManager: ScriptManager
29 | @State private var launchStatusProgressBarValue: Double = -1
30 |
31 | var body: some View {
32 | VStack {
33 | if scriptManager.genStatus != .idle {
34 | ProgressView(value: scriptManager.genProgress)
35 | .progressViewStyle(CustomLinearProgressViewStyle())
36 | .frame(minWidth: 75, idealWidth: 120, maxWidth: 120)
37 |
38 |
39 | } else if launchStatusProgressBarValue < 0 {
40 | ProgressView(value: launchStatusProgressBarValue)
41 | .progressViewStyle(CustomLinearProgressViewStyle())
42 | .frame(minWidth: 75, idealWidth: 120, maxWidth: 120)
43 | }
44 | }
45 | .onChange(of: scriptManager.scriptState) {
46 | if scriptManager.scriptState == .launching {
47 | launchStatusProgressBarValue = -1
48 | } else {
49 | launchStatusProgressBarValue = 100
50 | }
51 | }
52 | }
53 |
54 |
55 | }
56 |
57 | #Preview {
58 | ContentProgressBar(scriptManager: ScriptManager.preview(withState: .active))
59 | .frame(width: 200, height: 80)
60 | }
61 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/SidebarModels/SidebarFolder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarFolder.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/27/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | @Model class SidebarFolder: Identifiable {
12 | @Attribute(.unique) var id: UUID = UUID()
13 | @Attribute var name: String
14 | @Attribute var timestamp: Date
15 | @Attribute var isRoot: Bool = false
16 | @Attribute var isWorkspace: Bool = false
17 | @Relationship(deleteRule: .cascade) var items: [SidebarItem]
18 | @Relationship(deleteRule: .cascade) var folders: [SidebarFolder]
19 | @Relationship var parent: SidebarFolder?
20 |
21 | init(name: String, timestamp: Date = Date(), isRoot: Bool = false, isWorkspace: Bool = false, items: [SidebarItem] = [], folders: [SidebarFolder] = [], parent: SidebarFolder? = nil) {
22 | self.name = name
23 | self.timestamp = timestamp
24 | self.isRoot = isRoot
25 | self.isWorkspace = isWorkspace
26 | self.items = items
27 | self.folders = folders
28 | self.parent = parent
29 | }
30 | }
31 |
32 | extension SidebarFolder: Equatable {
33 | static func == (lhs: SidebarFolder, rhs: SidebarFolder) -> Bool {
34 | lhs.id == rhs.id
35 | }
36 | }
37 |
38 | extension SidebarFolder {
39 | func add(item: SidebarItem) {
40 | item.parent = self
41 | self.items.append(item)
42 | }
43 | func add(folder: SidebarFolder) {
44 | folder.parent = self
45 | self.folders.append(folder)
46 | }
47 | func remove(item: SidebarItem) {
48 | self.items.removeAll(where: { $0.id == item.id })
49 | }
50 | func remove(folder: SidebarFolder) {
51 | self.folders.removeAll(where: { $0.id == folder.id })
52 | }
53 | }
54 |
55 | extension SidebarFolder {
56 | /// The folder is deletable if it's neither a root nor a workspace folder
57 | func isDeletable() -> Bool {
58 | return !isRoot && !isWorkspace
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarFolderView/SidebarFolderItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarFolderItem.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/28/24.
6 | //
7 |
8 | import SwiftUI
9 | import UniformTypeIdentifiers
10 |
11 | class DragState: ObservableObject {
12 | static let shared = DragState()
13 | @Published var isDragging = false
14 | }
15 |
16 | struct SidebarFolderItem: View {
17 | @EnvironmentObject var sidebarModel: SidebarModel
18 | let folder: SidebarFolder
19 | @State private var isHovering = false
20 | @State private var isHoveringWithoutDragging: Bool = false
21 |
22 | var totalNumberOfItemsContained: Int {
23 | folder.folders.count + folder.items.count
24 | }
25 |
26 | var body: some View {
27 | HStack {
28 | SFSymbol.folder.image
29 | .foregroundStyle(isHovering ? .white : .blue)
30 | .frame(width: 20)
31 | Text(folder.name)
32 | .foregroundColor(isHovering ? .white : .primary)
33 | Spacer()
34 | Text("\(totalNumberOfItemsContained)")
35 | .frame(minWidth: 20)
36 | .padding(2)
37 | .background(Capsule().fill(Color.black).opacity(0.2))
38 | .foregroundColor(.secondary)
39 | .font(.caption)
40 | }
41 | .frame(height: 30)
42 | .padding(.horizontal, 4)
43 | .contentShape(Rectangle())
44 | .onHover { hovering in
45 | if DragState.shared.isDragging {
46 | isHovering = hovering
47 | }
48 | }
49 | .cornerRadius(8)
50 | .background(Group {
51 | if isHovering {
52 | RoundedRectangle(cornerRadius: 8)
53 | .fill(Color.blue.opacity(0.9))
54 | }
55 | })
56 | .onDropHandling(isHovering: $isHovering, folderId: folder.id, sidebarModel: sidebarModel)
57 | }
58 | }
59 |
60 | #Preview {
61 | let sidebarFolder = SidebarFolder(name: "Some Sidebar")
62 | return SidebarFolderItem(folder: sidebarFolder)
63 | }
64 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/AutomaticModels/Custom/Checkpoints/Models/CheckpointModelPreferences.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckpointModelPreferences.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import Foundation
9 |
10 | @MainActor
11 | class CheckpointModelPreferences: ObservableObject {
12 | @Published var samplingMethod: String
13 | @Published var positivePrompt: String = ""
14 | @Published var negativePrompt: String = ""
15 | @Published var width: Double = 512
16 | @Published var height: Double = 512
17 | @Published var cfgScale: Double = 7
18 | @Published var samplingSteps: Double = 20
19 | @Published var clipSkip: Double = 1
20 | @Published var batchCount: Double = 1
21 | @Published var batchSize: Double = 1
22 | @Published var seed: String = "-1"
23 |
24 | init(samplingMethod: String = "DPM++ 2M Karras") {
25 | self.samplingMethod = samplingMethod
26 | }
27 |
28 | static func defaultSamplingForCheckpointModelType(type: CheckpointModelType) -> CheckpointModelPreferences {
29 | let samplingMethod: String
30 | switch type {
31 | case .coreMl: samplingMethod = "DPM-Solver++"
32 | case .python: samplingMethod = "DPM++ 2M Karras"
33 | }
34 | return CheckpointModelPreferences(samplingMethod: samplingMethod)
35 | }
36 | }
37 |
38 | extension CheckpointModelPreferences {
39 | convenience init(from promptModel: PromptModel) {
40 | self.init(samplingMethod: promptModel.samplingMethod ?? "DPM++ 2M Karras")
41 | self.positivePrompt = promptModel.positivePrompt
42 | self.negativePrompt = promptModel.negativePrompt
43 | self.width = promptModel.width
44 | self.height = promptModel.height
45 | self.cfgScale = promptModel.cfgScale
46 | self.samplingSteps = promptModel.samplingSteps
47 | self.clipSkip = promptModel.clipSkip
48 | self.batchCount = promptModel.batchCount
49 | self.batchSize = promptModel.batchSize
50 | self.seed = promptModel.seed
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/SwiftDiffusion/ScriptManager/ScriptManager/GenerationStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GenerationStatus.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/9/24.
6 | //
7 |
8 | import Foundation
9 |
10 | enum GenerationStatus {
11 | case idle
12 | case preparingToGenerate
13 | case generating
14 | case finishingUp
15 | case done
16 | }
17 |
18 | /*
19 | >>
20 | 12%|█▎ | 1/8 [00:01<00:12, 1.80s/it]
21 | */
22 |
23 | extension ScriptManager {
24 | func parseAndUpdateProgress(output: String) {
25 | let regexPattern = "\\s*(Total progress: )?(\\d+)%"
26 | do {
27 | let regex = try NSRegularExpression(pattern: regexPattern, options: [])
28 | let nsRange = NSRange(output.startIndex.. 0, let range = Range(matchRange, in: output) {
34 | let progressString = String(output[range])
35 | if let progressValue = Double(progressString) {
36 | DispatchQueue.main.async {
37 | self.progressHasReachedAboveZero(progressValue)
38 | self.genProgress = progressValue / 100.0
39 | Debug.log("Updated progress to \(progressValue)%")
40 | }
41 | break // Optionally break after the first successful update
42 | }
43 | }
44 | }
45 | } catch {
46 | Debug.log("Regex error: \(error)")
47 | }
48 | }
49 |
50 | func progressHasReachedAboveZero(_ progress: Double) {
51 | if progress > 0 {
52 | genStatus = .generating
53 | }
54 |
55 | if progress >= 100 {
56 | genStatus = .finishingUp
57 | }
58 | }
59 |
60 | func updateProgressBasedOnOutput(output: String) {
61 | Task {
62 | parseAndUpdateProgress(output: output)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Utilities/ImageSaver/ImageSaver+Composite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageSaver+Composite.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import AppKit
9 |
10 | extension ImageSaver {
11 | static func createCompositeImageData(from images: [NSImage], withCompressionFactor: Double = 1.0) async -> Data? {
12 | guard !images.isEmpty else { return nil }
13 |
14 | let rowCount = Int(ceil(Double(images.count) / 2.0))
15 | let columnCount = min(images.count, 2) // ensures we don't calculate more columns than there are images
16 |
17 | guard let firstImage = images.first else { return nil }
18 | guard let firstImageRep = firstImage.representations.first else { return nil }
19 | let imageSize = CGSize(width: firstImageRep.pixelsWide, height: firstImageRep.pixelsHigh)
20 |
21 | let finalSize = CGSize(width: imageSize.width * CGFloat(columnCount), height: imageSize.height * CGFloat(rowCount))
22 |
23 | let finalImage = NSImage(size: finalSize)
24 | finalImage.lockFocus()
25 |
26 | NSColor.black.setFill()
27 | NSRect(origin: .zero, size: finalSize).fill()
28 |
29 | for (index, image) in images.enumerated() {
30 | let row = index / columnCount
31 | let column = index % columnCount
32 | let xPosition = CGFloat(column) * imageSize.width
33 | let yPosition = CGFloat(row) * imageSize.height
34 | image.draw(in: NSRect(x: xPosition, y: yPosition, width: imageSize.width, height: imageSize.height))
35 | }
36 |
37 | finalImage.unlockFocus()
38 |
39 | // convert finalImage to a compressed JPEG
40 | guard let tiffData = finalImage.tiffRepresentation,
41 | let imageRep = NSBitmapImageRep(data: tiffData),
42 | let jpegData = imageRep.representation(using: .jpeg, properties: [.compressionFactor: withCompressionFactor]) else {
43 | Debug.log("Failed to compress image")
44 | return nil
45 | }
46 |
47 | return jpegData
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FullscreenImage/ImageWindowManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageWindowManager.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/11/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import AppKit
11 |
12 | class ImageWindowManager: ObservableObject {
13 | private var imageWindowController: NSWindowController?
14 |
15 | func openImageWindow(with image: NSImage) {
16 | let contentView = FullscreenImageView(image: image) {
17 | self.imageWindowController?.close()
18 | self.imageWindowController = nil
19 | }
20 |
21 | var contentRect = NSRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
22 |
23 | if let screen = NSScreen.main {
24 | let screenRect = screen.visibleFrame
25 | let maxWidth = screenRect.width * 1.0
26 | let maxHeight = screenRect.height * 1.0
27 |
28 | let scalingFactor = min(1, min(maxWidth / image.size.width, maxHeight / image.size.height))
29 |
30 | contentRect.size = NSSize(width: image.size.width * scalingFactor, height: image.size.height * scalingFactor)
31 |
32 | let centerX = screenRect.origin.x + (screenRect.width - contentRect.width) / 2
33 | let centerY = screenRect.origin.y + (screenRect.height - contentRect.height) / 2
34 | contentRect.origin = CGPoint(x: centerX, y: centerY)
35 | }
36 |
37 | let window = NSWindow(
38 | contentRect: contentRect,
39 | styleMask: [.closable, .resizable, .miniaturizable],
40 | backing: .buffered, defer: false)
41 | window.contentView = NSHostingView(rootView: contentView)
42 | window.isMovableByWindowBackground = true
43 | window.title = "Image Preview"
44 | window.aspectRatio = contentRect.size
45 |
46 | self.imageWindowController = NSWindowController(window: window)
47 | self.imageWindowController?.showWindow(nil)
48 |
49 | window.makeKeyAndOrderFront(nil)
50 | NSApp.activate(ignoringOtherApps: true)
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/RequiredInputPaths/RequiredInputPathsPulsatingButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequiredInputPathsPulsatingButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RequiredInputPathsPulsatingButton: View {
11 | @Binding var showingRequiredInputPathsView: Bool
12 | @Binding var hasDismissedRequiredInputPathsView: Bool
13 | @State private var isPulsating = false
14 | @State private var timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
15 |
16 | var body: some View {
17 | Button(action: {
18 | showingRequiredInputPathsView = true
19 | hasDismissedRequiredInputPathsView = false // reset dismissal tracking
20 | }) {
21 | SFSymbol.warning.image
22 | .scaleEffect(isPulsating ? 1.1 : 1.0)
23 | .foregroundColor(isPulsating ? .orange : .secondary)
24 | }
25 | .onAppear {
26 | triggerPulsation()
27 | }
28 | .onReceive(timer) { _ in
29 | triggerPulsation()
30 | }
31 | }
32 |
33 | private func triggerPulsation() {
34 | // reset to original state before starting animation
35 | self.isPulsating = false
36 | // start pulsation with a delay to allow for reset to take effect
37 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
38 | withAnimation(Animation.easeInOut(duration: 1)) {
39 | self.isPulsating = true
40 | }
41 | // smoothly return to the initial state after the animation completes
42 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
43 | withAnimation(Animation.easeInOut(duration: 1)) {
44 | self.isPulsating = false
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
51 | #Preview {
52 | NavigationView {
53 |
54 | }.toolbar {
55 | ToolbarItemGroup(placement: .navigation) {
56 | RequiredInputPathsPulsatingButton(showingRequiredInputPathsView: .constant(false), hasDismissedRequiredInputPathsView: .constant(true))
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/SwiftDiffusion/App/AppStructure/AppFileStructure.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppFileStructure.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/8/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension AppDirectory {
11 | /// `URL` to the AppDirectory case (if it exists)
12 | var url: URL? {
13 | guard let appSupportUrl = Constants.FileStructure.AppSupportUrl else {
14 | Debug.log("Error: Unable to find Application Support directory.")
15 | return nil
16 | }
17 | let baseFolderUrl = appSupportUrl.appendingPathComponent(Constants.FileStructure.AppSupportFolderName)
18 | return baseFolderUrl.appendingPathComponent(self.rawValue)
19 | }
20 | }
21 | /// Core app file-folder structure setup and configuration.
22 | struct AppFileStructure {
23 | /// Attempts to ensure that the required directory structure for the application exists.
24 | /// Calls the completion handler with an error and the URL of the directory that failed to be created, if applicable.
25 | ///
26 | /// ## Usage
27 | /// ```swift
28 | /// FileUtility.AppFileStructure.setup { error, failedUrl in
29 | /// if let error = error, let failedUrl = failedUrl {
30 | /// print("Failed to create directory at \(failedUrl): \(error)")
31 | /// } else if let error = error {
32 | /// print("Error: \(error)")
33 | /// } else {
34 | /// print("Success")
35 | /// }
36 | /// ```
37 | static func setup(completion: @escaping (Error?, URL?) -> Void) {
38 | for directoryPath in AppDirectory.allCases {
39 | guard let directoryUrl = directoryPath.url else {
40 | completion(FileUtilityError.urlConstructionFailed, nil)
41 | return
42 | }
43 | do {
44 | try FileUtility.ensureDirectoryExists(at: directoryUrl)
45 | } catch {
46 | completion(error, directoryUrl)
47 | return
48 | }
49 | }
50 | // indicate success if all directories were ensured without errors
51 | completion(nil, nil)
52 | }
53 | }
54 |
55 | extension AppFileStructure {
56 |
57 |
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/Components/DisplayOptionsBar/HoverToggleButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HoverButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/15/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Constants for HoverToggleButton customization
11 | private struct HoverToggleButtonConstants {
12 | static let defaultSymbol: String = "arkit"
13 | static let itemWidth: CGFloat = 30.0
14 | static let itemHeight: CGFloat = 30.0
15 | static let cornerRadius: CGFloat = 10.0
16 | static let hoverBackgroundOpacity: CGFloat = 0.2
17 | static let nonHoverBackgroundOpacity: CGFloat = 0.0
18 | static let paddingLeading: CGFloat = 4.0
19 | }
20 |
21 | struct HoverToggleButton: View {
22 | @Binding var buttonToggled: Bool
23 | var symbol: String = HoverToggleButtonConstants.defaultSymbol
24 | let itemWidth: CGFloat = HoverToggleButtonConstants.itemWidth
25 | let itemHeight: CGFloat = HoverToggleButtonConstants.itemHeight
26 | @State private var isHovering: Bool = false
27 |
28 | var body: some View {
29 | Button(action: {
30 | buttonToggled.toggle()
31 | }) {
32 | Image(systemName: symbol)
33 | .foregroundColor(buttonToggled ? .blue : .primary)
34 | }
35 | .buttonStyle(BorderlessButtonStyle())
36 | .frame(width: itemWidth, height: itemHeight)
37 | .background(isHovering ? RoundedRectangle(cornerRadius: HoverToggleButtonConstants.cornerRadius).fill(Color.gray.opacity(HoverToggleButtonConstants.hoverBackgroundOpacity)) : RoundedRectangle(cornerRadius: HoverToggleButtonConstants.cornerRadius).fill(Color.clear.opacity(HoverToggleButtonConstants.nonHoverBackgroundOpacity)))
38 | .clipShape(RoundedRectangle(cornerRadius: HoverToggleButtonConstants.cornerRadius))
39 | .padding(.leading, HoverToggleButtonConstants.paddingLeading)
40 | .onHover { hovering in
41 | isHovering = hovering
42 | }
43 | }
44 | }
45 |
46 |
47 | #Preview {
48 | @State var toggle: Bool = true
49 | return HoverToggleButton(buttonToggled: $toggle)
50 | .frame(width: 100, height: 40)
51 | }
52 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/DeveloperSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeveloperSection.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DeveloperSection: View {
11 | @ObservedObject var userSettings: UserSettings
12 |
13 | var body: some View {
14 | VStack(alignment: .leading) {
15 | ToggleWithHeader(isToggled: $userSettings.alwaysStartPythonEnvironmentAtLaunch, header: "Start Python environment at launch", description: "This will automatically ready the Python environment such that you can start generating immediately.", showAllDescriptions: userSettings.alwaysShowSettingsHelp)
16 |
17 | ToggleWithHeader(isToggled: $userSettings.showPythonEnvironmentControls, header: "Show Python environment controls", description: "This will allow you to build and stop the Python environment from the toolbar. Also comes with a little status light!", showAllDescriptions: userSettings.alwaysShowSettingsHelp).disabled(userSettings.showDeveloperInterface)
18 |
19 | ToggleWithHeader(isToggled: $userSettings.showDeveloperInterface, header: "Show developer interface", description: "This will show the developer tools, live variable states and other debugging information.", showAllDescriptions: userSettings.alwaysShowSettingsHelp)
20 |
21 | ToggleWithHeader(isToggled: $userSettings.launchWebUiAlongsideScriptLaunch, header: "Launch webui with script", description: "This will cause the --nowebui command line argument to be excluded from the launch conditions. Useful for needing to debug in a web browser.", showAllDescriptions: userSettings.alwaysShowSettingsHelp)
22 |
23 | ToggleWithHeader(isToggled: $userSettings.killAllPythonProcessesOnTerminate, header: "Kill all Python processes on terminate", description: "Will terminate all Python processes on terminate. Useful for Xcode development force stopping.", showAllDescriptions: userSettings.alwaysShowSettingsHelp)
24 | }
25 | }
26 | }
27 |
28 | #Preview {
29 | SettingsView(selectedTab: .developer)
30 | }
31 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/EngineSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EngineSection.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct EngineSection: View {
11 | @ObservedObject var userSettings: UserSettings
12 | @ObservedObject var scriptManager = ScriptManager.shared
13 |
14 | @State var showRestartAppForChangesToTakeEffectAlert: Bool = false
15 |
16 | var body: some View {
17 | VStack(alignment: .leading) {
18 | ToggleWithHeader(isToggled: $userSettings.disableModelLoadingRamOptimizations, header: "Disable model loading RAM optimizations", description: "Can resolve certain model loading issues involving the MPS framework (TypeError: float16, float32, float64).\n\nNote: Can also increase model loading times.", showAllDescriptions: userSettings.alwaysShowSettingsHelp)
19 | }
20 | .onChange(of: userSettings.disableModelLoadingRamOptimizations) {
21 | showRestartAppForChangesToTakeEffectAlert = true
22 | }
23 | .alert(isPresented: $showRestartAppForChangesToTakeEffectAlert) {
24 | var message: String = ""
25 | message.append("SwiftDiffusion needs to be restarted in order for these changes to take effect.\n\nWould you like to close the app now?\n\nNote: Due to MacOS restrictions, you may need to re-open the app yourself.")
26 |
27 | return Alert(
28 | title: Text("App Restart Required"),
29 | message: Text(message),
30 | primaryButton: .default(Text("Close App")) {
31 | scriptManager.terminateImmediately()
32 | Delay.by(0.1) {
33 | relaunchApplication()
34 | }
35 |
36 | },
37 | secondaryButton: .cancel(Text("Later")) {
38 | Debug.log("User chose to postpone restart")
39 | }
40 | )
41 | }
42 | }
43 |
44 | func relaunchApplication() {
45 | let appRelauncher = AppRelauncherUtility()
46 | appRelauncher.relaunchApplication()
47 | }
48 | }
49 |
50 | #Preview {
51 | SettingsView(selectedTab: .engine)
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FileOutlineView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileOutlineView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FileOutlineView: View {
11 | @ObservedObject var fileHierarchyObject: FileHierarchy
12 | @Binding var selectedImage: NSImage?
13 | @State private var selectedNode: FileNode?
14 | var onSelectImage: (String) -> Void
15 | var lastSelectedImagePath: String
16 |
17 | var body: some View {
18 | List(self.fileHierarchyObject.rootNodes, children: \.children) { node in
19 | HStack {
20 | if node.isLeaf {
21 | if node.isImage {
22 | FileRowView(node: node)
23 | } else {
24 | Image(systemName: node.iconName)
25 | Text(node.name)
26 | }
27 | } else {
28 | Image(systemName: node.iconName)
29 | Text(node.name)
30 | }
31 | Spacer()
32 | }
33 | .padding(5)
34 | .background(self.selectedNode == node ? Color.blue : Color.clear)
35 | .cornerRadius(5)
36 | .onTapGesture {
37 | self.selectNode(node)
38 | }
39 | .onAppear {
40 | if node.fullPath == lastSelectedImagePath {
41 | self.selectedNode = node
42 | }
43 | }
44 | }
45 | }
46 |
47 | private func isSelected(_ node: FileNode) -> Bool {
48 | node.fullPath == lastSelectedImagePath
49 | }
50 |
51 | private func thumbnailForImage(at path: String) -> NSImage {
52 | if let image = NSImage(contentsOfFile: path) {
53 | return image.resizedToMaintainAspectRatio(targetHeight: 20)
54 | } else {
55 | return NSImage(systemSymbolName: "photo.fill", accessibilityDescription: nil) ?? NSImage()
56 | }
57 | }
58 |
59 | private func selectNode(_ node: FileNode) {
60 | selectedNode = node
61 | guard node.isLeaf else { return }
62 | if let _ = NSImage(contentsOfFile: node.fullPath) {
63 | self.selectedImage = NSImage(contentsOfFile: node.fullPath)
64 | Task {
65 | await MainActor.run {
66 | onSelectImage(node.fullPath)
67 | }
68 | }
69 | }
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/StoredModels/StoredCheckpointApiModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoredCheckpointApiModel.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/27/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | @Model
12 | class StoredCheckpointApiModel {
13 | @Attribute var title: String
14 | @Attribute var modelName: String
15 | @Attribute var modelHash: String?
16 | @Attribute var sha256: String?
17 | @Attribute var filename: String
18 | @Attribute var config: String?
19 |
20 | init(title: String, modelName: String, modelHash: String? = nil, sha256: String? = nil, filename: String, config: String? = nil) {
21 | self.title = title
22 | self.modelName = modelName
23 | self.modelHash = modelHash
24 | self.sha256 = sha256
25 | self.filename = filename
26 | self.config = config
27 | }
28 | }
29 |
30 | extension MapModelData {
31 |
32 | func toCheckpointApiModel(from storedCheckpointApiModel: StoredCheckpointApiModel? = nil) -> CheckpointApiModel? {
33 | guard let storedApiModel = storedCheckpointApiModel else { return nil }
34 | return CheckpointApiModel(title: storedApiModel.title,
35 | modelName: storedApiModel.modelName,
36 | modelHash: storedApiModel.modelHash,
37 | sha256: storedApiModel.sha256,
38 | filename: storedApiModel.filename,
39 | config: storedApiModel.config
40 | )
41 | }
42 |
43 | func toStoredCheckpointApiModel(from checkpointApiModel: CheckpointApiModel?) -> StoredCheckpointApiModel? {
44 | guard let checkpointApiModel = checkpointApiModel else { return nil }
45 |
46 | return StoredCheckpointApiModel(title: checkpointApiModel.title,
47 | modelName: checkpointApiModel.modelName,
48 | modelHash: checkpointApiModel.modelHash,
49 | sha256: checkpointApiModel.sha256,
50 | filename: checkpointApiModel.filename,
51 | config: checkpointApiModel.config
52 | )
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarModel/SidebarModel+Save.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarModel+Save.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/7/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension SidebarModel {
11 | @MainActor func saveWorkspaceItem(withPrompt prompt: PromptModel) {
12 | guard let selectedSidebarItem = selectedSidebarItem
13 | else { return }
14 |
15 | let mapModelData = MapModelData()
16 | selectedSidebarItem.prompt = mapModelData.toStored(promptModel: prompt)
17 | saveData(in: modelContext)
18 | }
19 | }
20 |
21 | // TODO: Update with newer syntax
22 | extension SidebarModel {
23 | @MainActor func storeChangesOfSelectedSidebarItem(with prompt: PromptModel) {
24 | if let selectedSidebarItem = selectedSidebarItem {
25 | storeChanges(of: selectedSidebarItem, with: prompt)
26 | }
27 | }
28 |
29 | @MainActor func storeChanges(of sidebarItem: SidebarItem, with prompt: PromptModel) {
30 | if workspaceFolderContains(sidebarItem: sidebarItem) {
31 | let mapModelData = MapModelData()
32 | let updatedPrompt = mapModelData.toStored(promptModel: prompt)
33 |
34 | if !selectedSidebarItemTitle(hasEqualTitleTo: updatedPrompt) && !prompt.positivePrompt.isEmpty {
35 | if let newTitle = updatedPrompt?.positivePrompt {
36 | selectedSidebarItem?.title = newTitle.truncatingToLength(Constants.Sidebar.titleLength)
37 | }
38 | }
39 |
40 | selectedSidebarItem?.prompt = updatedPrompt
41 | saveData(in: modelContext)
42 | }
43 | }
44 | }
45 |
46 | extension String {
47 | /// Truncates the string to the specified length and appends an ellipsis if the string was longer than the specified length.
48 | ///
49 | /// - Parameter maxLength: The maximum allowed length of the string.
50 | /// - Returns: A string truncated to `maxLength` characters with an ellipsis appended if the original string exceeded `maxLength`.
51 | func truncatingToLength(_ maxLength: Int) -> String {
52 | if self.count > maxLength {
53 | let index = self.index(self.startIndex, offsetBy: maxLength)
54 | return String(self[.. ScriptManager {
77 | let previewManager = ScriptManager()
78 | previewManager.scriptState = state
79 | previewManager.serviceUrl = URL(string: "http://127.0.0.1:7860")
80 | return previewManager
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/ContentView/ContentToolbar/DeveloperItems/DeveloperToolbarItems.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeveloperToolbarItems.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/8/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DeveloperToolbarItems: View {
11 | @Binding var selectedView: ViewManager
12 | @ObservedObject private var userSettings = UserSettings.shared
13 | @ObservedObject private var scriptManager = ScriptManager.shared
14 | @ObservedObject private var pastableService = PastableService.shared
15 |
16 | var body: some View {
17 | if userSettings.showDeveloperInterface || userSettings.showPythonEnvironmentControls {
18 | if userSettings.showPythonEnvironmentControls {
19 | Circle()
20 | .fill(scriptManager.scriptState.statusColor)
21 | .frame(width: 10, height: 10)
22 | .padding(.trailing, 2)
23 | }
24 |
25 | if userSettings.showPythonEnvironmentControls && userSettings.launchWebUiAlongsideScriptLaunch {
26 | if scriptManager.scriptState == .active, let url = scriptManager.serviceUrl {
27 |
28 | ToolbarSymbolButton(title: "Network", symbol: .network, action: {
29 | NSWorkspace.shared.open(url)
30 | })
31 | }
32 | }
33 |
34 | if userSettings.showDeveloperInterface {
35 | SegmentedViewPicker(selectedView: $selectedView)
36 | }
37 |
38 | if userSettings.showPythonEnvironmentControls,
39 | let title = (scriptManager.scriptState == .readyToStart) ? "Start" : "Stop" {
40 |
41 | ToolbarSymbolButton(title: title, symbol: actionButtonSymbol, action: {
42 | scriptManager.scriptState == .readyToStart ? scriptManager.run() : scriptManager.terminate()
43 | })
44 | .disabled(scriptManager.scriptState == .terminated)
45 | }
46 | }
47 | }
48 |
49 | var actionButtonTitle: String {
50 | if scriptManager.scriptState == .readyToStart {
51 | "Start"
52 | } else {
53 | "Stop"
54 | }
55 | }
56 |
57 | var actionButtonSymbol: SFSymbol {
58 | if scriptManager.scriptState == .readyToStart {
59 | SFSymbol.play
60 | } else {
61 | SFSymbol.stop
62 | }
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptRows/PromptRows.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PromptRows.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/8/24.
6 | //
7 |
8 | import SwiftUI
9 | import CompactSlider
10 |
11 | #Preview {
12 | CommonPreviews.promptView
13 | }
14 |
15 | struct PromptRowHeading: View {
16 | var title: String
17 |
18 | var body: some View {
19 | Text(title)
20 | .textCase(.uppercase)
21 | .font(.system(size: 11, weight: .bold, design: .rounded))
22 | .opacity(0.8)
23 | .padding(.horizontal, 8)
24 | }
25 |
26 | }
27 |
28 | struct DimensionSelectionRow: View {
29 | @Binding var width: Double
30 | @Binding var height: Double
31 |
32 | var body: some View {
33 | VStack(alignment: .leading) {
34 | PromptRowHeading(title: "Dimensions")
35 | HStack {
36 | CompactSlider(value: $width, in: 64...2048, step: 64) {
37 | Text("Width")
38 | Spacer()
39 | Text("\(Int(width))")
40 | }
41 | CompactSlider(value: $height, in: 64...2048, step: 64) {
42 | Text("Height")
43 | Spacer()
44 | Text("\(Int(height))")
45 | }
46 | }
47 | }
48 | .padding(.bottom, Constants.Layout.promptRowPadding)
49 | }
50 | }
51 |
52 | struct DetailSelectionRow: View {
53 | @Binding var cfgScale: Double
54 | @Binding var samplingSteps: Double
55 |
56 | var body: some View {
57 | VStack(alignment: .leading) {
58 | PromptRowHeading(title: "Detail")
59 | HStack {
60 | CompactSlider(value: $cfgScale, in: 1...30, step: 0.5) {
61 | Text("CFG Scale")
62 | Spacer()
63 | Text(String(format: "%.1f", cfgScale))
64 | }
65 | CompactSlider(value: $samplingSteps, in: 1...150, step: 1) {
66 | Text("Sampling Steps")
67 | Spacer()
68 | Text("\(Int(samplingSteps))")
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 | struct HalfSkipClipRow: View {
76 | @Binding var clipSkip: Double
77 |
78 | var body: some View {
79 | VStack {
80 | HStack {
81 | HalfMaxWidthView {}
82 | CompactSlider(value: $clipSkip, in: 1...12, step: 1) {
83 | Text("Clip Skip")
84 | Spacer()
85 | Text("\(Int(clipSkip))")
86 | }
87 | }
88 | }
89 | }
90 | }
91 |
92 |
93 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/Views/ShareButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShareButton.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/26/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ShareButton: View {
11 | @Binding var selectedImage: NSImage?
12 |
13 | var body: some View {
14 | DetailToolbarSymbolButton(hint: "Share Image", symbol: .share, action: {
15 | if let image = selectedImage {
16 | SharePickerCoordinator.shared.showSharePicker(for: image)
17 | }
18 | })
19 | .disabled(selectedImage == nil)
20 | }
21 | }
22 |
23 | #Preview {
24 | CommonPreviews.detailView
25 | }
26 |
27 | class SharePickerCoordinator: NSObject, NSSharingServicePickerDelegate {
28 | static let shared = SharePickerCoordinator()
29 |
30 | func showSharePicker(for image: NSImage) {
31 | let sharingServicePicker = NSSharingServicePicker(items: [image])
32 | sharingServicePicker.delegate = self
33 |
34 | if let window = NSApp.keyWindow {
35 | sharingServicePicker.show(relativeTo: CGRect(x: window.frame.width / 2, y: window.frame.height / 2, width: 0, height: 0), of: window.contentView!, preferredEdge: .minY)
36 | }
37 | }
38 | }
39 |
40 | struct SharePickerRepresentable: NSViewRepresentable {
41 | @Binding var showingSharePicker: Bool
42 | @Binding var selectedImage: NSImage?
43 |
44 | func makeNSView(context: Context) -> NSView {
45 | let view = NSView()
46 | return view
47 | }
48 |
49 | func updateNSView(_ nsView: NSView, context: Context) {
50 | }
51 |
52 | func makeCoordinator() -> Coordinator {
53 | Coordinator(self)
54 | }
55 |
56 | class Coordinator: NSObject, NSSharingServicePickerDelegate {
57 | var parent: SharePickerRepresentable
58 |
59 | init(_ parent: SharePickerRepresentable) {
60 | self.parent = parent
61 | }
62 |
63 | func share() {
64 | guard let image = parent.selectedImage else { return }
65 | let sharingServicePicker = NSSharingServicePicker(items: [image])
66 | sharingServicePicker.delegate = self
67 |
68 | if let window = NSApp.keyWindow {
69 | sharingServicePicker.show(relativeTo: .zero, of: window.contentView!, preferredEdge: .minY)
70 | }
71 |
72 | DispatchQueue.main.async {
73 | self.parent.showingSharePicker = false
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarFolderView/SidebarItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarItemView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/29/24.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftData
10 |
11 | /// Represents a view for displaying stored sidebar items, including handling for small and large previews.
12 | /// This view dynamically selects and presents thumbnail or preview images based on user interaction toggles.
13 | /// - Parameters:
14 | /// - item: The `SidebarItem` to display, including all associated image and model information.
15 | struct SidebarItemView: View {
16 | @EnvironmentObject var sidebarModel: SidebarModel
17 | let item: SidebarItem
18 | @State private var currentSmallThumbnailImageUrl: URL?
19 | @State private var currentLargeImageUrl: URL?
20 |
21 | var body: some View {
22 | HStack(alignment: .center, spacing: 8) {
23 | if sidebarModel.smallPreviewsButtonToggled, let thumbnailUrl = thumbnailInfo?.url {
24 | CachedThumbnailImageView(imageUrl: thumbnailUrl, width: 70, height: 70)
25 | .clipShape(RoundedRectangle(cornerRadius: 8))
26 | .shadow(color: .black, radius: 1, x: 0, y: 1)
27 | }
28 |
29 | VStack(alignment: .center) {
30 | if sidebarModel.largePreviewsButtonToggled, let largePreviewInfo = previewInfo {
31 | CachedPreviewImageView(imageInfo: largePreviewInfo)
32 | .scaledToFill()
33 | .frame(width: sidebarModel.currentWidth)
34 | .shadow(color: .black, radius: 1, x: 0, y: 1)
35 | .padding(.bottom, 8)
36 | }
37 |
38 | Text(item.title)
39 | .lineLimit(2)
40 |
41 | if sidebarModel.modelNameButtonToggled, let modelName = item.prompt?.selectedModel?.name {
42 | Text(modelName)
43 | .font(.system(size: 10, weight: .light, design: .monospaced))
44 | .foregroundStyle(Color.secondary)
45 | .padding(.top, 1)
46 | }
47 | }
48 | .frame(maxWidth: .infinity, alignment: .leading)
49 | }
50 | }
51 |
52 | var thumbnailInfo: ImageInfo? {
53 | item.imageThumbnails.first(where: { $0.url.lastPathComponent.contains("-grid") })
54 | ?? item.imageThumbnails.last
55 | }
56 |
57 | var previewInfo: ImageInfo? {
58 | item.imagePreviews.first(where: { $0.url.lastPathComponent.contains("-grid") })
59 | ?? item.imagePreviews.last
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Services/AutomaticServices/AutomaticApiService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AutomaticApiService.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol EndpointRepresentable {
11 | static var fetchEndpoint: String? { get }
12 | static var refreshEndpoint: String? { get }
13 | }
14 |
15 | class AutomaticApiService {
16 | static let shared = AutomaticApiService()
17 | private let scriptManager = ScriptManager.shared
18 |
19 | private init() {}
20 |
21 | func request(endpoint: String, httpMethod: HttpMethod = .get) async throws -> Data {
22 | guard let apiUrl = await scriptManager.serviceUrl else {
23 | throw NetworkError.invalidURL
24 | }
25 |
26 | let fullUrl = apiUrl.appendingPathComponent(endpoint)
27 |
28 | var request = URLRequest(url: fullUrl)
29 | request.httpMethod = httpMethod.rawValue
30 |
31 | let (data, response) = try await URLSession.shared.data(for: request)
32 |
33 | guard let httpResponse = response as? HTTPURLResponse,
34 | (200...299).contains(httpResponse.statusCode) else {
35 | let statusCode = (response as? HTTPURLResponse)?.statusCode
36 | throw NetworkError.badResponse(statusCode: statusCode)
37 | }
38 |
39 | return data
40 | }
41 |
42 | }
43 |
44 | extension AutomaticApiService {
45 |
46 | func fetchDataItem(for type: T.Type) async throws -> T {
47 | guard let endpoint = T.fetchEndpoint else {
48 | throw NetworkError.fetchEndpointIsNil
49 | }
50 | let data = try await request(endpoint: endpoint)
51 | return try JSONDecoder().decode(T.self, from: data)
52 | }
53 |
54 | func fetchData(for type: [T].Type) async throws -> [T] {
55 | guard let endpoint = T.fetchEndpoint else {
56 | throw NetworkError.fetchEndpointIsNil
57 | }
58 | let data = try await request(endpoint: endpoint)
59 | return try JSONDecoder().decode([T].self, from: data)
60 | }
61 |
62 | func refreshData(for type: T.Type) async throws {
63 | guard let endpoint = T.refreshEndpoint else { return }
64 | _ = try await request(endpoint: endpoint, httpMethod: .post)
65 | }
66 |
67 | }
68 |
69 | extension AutomaticApiService {
70 | enum HttpMethod: String {
71 | case get = "GET"
72 | case post = "POST"
73 | }
74 |
75 | enum NetworkError: Error {
76 | case fetchEndpointIsNil
77 | case invalidURL
78 | case badResponse(statusCode: Int? = nil)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/AutomaticModels/Generic/ModelManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelManager.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import Foundation
9 |
10 | class ModelManager: ObservableObject {
11 | @Published var models: [T] = []
12 | private var directoryObserver: DirectoryObserver?
13 | private var userSettings = UserSettings.shared
14 |
15 | @Published var errorMessage: String?
16 | @Published var showError: Bool = false
17 |
18 | func startObservingDirectory() {
19 | guard let directoryUrl = UserSettings.shared.modelDirectoryUrl(forType: T.self) else { return }
20 |
21 | loadModels()
22 |
23 | directoryObserver = DirectoryObserver()
24 | directoryObserver?.startObserving(url: directoryUrl) { [weak self] in
25 | DispatchQueue.main.async {
26 | self?.loadModels()
27 | }
28 | }
29 | }
30 |
31 | func stopObservingDirectory() {
32 | directoryObserver?.stopObserving()
33 | }
34 |
35 | func refreshModels() {
36 | Task {
37 | do {
38 | if let _ = T.refreshEndpoint {
39 | try await AutomaticApiService.shared.refreshData(for: T.self)
40 | }
41 | } catch {
42 | DispatchQueue.main.async {
43 | self.errorMessage = "Failed to refresh models: \(error.localizedDescription)"
44 | Debug.log(self.errorMessage)
45 | self.showError = true
46 | }
47 | }
48 | }
49 | }
50 |
51 | func loadModels() {
52 | Task {
53 | do {
54 | if let _ = T.refreshEndpoint {
55 | try await AutomaticApiService.shared.refreshData(for: T.self)
56 | }
57 |
58 | let models = try await AutomaticApiService.shared.fetchData(for: [T].self)
59 | DispatchQueue.main.async {
60 | self.models = models
61 | self.showError = false
62 | }
63 | } catch {
64 | DispatchQueue.main.async {
65 | self.errorMessage = "Failed to load models: \(error.localizedDescription)"
66 | Debug.log(self.errorMessage)
67 | self.showError = true
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 |
75 | // Boilerplate code for SwiftUI:
76 | /*
77 | .alert("Error", isPresented: $modelManager.errorMessage.isNotNil()) {
78 | Button("OK", role: .cancel) { }
79 | }
80 | message: {
81 | if let errorMessage = modelManager.errorMessage {
82 | Text(errorMessage)
83 | }
84 | }
85 |
86 | if modelManager.showError {
87 | Button("Retry") {
88 | modelManager.loadModels()
89 | }
90 | }
91 | */
92 |
--------------------------------------------------------------------------------
/SwiftDiffusion/ScriptManager/ScriptManager/ScriptState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScriptState.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/3/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum ScriptState: Equatable {
11 | case readyToStart
12 | case launching
13 | case active
14 | case isTerminating
15 | case terminated
16 | case unableToLocateScript
17 | //case error
18 | }
19 |
20 | extension ScriptManager {
21 | var scriptStateText: String {
22 | switch scriptState {
23 | case .readyToStart:
24 | return "Ready to start"
25 | case .launching:
26 | return "Launching service..."
27 | case .active:
28 | if let urlString = self.serviceUrl?.absoluteString {
29 | return "Active (\(urlString.replacingOccurrences(of: "http://", with: "")))"
30 | } else {
31 | Debug.log("Unable to get absoluteString of '\(String(describing: self.serviceUrl))'")
32 | return "Active"
33 | }
34 | case .isTerminating:
35 | return "Terminating..."
36 | case .terminated:
37 | return "Terminated"
38 | case .unableToLocateScript:
39 | return "Error: Unable to start script"
40 | }
41 | }
42 | }
43 |
44 | extension ScriptState {
45 | var statusColor: Color {
46 | switch self {
47 | case .readyToStart: return Color.gray
48 | case .launching: return Color.yellow
49 | case .active: return Color.green
50 | case .isTerminating: return Color.yellow
51 | case .terminated: return Color.red
52 | case .unableToLocateScript: return Color.red
53 | }
54 | }
55 | var isActive: Bool {
56 | if case .active = self {
57 | return true
58 | } else {
59 | return false
60 | }
61 | }
62 |
63 | var isAwaitingProcessToPlayOut: Bool {
64 | switch self {
65 | case .readyToStart: return false
66 | case .launching: return true
67 | case .active: return false
68 | case .isTerminating: return true
69 | case .terminated: return true
70 | case .unableToLocateScript: return false
71 | }
72 | }
73 |
74 | var isStartable: Bool {
75 | switch self {
76 | case .readyToStart: return true
77 | case .launching: return false
78 | case .active: return false
79 | case .isTerminating: return false
80 | case .terminated: return true
81 | case .unableToLocateScript: return true
82 | }
83 | }
84 |
85 | var isTerminatable: Bool {
86 | switch self {
87 | case .readyToStart: return false
88 | case .launching: return true
89 | case .active: return true
90 | case .isTerminating: return false
91 | case .terminated: return false
92 | case .unableToLocateScript: return false
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarFolderView/SidebarStoredItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SidebarStoredItemView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/26/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Represents a view for displaying stored sidebar items, including handling for small and large previews.
11 | /// This view dynamically selects and presents thumbnail or preview images based on user interaction toggles.
12 | /// - Parameters:
13 | /// - item: The `SidebarItem` to display, including all associated image and model information.
14 | /// - smallPreviewsButtonToggled: A Boolean value indicating whether small previews are enabled.
15 | /// - largePreviewsButtonToggled: A Boolean value indicating whether large previews are enabled.
16 | /// - modelNameButtonToggled: A Boolean value indicating whether the model name display is enabled.
17 | struct SidebarStoredItemView: View {
18 | let item: SidebarItem
19 |
20 | @State private var currentSmallThumbnailImageUrl: URL?
21 | @State private var currentLargeImageUrl: URL?
22 |
23 | @EnvironmentObject var sidebarModel: SidebarModel
24 |
25 | var body: some View {
26 | HStack(alignment: .center, spacing: 0) {
27 | if sidebarModel.smallPreviewsButtonToggled, let thumbnailUrl = thumbnailInfo?.url {
28 | CachedThumbnailImageView(imageUrl: thumbnailUrl, width: 70, height: 70)
29 | .clipShape(RoundedRectangle(cornerRadius: 8))
30 | .shadow(color: .black, radius: 1, x: 0, y: 1)
31 | .padding(.trailing, 8)
32 | }
33 |
34 | VStack(alignment: .leading) {
35 | if sidebarModel.largePreviewsButtonToggled, let largePreviewInfo = previewInfo {
36 | CachedPreviewImageView(imageInfo: largePreviewInfo)
37 | .scaledToFill()
38 | .frame(width: sidebarModel.currentWidth)
39 | .clipShape(RoundedRectangle(cornerRadius: 12))
40 | .shadow(color: .black, radius: 1, x: 0, y: 1)
41 | .padding(.bottom, 8)
42 | }
43 |
44 | Text(item.title)
45 | .lineLimit(2)
46 |
47 | if sidebarModel.modelNameButtonToggled, let modelName = item.prompt?.selectedModel?.name {
48 | Text(modelName)
49 | .font(.system(size: 10, weight: .light, design: .monospaced))
50 | .foregroundStyle(Color.secondary)
51 | .padding(.top, 1)
52 | }
53 | }
54 | .frame(maxWidth: .infinity, alignment: .leading)
55 | }
56 | }
57 |
58 | var thumbnailInfo: ImageInfo? {
59 | item.imageThumbnails.first(where: { $0.url.lastPathComponent.contains("-grid") })
60 | ?? item.imageThumbnails.last
61 | }
62 |
63 | var previewInfo: ImageInfo? {
64 | item.imagePreviews.first(where: { $0.url.lastPathComponent.contains("-grid") })
65 | ?? item.imagePreviews.last
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/SwiftDiffusion/App/Debug.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Debug.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 |
10 | let CanvasPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
11 |
12 | /// A debugging utility class that conditionally performs logging and actions based on its active state.
13 | ///
14 | /// This class provides a global access point through `Debug.shared` to perform logging and execute closures conditionally if debugging is enabled.
15 | class Debug: ObservableObject {
16 | /// The singleton instance for global access.
17 | static let shared = Debug()
18 |
19 | /// Indicates whether debugging actions should be performed.
20 | @Published var isActive = true
21 |
22 | /// Logs a given value to the console if debugging is active.
23 | ///
24 | /// Use this instance method to log any value (e.g., String, Int) when debugging is enabled.
25 | ///
26 | /// - Parameter value: The value to be logged.
27 | ///
28 | /// **Usage:**
29 | ///
30 | /// ```swift
31 | /// Debug.shared.logInstance("Debugging started")
32 | /// ```
33 | ///
34 | /// - important: Use `Debug.log` instead.
35 | private func logInstance(_ value: T) {
36 | if isActive {
37 | print(value)
38 | }
39 | }
40 |
41 | /// Logs a given value to the console if debugging is active.
42 | ///
43 | /// Use this static method to log any value conveniently without needing direct access to the `Debug` singleton.
44 | ///
45 | /// - Parameter value: The value to be logged.
46 | ///
47 | /// **Usage:**
48 | ///
49 | /// ```swift
50 | /// Debug.log("User login attempt")
51 | /// ```
52 | static func log(_ value: T) {
53 | Debug.shared.logInstance(value)
54 | }
55 | /// Executes a closure if debugging is active.
56 | ///
57 | /// This instance method conditionally performs the given action allowing for execution of debugging-specific tasks.
58 | ///
59 | /// - Parameter action: A closure to be executed if debugging is active.
60 | ///
61 | /// **Usage:**
62 | ///
63 | /// ```swift
64 | /// Debug.shared.perform {
65 | /// print("Performing an action specific to debugging.")
66 | /// }
67 | /// ```
68 | ///
69 | /// - important: Use `Debug.perform` instead
70 | private func perform(action: () -> Void) {
71 | if isActive {
72 | action()
73 | }
74 | }
75 |
76 | /// Executes a closure if debugging is active.
77 | ///
78 | /// This static method provides a convenient way to execute debugging-specific tasks without needing direct access to the `Debug` singleton.
79 | ///
80 | /// - Parameter action: A closure to be executed if debugging is active.
81 | ///
82 | /// **Usage:**
83 | ///
84 | /// ```swift
85 | /// Debug.perform {
86 | /// print("Performing an action specific to debugging.")
87 | /// }
88 | /// ```
89 | static func perform(action: @escaping () -> Void) {
90 | Debug.shared.perform(action: action)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/PreviewImageProcessing/CachedPreviewImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CachedPreviewImageView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/26/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A view responsible for displaying an image, either from cache or by loading it asynchronously.
11 | /// This view supports displaying a placeholder until the image is loaded and caches the image once loaded.
12 | /// - Parameters:
13 | /// - imageInfo: An optional `ImageInfo` object containing the image URL and its dimensions. Used for calculating aspect ratio and size if available.
14 | struct CachedPreviewImageView: View {
15 | @EnvironmentObject var sidebarModel: SidebarModel
16 | let imageInfo: ImageInfo?
17 |
18 | @State private var displayedImage: NSImage?
19 |
20 | var body: some View {
21 | Group {
22 | if let displayedImage = displayedImage {
23 | Image(nsImage: displayedImage)
24 | .resizable()
25 | .scaledToFit()
26 | .frame(width: calculateWidth(), height: calculateHeight())
27 | .clipped()
28 | .clipShape(RoundedRectangle(cornerRadius: 12))
29 | } else {
30 | Rectangle()
31 | .foregroundColor(Color.gray.opacity(0.3))
32 | .frame(width: calculateWidth(), height: calculateHeight())
33 | }
34 | }
35 | .onAppear(perform: loadImage)
36 | }
37 |
38 | /// Calculates the width of the image to be displayed, based on the current width available in the sidebar view model.
39 | /// - Returns: The calculated width as a `CGFloat`.
40 | private func calculateWidth() -> CGFloat {
41 | return sidebarModel.currentWidth
42 | }
43 |
44 | /// Calculates the height of the image to be displayed, utilizing the aspect ratio defined in `ImageInfo` if available.
45 | /// If no `ImageInfo` is available, defaults to a square based on the width.
46 | /// - Returns: The calculated height as a `CGFloat`.
47 | private func calculateHeight() -> CGFloat {
48 | guard let imageInfo = imageInfo else {
49 | return calculateWidth()
50 | }
51 | let aspectRatio = imageInfo.height / max(imageInfo.width, 1)
52 | return calculateWidth() * aspectRatio
53 | }
54 |
55 | /// Loads the image either from the cache or by fetching it asynchronously if not present in the cache.
56 | /// Upon successful loading, the image is cached for future use.
57 | private func loadImage() {
58 | guard let imageUrl = imageInfo?.url else {
59 | return
60 | }
61 |
62 | if let cachedImage = ImageCache.shared.image(forKey: imageUrl.path) {
63 | displayedImage = cachedImage
64 | return
65 | }
66 |
67 | DispatchQueue.global(qos: .userInitiated).async {
68 | if let image = NSImage(contentsOf: imageUrl) {
69 | DispatchQueue.main.async {
70 | ImageCache.shared.setImage(image, forKey: imageUrl.path)
71 | self.displayedImage = image
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PromptView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import SwiftUI
9 | import Combine
10 | import CompactSlider
11 |
12 | extension Constants.Layout {
13 | static let promptRowPadding: CGFloat = 16
14 | }
15 |
16 | struct PromptView: View {
17 | @Environment(\.modelContext) var modelContext
18 | @EnvironmentObject var sidebarModel: SidebarModel
19 | @EnvironmentObject var currentPrompt: PromptModel
20 | @EnvironmentObject var checkpointsManager: CheckpointsManager
21 | @EnvironmentObject var loraModelsManager: ModelManager
22 | @EnvironmentObject var vaeModelsManager: ModelManager
23 |
24 | @ObservedObject var scriptManager = ScriptManager.shared
25 | @ObservedObject var userSettings = UserSettings.shared
26 |
27 | @State var isRightPaneVisible: Bool = false
28 | @State var generationDataInPasteboard: Bool = false
29 |
30 | private var leftPane: some View {
31 | VStack(spacing: 0) {
32 |
33 | DebugPromptStatusView()
34 |
35 | PromptControlBarView()
36 |
37 | ScrollView {
38 | Form {
39 | HStack {
40 | CheckpointMenu()
41 | SamplingMethodMenu()
42 | }
43 | .padding(.vertical, 12)
44 |
45 | VStack {
46 | PromptEditorView(label: "Positive Prompt", text: $currentPrompt.positivePrompt)
47 | PromptEditorView(label: "Negative Prompt", text: $currentPrompt.negativePrompt)
48 | }
49 | .padding(.bottom, 6)
50 |
51 | DimensionSelectionRow(width: $currentPrompt.width, height: $currentPrompt.height)
52 |
53 | DetailSelectionRow(cfgScale: $currentPrompt.cfgScale, samplingSteps: $currentPrompt.samplingSteps)
54 |
55 | HalfSkipClipRow(clipSkip: $currentPrompt.clipSkip)
56 |
57 | SeedRow(seed: $currentPrompt.seed, controlButtonLayout: .beside)
58 |
59 | ExportSelectionRow(batchCount: $currentPrompt.batchCount, batchSize: $currentPrompt.batchSize)
60 |
61 | VaeModelMenu()
62 | }
63 | .padding(.leading, 8).padding(.trailing, 16)
64 | .disabled(sidebarModel.disablePromptView)
65 | }
66 | .scrollBounceBehavior(.basedOnSize)
67 |
68 | DebugPromptActionView(scriptManager: scriptManager)
69 | }
70 | .background(Color(NSColor.windowBackgroundColor))
71 | }
72 |
73 | private var rightPane: some View {
74 | ConsoleView(scriptManager: scriptManager)
75 | .background(Color(NSColor.windowBackgroundColor))
76 |
77 | }
78 |
79 | var body: some View {
80 | HSplitView {
81 | leftPane
82 | .frame(minWidth: 370)
83 | if isRightPaneVisible {
84 | rightPane
85 | .frame(minWidth: 370)
86 | }
87 | }
88 | }
89 | }
90 |
91 | #Preview {
92 | CommonPreviews.promptView
93 | .frame(width: 600, height: 800)
94 | }
95 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/SidebarFolderView/FolderTitleControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FolderTitleControl.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/29/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FolderTitleControl: View {
11 | @Environment(\.modelContext) private var modelContext
12 | @EnvironmentObject var sidebarModel: SidebarModel
13 | let folder: SidebarFolder?
14 | @State private var isEditing: Bool = false
15 | @State private var editableName: String = ""
16 |
17 | func storeChanges() {
18 | isEditing = false
19 | sidebarModel.currentFolder?.name = editableName
20 | sidebarModel.saveData(in: modelContext)
21 | }
22 |
23 | var body: some View {
24 | Divider()
25 |
26 | HStack(spacing: 0) {
27 | if isEditing {
28 | TextField("Untitled", text: $editableName, onCommit: {
29 | storeChanges()
30 | })
31 | .textFieldStyle(RoundedBorderTextFieldStyle())
32 | } else {
33 | Text(sidebarModel.currentFolder?.name ?? "Untitled")
34 | .fontWeight(.medium)
35 | .onTapGesture {
36 | isEditing = true
37 | editableName = sidebarModel.currentFolder?.name ?? ""
38 | }
39 | }
40 |
41 | Spacer()
42 |
43 | Group {
44 | if isEditing {
45 | AccessorySymbolButton(symbol: .checkmark, action: {
46 | isEditing = false
47 | sidebarModel.currentFolder?.name = editableName
48 | sidebarModel.saveData(in: modelContext)
49 | })
50 | } else {
51 | AccessorySymbolButton(symbol: .pencil, action: {
52 | isEditing = true
53 | editableName = sidebarModel.currentFolder?.name ?? ""
54 | })
55 | }
56 |
57 | TrashFolderButton(folder: folder)
58 | .padding(.leading, 2)
59 | }
60 | .frame(minWidth: 0)
61 | }
62 | .listRowInsets(EdgeInsets())
63 | }
64 | }
65 |
66 | struct TrashFolderButton: View {
67 | @Environment(\.modelContext) private var modelContext
68 | @EnvironmentObject var sidebarModel: SidebarModel
69 | @State private var showDeleteFolderConfirmationAlert: Bool = false
70 |
71 | let folder: SidebarFolder?
72 | var body: some View {
73 | AccessorySymbolButton(symbol: .trash, action: {
74 | if let folder = folder, folder.folders.isEmpty && folder.items.isEmpty {
75 | deleteFolder()
76 | } else {
77 | showDeleteFolderConfirmationAlert = true
78 | }
79 | })
80 | .alert(isPresented: $showDeleteFolderConfirmationAlert) {
81 | Alert(
82 | title: Text("Are you sure you want to delete this folder?"),
83 | message: Text("All of its folders and items will be lost."),
84 | primaryButton: .destructive(Text("Delete")) {
85 | deleteFolder()
86 | },
87 | secondaryButton: .cancel()
88 | )
89 | }
90 | }
91 |
92 | func deleteFolder() {
93 | if let folder = folder, let parentFolder = sidebarModel.currentFolder?.parent {
94 | withAnimation {
95 | sidebarModel.deleteFolder(folder, from: parentFolder)
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Services/PastableService/PastableService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PastableService.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 3/7/24.
6 | //
7 |
8 | import Foundation
9 | import AppKit
10 |
11 | extension Constants.Parsing {
12 | static let civitaiTags = ["Negative prompt:", "Steps:", "Seed:", "Sampler:", "CFG scale:", "Clip skip:", "Model:", "Model hash:", "VAE:"]
13 | }
14 |
15 | class PastableService: ObservableObject {
16 | static let shared = PastableService()
17 |
18 | @Published var canPasteData: Bool = false
19 | @Published var pastablePromptData: StoredPromptModel? = nil
20 |
21 | func newWorkspaceItemFromParsedPasteboard(sidebarModel: SidebarModel?, checkpoints: [CheckpointModel], vaeModels: [VaeModel]) {
22 | guard let sidebarModel = sidebarModel else {
23 | Debug.log("[PastableService] newWorkspaceItemFromPasteboard")
24 | return
25 | }
26 |
27 | if let pastablePromptData = parsePasteboard(checkpoints: checkpoints, vaeModels: vaeModels) {
28 | sidebarModel.createNewWorkspaceItem(withPrompt: pastablePromptData)
29 | }
30 | clearPasteboard()
31 | canPasteData = false
32 | }
33 |
34 | func parsePasteboard(checkpoints: [CheckpointModel], vaeModels: [VaeModel]) -> StoredPromptModel? {
35 | guard let pasteboardContent = getPasteboardString() else { return nil }
36 |
37 | if canPasteData {
38 | let parseCivitai = ParseCivitai(checkpoints: checkpoints, vaeModels: vaeModels)
39 | let storedPromptModel = parseCivitai.parsePastablePromptModel(pasteboardContent)
40 | return storedPromptModel
41 | }
42 | return nil
43 | }
44 |
45 |
46 | private init() {}
47 |
48 | /// Checks the pasteboard asynchronously for pastable data
49 | func checkForPastableData() async {
50 | guard let pasteboardContent = getPasteboardString() else { return }
51 | let dataFound = await didFindGenerationDataTags(from: pasteboardContent)
52 | await updateCanPasteData(dataFound)
53 | }
54 |
55 | /// Updates the `canPasteData` property on the main actor to ensure it's on the main thread.
56 | @MainActor
57 | func updateCanPasteData(_ newValue: Bool) {
58 | canPasteData = newValue
59 | }
60 |
61 | func clearPasteboard() {
62 | NSPasteboard.general.clearContents()
63 | }
64 |
65 | /// Returns the string currently stored in the system's pasteboard, if available.
66 | func getPasteboardString() -> String? {
67 | NSPasteboard.general.string(forType: .string)
68 | }
69 |
70 | /// Asynchronously determines if the pasteboard content contains generation data by looking for specific keywords.
71 | func didFindGenerationDataTags(from pasteboardContent: String) async -> Bool {
72 | await withTaskGroup(of: Bool.self, returning: Bool.self) { group in
73 | let keywords = Constants.Parsing.civitaiTags
74 |
75 | for keyword in keywords {
76 | group.addTask {
77 | pasteboardContent.contains(keyword)
78 | }
79 | }
80 |
81 | var foundKeywords = 0
82 | for await containsKeyword in group {
83 | if containsKeyword {
84 | foundKeywords += 1
85 | if foundKeywords >= 2 {
86 | return true
87 | }
88 | }
89 | }
90 |
91 | return false
92 | }
93 | }
94 |
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/SwiftDiffusion/ScriptManager/PythonProcess/PythonProcess.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PythonProcess.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol PythonProcessDelegate: AnyObject {
11 | func pythonProcessDidUpdateOutput(output: String)
12 | func pythonProcessDidFinishRunning(with result: ScriptResult)
13 | }
14 |
15 | /// Manages execution of external Python processes.
16 | class PythonProcess {
17 | private var process: Process?
18 | private var outputPipe: Pipe?
19 | private var errorPipe: Pipe?
20 |
21 | weak var delegate: PythonProcessDelegate?
22 |
23 | /// Initializes a new PythonProcess.
24 | init() { }
25 |
26 | /// Sets up and starts a process with given script directory, name and optional overriding arguments..
27 | /// - Parameters:
28 | /// - scriptDirectory: The directory where the script is located.
29 | /// - scriptName: The name of the script to be executed.
30 | /// - arguments: Override with custom arguments.
31 | ///
32 | /// - important: If overriding default arguments, you must include: `cd \(scriptDirectory); ./\(scriptName)`
33 | func runScript(at path: String, scriptName: String, arguments: [String] = []) {
34 | let process = Process()
35 | let outputPipe = Pipe()
36 | let errorPipe = Pipe()
37 |
38 | self.process = process
39 | self.outputPipe = outputPipe
40 | self.errorPipe = errorPipe
41 |
42 | process.executableURL = Constants.CommandLine.zshUrl
43 | let scriptDirectory = path
44 | let command = arguments.isEmpty ? Constants.CommandLine.defaultCommand(scriptDirectory, scriptName) : arguments.joined(separator: " ")
45 | process.arguments = ["-c", command]
46 | process.standardOutput = outputPipe
47 | process.standardError = errorPipe
48 |
49 | setupOutputHandling()
50 |
51 | do {
52 | try process.run()
53 | } catch {
54 | delegate?.pythonProcessDidFinishRunning(with: .failure(error))
55 | }
56 | }
57 |
58 | /// Sets up handlers for process output and error streams.
59 | private func setupOutputHandling() {
60 | outputPipe?.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
61 | let data = fileHandle.availableData
62 | guard !data.isEmpty, let output = String(data: data, encoding: .utf8) else { return }
63 | self?.delegate?.pythonProcessDidUpdateOutput(output: output)
64 | }
65 |
66 | errorPipe?.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
67 | let data = fileHandle.availableData
68 | guard !data.isEmpty, let output = String(data: data, encoding: .utf8) else { return }
69 | self?.delegate?.pythonProcessDidUpdateOutput(output: output)
70 | }
71 |
72 | process?.terminationHandler = { [weak self] _ in
73 | self?.delegate?.pythonProcessDidFinishRunning(with: .success("Script finished running"))
74 | }
75 | }
76 |
77 | /// Terminate the PythonProcess.
78 | func terminate() {
79 | process?.terminate()
80 | clearProcessAndPipes()
81 | }
82 |
83 | private func clearProcessAndPipes() {
84 | outputPipe?.fileHandleForReading.readabilityHandler = nil
85 | errorPipe?.fileHandleForReading.readabilityHandler = nil
86 | process = nil
87 | outputPipe = nil
88 | errorPipe = nil
89 | }
90 |
91 | deinit {
92 | clearProcessAndPipes()
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Services/FilePickerService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilePickerService.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/5/24.
6 | //
7 |
8 | import Foundation
9 | import AppKit
10 | import UniformTypeIdentifiers
11 |
12 | extension Constants.FileTypes {
13 | static let shellExtension = "sh"
14 | static let shellScriptType = UTType(filenameExtension: shellExtension)
15 | }
16 |
17 | struct FilePickerService {
18 | /// Presents an open panel dialog allowing the user to select a shell script file.
19 | ///
20 | /// This function asynchronously displays a file picker dialog configured to allow the selection of files with the `.sh` extension only. It ensures that the user cannot choose directories or multiple files. If the user selects a file and confirms, the function returns the path to the selected file. If the user cancels the dialog or selects a file of an incorrect type, the function returns `nil`.
21 | ///
22 | /// - Returns: A `String` representing the path to the selected `.sh` file, or `nil` if no file is selected or the operation is cancelled.
23 | @MainActor
24 | static func browseForShellFile() async -> String? {
25 | return await withCheckedContinuation { continuation in
26 | let panel = NSOpenPanel()
27 | panel.allowsMultipleSelection = false
28 | panel.canChooseDirectories = false
29 |
30 | if let shellScriptType = Constants.FileTypes.shellScriptType {
31 | panel.allowedContentTypes = [shellScriptType]
32 | } else {
33 | Debug.log("Failed to find UTType for .\(Constants.FileTypes.shellExtension) files")
34 | continuation.resume(returning: nil)
35 | return
36 | }
37 |
38 | panel.begin { response in
39 | if response == .OK, let url = panel.urls.first {
40 | if url.pathExtension == Constants.FileTypes.shellExtension {
41 | continuation.resume(returning: url.path)
42 | } else {
43 | Debug.log("Error: Selected file is not a .\(Constants.FileTypes.shellExtension) shell script file.\n > \(url)")
44 | continuation.resume(returning: nil)
45 | }
46 | }
47 | }
48 | }
49 | }
50 | /// Presents an open panel dialog allowing the user to select a directory.
51 | ///
52 | /// This function asynchronously displays a file picker dialog configured to allow the selection of directories only. It ensures that the user cannot choose files or multiple directories. If the user selects a directory and confirms, the function returns the path to the selected directory. If the user cancels the dialog or selects an incorrect type, the function returns `nil`.
53 | ///
54 | /// - Returns: A `String` representing the path to the selected directory, or `nil` if no directory is selected or the operation is cancelled.
55 | @MainActor
56 | static func browseForDirectory() async -> String? {
57 | return await withCheckedContinuation { continuation in
58 | let panel = NSOpenPanel()
59 | panel.allowsMultipleSelection = false
60 | panel.canChooseDirectories = true
61 | panel.canCreateDirectories = true
62 | panel.canChooseFiles = false
63 |
64 | panel.begin { response in
65 | if response == .OK, let url = panel.urls.first {
66 | continuation.resume(returning: url.path)
67 | } else {
68 | continuation.resume(returning: nil)
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Models/StoredModels/StoredCheckpointModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoredCheckpointModel.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/27/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | @Model
12 | class StoredCheckpointModel {
13 | @Attribute var name: String
14 | @Attribute var path: String
15 | @Attribute var type: StoredCheckpointModelType
16 | @Attribute var storedCheckpointApiModel: StoredCheckpointApiModel?
17 |
18 | init(name: String, path: String, type: StoredCheckpointModelType, storedCheckpointApiModel: StoredCheckpointApiModel? = nil) {
19 | self.name = name
20 | self.path = path
21 | self.type = type
22 | self.storedCheckpointApiModel = storedCheckpointApiModel
23 | }
24 | }
25 |
26 | enum StoredCheckpointModelType: String, Codable {
27 | case coreMl = "coreMl"
28 | case python = "python"
29 | }
30 |
31 | extension MapModelData {
32 | func toStoredCheckpoint(from checkpointModel: CheckpointModel?) -> StoredCheckpointModel? {
33 | guard let checkpointModel = checkpointModel else { return nil }
34 | let storedCheckpointModelType = toStoredCheckpointModelType(from: checkpointModel.type)
35 | let storedCheckpointApiModel = toStoredCheckpointApiModel(from: checkpointModel.checkpointApiModel)
36 | return StoredCheckpointModel(name: checkpointModel.name,
37 | path: checkpointModel.path,
38 | type: storedCheckpointModelType,
39 | storedCheckpointApiModel: storedCheckpointApiModel
40 | )
41 | }
42 |
43 |
44 | @MainActor
45 | func toStoredCheckpointModel(from checkpointModel: CheckpointModel?) -> StoredCheckpointModel? {
46 | guard let checkpointModel = checkpointModel else { return nil }
47 | let storedCheckpointModelType = toStoredCheckpointModelType(from: checkpointModel.type)
48 | let storedCheckpointApiModel = toStoredCheckpointApiModel(from: checkpointModel.checkpointApiModel)
49 | return StoredCheckpointModel(name: checkpointModel.name,
50 | path: checkpointModel.path,
51 | type: storedCheckpointModelType,
52 | storedCheckpointApiModel: storedCheckpointApiModel
53 | )
54 | }
55 |
56 | @MainActor
57 | func toCheckpointModel(from storedCheckpointModel: StoredCheckpointModel?) -> CheckpointModel? {
58 | guard let storedCheckpointModel = storedCheckpointModel else { return nil }
59 | let checkpointModelType = toCheckpointModelType(from: storedCheckpointModel.type)
60 | let checkpointApiModel = toCheckpointApiModel(from: storedCheckpointModel.storedCheckpointApiModel)
61 |
62 | return CheckpointModel(name: storedCheckpointModel.name,
63 | path: storedCheckpointModel.path,
64 | type: checkpointModelType,
65 | checkpointApiModel: checkpointApiModel
66 | )
67 | }
68 |
69 | func toStoredCheckpointModelType(from type: CheckpointModelType) -> StoredCheckpointModelType {
70 | switch type {
71 | case .coreMl: return .coreMl
72 | case .python: return .python
73 | }
74 | }
75 |
76 | func toCheckpointModelType(from type: StoredCheckpointModelType) -> CheckpointModelType {
77 | switch type {
78 | case .coreMl: return .coreMl
79 | case .python: return .python
80 | }
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Constants.WindowSize {
11 | struct Settings {
12 | static let defaultWidth: CGFloat = 670
13 | static let defaultHeight: CGFloat = 700
14 | }
15 | }
16 |
17 | struct SettingsView: View {
18 | @ObservedObject var userSettings = UserSettings.shared
19 |
20 | @Environment(\.presentationMode) var presentationMode
21 |
22 | var openWithTab: SettingsTab? = nil
23 |
24 | @State var selectedTab: SettingsTab = {
25 | let savedValue = UserDefaults.standard.string(forKey: "selectedSettingsTab") ?? ""
26 | return SettingsTab(rawValue: savedValue) ?? .prompt //.engine
27 | }()
28 |
29 | var body: some View {
30 | VStack(spacing: 0) {
31 | ScrollView {
32 |
33 | SettingsSectionHeader(userSettings: userSettings, selectedTab: selectedTab)
34 |
35 | VStack(alignment: .leading) {
36 | switch selectedTab {
37 | case .general:
38 | GeneralSection(userSettings: userSettings)
39 | case .files:
40 | FilesSection(userSettings: userSettings)
41 | case .prompt:
42 | PromptSection(userSettings: userSettings)
43 | case .engine:
44 | EngineSection(userSettings: userSettings)
45 | case .developer:
46 | DeveloperSection(userSettings: userSettings)
47 | }
48 | }
49 | .padding(.vertical)
50 | .padding(.horizontal, 14)
51 | }
52 | .frame(maxHeight: .infinity)
53 |
54 | VStack {
55 | HStack {
56 | OutlineButton(title: "Restore Defaults") {
57 | userSettings.restoreDefaults()
58 | }
59 | Spacer()
60 | BlueButton(title: "Done") {
61 | presentationMode.wrappedValue.dismiss()
62 | }
63 | }
64 | .padding(10)
65 | }
66 | .background(Color(NSColor.windowBackgroundColor))
67 | }
68 | .frame(minWidth: 615, idealWidth: Constants.WindowSize.Settings.defaultWidth, maxWidth: 900,
69 | minHeight: 300, idealHeight: Constants.WindowSize.Settings.defaultWidth, maxHeight: .infinity)
70 | .frame(maxWidth: .infinity, maxHeight: .infinity)
71 | .toolbar {
72 | ToolbarItemGroup(placement: .automatic) {
73 | HStack {
74 |
75 | ForEach(SettingsTab.allCases, id: \.self) { tab in
76 | Button(action: {
77 | self.selectedTab = tab
78 | }) {
79 | VStack {
80 | Image(systemName: tab.symbol)
81 | .padding(.bottom, 0.1)
82 | Text(tab.rawValue)
83 | .font(.system(size: 10))
84 | .fixedSize(horizontal: false, vertical: true)
85 | }
86 | }
87 | .buttonStyle(ToolbarTabButtonStyle(isSelected: selectedTab == tab))
88 | }
89 |
90 | }
91 | }
92 | }
93 | .onChange(of: selectedTab) {
94 | UserDefaults.standard.set(selectedTab.rawValue, forKey: "selectedSettingsTab")
95 | }
96 | .onAppear {
97 | if let tab = openWithTab {
98 | selectedTab = tab
99 | }
100 | }
101 | }
102 | }
103 |
104 | #Preview {
105 | SettingsView()
106 | .frame(width: 600, height: 700)
107 | }
108 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Observers/ScriptManagerObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScriptManagerObserver.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/17/24.
6 | //
7 |
8 | import SwiftUI
9 | import Combine
10 |
11 | @MainActor
12 | class ScriptManagerObserver {
13 | var scriptManager: ScriptManager
14 | var userSettings: UserSettings
15 | var checkpointsManager: CheckpointsManager
16 | var loraModelsManager: ModelManager
17 | var vaeModelsManager: ModelManager
18 |
19 | private var cancellables: Set = []
20 |
21 | init(scriptManager: ScriptManager, userSettings: UserSettings, checkpointsManager: CheckpointsManager, loraModelsManager: ModelManager, vaeModelsManager: ModelManager) {
22 | self.scriptManager = scriptManager
23 | self.userSettings = userSettings
24 | self.checkpointsManager = checkpointsManager
25 | self.loraModelsManager = loraModelsManager
26 | self.vaeModelsManager = vaeModelsManager
27 |
28 | setupObservers()
29 | }
30 |
31 | private func setupObservers() {
32 | scriptManager.$scriptState
33 | .print("scriptState Stream")
34 | .sink { [weak self] newState in
35 | self?.scriptStateDidChange(newState)
36 | }
37 | .store(in: &cancellables)
38 |
39 | userSettings.$stableDiffusionModelsPath
40 | .sink { [weak self] newPath in
41 | self?.stableDiffusionModelsPathDidChange(newPath)
42 | }
43 | .store(in: &cancellables)
44 |
45 | userSettings.$loraDirectoryPath
46 | .sink { [weak self] newPath in
47 | self?.loraDirectoryPathDidChange(newPath)
48 | }
49 | .store(in: &cancellables)
50 |
51 | userSettings.$vaeDirectoryPath
52 | .sink { [weak self] newPath in
53 | self?.vaeDirectoryPathDidChange(newPath)
54 | }
55 | .store(in: &cancellables)
56 | }
57 |
58 |
59 | private func scriptStateDidChange(_ newState: ScriptState) {
60 | Debug.log("[ScriptManagerObserver] scriptStateDidChange newState: \(newState)")
61 | if newState.isActive {
62 | Debug.log("[ScriptManagerObserver] newState.isActive")
63 | checkpointsManager.startObservingDirectory()
64 | loraModelsManager.startObservingDirectory()
65 | vaeModelsManager.startObservingDirectory()
66 |
67 | if let serviceUrl = scriptManager.serviceUrl {
68 | checkpointsManager.configureApiManager(with: serviceUrl.absoluteString)
69 | }
70 |
71 | } else {
72 | checkpointsManager.stopObservingDirectory()
73 | loraModelsManager.stopObservingDirectory()
74 | vaeModelsManager.stopObservingDirectory()
75 | }
76 | }
77 |
78 | private func stableDiffusionModelsPathDidChange(_ newPath: String) {
79 | Debug.log("stableDiffusionModelsPathDidChange newPath: \(newPath)")
80 | checkpointsManager.stopObservingDirectory()
81 | checkpointsManager.startObservingDirectory()
82 | }
83 |
84 | private func loraDirectoryPathDidChange(_ newPath: String = "") {
85 | Debug.log("loraDirectoryPathDidChange newPath: \(newPath)")
86 | loraModelsManager.stopObservingDirectory()
87 | loraModelsManager.startObservingDirectory()
88 | }
89 |
90 | private func vaeDirectoryPathDidChange(_ newPath: String = "") {
91 | Debug.log("vaeDirectoryPathDidChange newPath: \(newPath)")
92 | vaeModelsManager.stopObservingDirectory()
93 | vaeModelsManager.startObservingDirectory()
94 | }
95 |
96 | deinit {
97 | Debug.log("ScriptManagerObserver is being deinitialized")
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/DebugPromptViews/DebugPromptActionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DebugPromptActionView.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DebugPromptActionView: View {
11 | @EnvironmentObject var currentPrompt: PromptModel
12 | @EnvironmentObject var checkpointsManager: CheckpointsManager
13 | @EnvironmentObject var loraModelsManager: ModelManager
14 |
15 | @ObservedObject var scriptManager = ScriptManager.shared
16 | @ObservedObject var userSettings = UserSettings.shared
17 |
18 | @State var consoleOutput: String = ""
19 |
20 | func setNewConsoleOutputText(_ output: String) {
21 | consoleOutput = output
22 | Debug.log(output)
23 | }
24 |
25 | var body: some View {
26 | if userSettings.showDeveloperInterface {
27 | HStack {
28 | Spacer()
29 | VStack(alignment: .leading) {
30 |
31 | if !consoleOutput.isEmpty {
32 | HStack {
33 | Spacer()
34 | Button(action: {
35 | setNewConsoleOutputText("")
36 | }) {
37 | Text("Clear Console")
38 | .font(.system(size: 10))
39 | }
40 | .buttonStyle(.accessoryBar)
41 | }
42 | HStack {
43 | TextEditor(text: $consoleOutput)
44 | .font(.system(size: 9, design: .monospaced))
45 | .frame(minHeight: 10, idealHeight: 30, maxHeight: 80)
46 | }
47 | .padding(.vertical, 2)
48 | }
49 |
50 | HStack {
51 | Spacer()
52 | Button("Log Prompt") {
53 | logPromptProperties()
54 | }
55 | .padding(.trailing, 6)
56 |
57 | Button("Load Models") {
58 | Debug.log("NO FUNCTION")
59 | //Task { await checkpointsManager.loadModels() }
60 | }
61 | .padding(.trailing, 6)
62 |
63 | Spacer()
64 | }
65 | }
66 | .padding(.horizontal)
67 | .font(.system(size: 12, design: .monospaced))
68 | .foregroundColor(Color.white)
69 | Spacer()
70 | }
71 | .padding(.vertical, 6).padding(.bottom, 2)
72 | .background(Color.black)
73 | }
74 | }
75 | }
76 |
77 |
78 | #Preview {
79 | CommonPreviews.promptView
80 | }
81 |
82 |
83 | #Preview {
84 | DebugPromptActionView(scriptManager: ScriptManager.preview(withState: .readyToStart))
85 | .environmentObject(PromptModel())
86 | }
87 |
88 | extension DebugPromptActionView {
89 | func logPromptProperties() {
90 | var debugOutput = ""
91 | debugOutput += "selectedModel: \(currentPrompt.selectedModel?.name ?? "nil")\n"
92 | debugOutput += "samplingMethod: \(currentPrompt.samplingMethod ?? "nil")\n"
93 | debugOutput += "positivePrompt: \(currentPrompt.positivePrompt)\n"
94 | debugOutput += "negativePrompt: \(currentPrompt.negativePrompt)\n"
95 | debugOutput += "width: \(currentPrompt.width)\n"
96 | debugOutput += "height: \(currentPrompt.height)\n"
97 | debugOutput += "cfgScale: \(currentPrompt.cfgScale)\n"
98 | debugOutput += "samplingSteps: \(currentPrompt.samplingSteps)\n"
99 | debugOutput += "seed: \(currentPrompt.seed)\n"
100 | debugOutput += "batchCount: \(currentPrompt.batchCount)\n"
101 | debugOutput += "batchSize: \(currentPrompt.batchSize)\n"
102 | debugOutput += "clipSkip: \(currentPrompt.clipSkip)\n"
103 |
104 | Debug.log(debugOutput)
105 | scriptManager.updateConsoleOutput(with: debugOutput)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/WindowViews/SettingsView/SettingsSections/FilesSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FilesSection.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FilesSection: View {
11 | @ObservedObject var userSettings: UserSettings
12 | @State private var isExpanded: Bool = false
13 |
14 | @State private var showingModifiedAutomaticPathAlert: Bool = false
15 |
16 | var body: some View {
17 | BrowseFileRow(labelText: "Generated image output directory",
18 | placeholderText: "~/Documents/SwiftDiffusion/",
19 | textValue: $userSettings.outputDirectoryPath) {
20 | await FilePickerService.browseForDirectory()
21 | }
22 |
23 | BrowseFileRow(labelText: "Automatic path directory",
24 | placeholderText: "../stable-diffusion-webui/",
25 | textValue: $userSettings.automaticDirectoryPath) {
26 | await FilePickerService.browseForDirectory()
27 | }
28 | .onChange(of: userSettings.automaticDirectoryPath) { newPath, oldPath in
29 | Debug.log("User set new automaticDirectoryPath: \(newPath) from oldPath: \(oldPath) ")
30 | if oldPath.isEmpty {
31 | userSettings.setDefaultPathsForEmptySettings()
32 | } else if newPath != oldPath {
33 | showingModifiedAutomaticPathAlert = true
34 | }
35 | }
36 | .alert(isPresented: $showingModifiedAutomaticPathAlert) {
37 | Alert(
38 | title: Text("New Automatic location"),
39 | message: Text("It looks like the path to your Automatic folder has changed. Would you like for \(Constants.App.name) to update your LoRA, VAE and other related directories for you as well?\n\nIf not, you'll need to set them manually yourself in the 'Custom Automatic Paths' section."),
40 | primaryButton: .default(Text("Update For Me")) {
41 | userSettings.resetDefaultPathsToEmpty()
42 | userSettings.setDefaultPathsForEmptySettings()
43 | },
44 | secondaryButton: .cancel() {
45 | isExpanded = true
46 | }
47 | )
48 | }
49 |
50 | ExpandableSectionHeader(title: "Custom Automatic Paths", isExpanded: $isExpanded)
51 |
52 | if isExpanded {
53 |
54 | BrowseFileRow(labelText: "webui.sh file",
55 | placeholderText: "../stable-diffusion-webui/webui.sh",
56 | textValue: $userSettings.webuiShellPath) {
57 | await FilePickerService.browseForShellFile()
58 | }
59 |
60 | BrowseFileRow(labelText: "Stable diffusion models",
61 | placeholderText: "../stable-diffusion-webui/models/Stable-diffusion/",
62 | textValue: $userSettings.stableDiffusionModelsPath) {
63 | await FilePickerService.browseForDirectory()
64 | }
65 |
66 | BrowseFileRow(labelText: "LoRA models path",
67 | placeholderText: "../stable-diffusion-webui/models/Lora/",
68 | textValue: $userSettings.loraDirectoryPath) {
69 | await FilePickerService.browseForDirectory()
70 | }
71 |
72 | BrowseFileRow(labelText: "VAE models path",
73 | placeholderText: "../stable-diffusion-webui/models/VAE/",
74 | textValue: $userSettings.vaeDirectoryPath) {
75 | await FilePickerService.browseForDirectory()
76 | }
77 | }
78 | }
79 | }
80 |
81 | #Preview {
82 | SettingsView(selectedTab: .files)
83 | }
84 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/Sidebar/PreviewImageProcessing/NSImage+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSImageExtensions.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/26/24.
6 | //
7 |
8 | import AppKit
9 |
10 | extension NSImage {
11 | /// Generates JPEG data from the image after resizing it to a specified maximum dimension and applying JPEG compression.
12 | /// This method first resizes the image to ensure that its largest dimension does not exceed the specified `maxDimension`,
13 | /// maintaining the original aspect ratio. It then converts the resized image to JPEG format with a specified compression factor.
14 | /// - Parameters:
15 | /// - maxDimension: The maximum width or height the image should have after resizing.
16 | /// - compressionFactor: The compression quality to use when converting the image to JPEG format. Ranges from 0.0 (most compression) to 1.0 (least compression).
17 | /// - Returns: The JPEG data of the resized and compressed image, or `nil` if the image could not be processed.
18 | func resizedAndCompressedImageData(maxDimension: CGFloat, compressionFactor: CGFloat) -> (Data?, NSImage?) {
19 | guard let resizedImage = self.resizedImage(to: maxDimension),
20 | let tiffRepresentation = resizedImage.tiffRepresentation,
21 | let bitmapImage = NSBitmapImageRep(data: tiffRepresentation),
22 | let data = bitmapImage.representation(using: .jpeg, properties: [.compressionFactor: compressionFactor]) else {
23 | return (nil, nil)
24 | }
25 | return (data, resizedImage)
26 | }
27 | }
28 |
29 | extension NSImage {
30 | /// Resizes the image to a specified maximum dimension while maintaining its aspect ratio.
31 | /// The image is scaled down such that its largest dimension (width or height) matches the `maxDimension` provided,
32 | /// ensuring that the aspect ratio of the original image is preserved.
33 | /// - Parameter maxDimension: The maximum width or height the resized image should have.
34 | /// - Returns: A new `NSImage` instance representing the resized image, or `nil` if the image could not be resized.
35 | func resizedImage(to maxDimension: CGFloat) -> NSImage? {
36 | let originalSize = self.size
37 | var newSize: CGSize = .zero
38 |
39 | let widthRatio = maxDimension / originalSize.width
40 | let heightRatio = maxDimension / originalSize.height
41 | let ratio = min(widthRatio, heightRatio)
42 |
43 | newSize.width = floor(originalSize.width * ratio)
44 | newSize.height = floor(originalSize.height * ratio)
45 |
46 | let newImage = NSImage(size: newSize)
47 | newImage.lockFocus()
48 | self.draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height),
49 | from: NSRect(x: 0, y: 0, width: originalSize.width, height: originalSize.height),
50 | operation: .copy, fraction: 1.0)
51 | newImage.unlockFocus()
52 |
53 | return newImage
54 | }
55 | }
56 |
57 | extension NSImage {
58 | /// Converts the image to JPEG format using a specified compression quality.
59 | /// This method generates JPEG data for the image using the provided compression factor.
60 | /// - Parameter compressionQuality: The desired compression quality for the JPEG encoding.
61 | /// Values range from 0.0 (maximum compression, lowest quality) to 1.0 (minimum compression, highest quality).
62 | /// - Returns: The JPEG data of the image, or `nil` if the image could not be converted.
63 | func jpegData(compressionQuality: CGFloat) -> Data? {
64 | guard let tiffRepresentation = self.tiffRepresentation,
65 | let bitmapImage = NSBitmapImageRep(data: tiffRepresentation) else {
66 | return nil
67 | }
68 | return bitmapImage.representation(using: .jpeg, properties: [.compressionFactor: compressionQuality])
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/SwiftDiffusion/SwiftDiffusionApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftDiffusionApp.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/3/24.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftData
10 |
11 | @main
12 | struct SwiftDiffusionApp: App {
13 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
14 | var modelContainer: ModelContainer
15 | var sidebarModel: SidebarModel
16 | var scriptManager = ScriptManager.shared
17 | let pastableService = PastableService.shared
18 | let updateManager = UpdateManager()
19 | let checkpointsManager = CheckpointsManager()
20 | let currentPrompt = PromptModel()
21 | let loraModelsManager = ModelManager()
22 | let vaeModelsManager = ModelManager()
23 |
24 | var body: some Scene {
25 | WindowGroup {
26 | ContentView()
27 | .frame(minWidth: 720, idealWidth: 1200, maxWidth: .infinity,
28 | minHeight: 500, idealHeight: 860, maxHeight: .infinity)
29 | .environmentObject(scriptManager)
30 | .environmentObject(pastableService)
31 | .environmentObject(updateManager)
32 | .environmentObject(sidebarModel)
33 | .environmentObject(checkpointsManager)
34 | .environmentObject(currentPrompt)
35 | .environmentObject(loraModelsManager)
36 | .environmentObject(vaeModelsManager)
37 | .onAppear {
38 | NSWindow.allowsAutomaticWindowTabbing = false
39 | }
40 | }
41 | .modelContainer(modelContainer)
42 | .windowToolbarStyle(.unified(showsTitle: false))
43 | .commands {
44 | CommandGroup(replacing: .newItem) {}
45 | CommandGroup(after: .appInfo) {
46 | Divider()
47 |
48 | Button("Check for Updates...") {
49 | WindowManager.shared.showUpdatesWindow(updateManager: updateManager)
50 | }
51 | .keyboardShortcut("U", modifiers: [.command])
52 |
53 | Divider()
54 |
55 | Button("Settings...") {
56 | WindowManager.shared.showSettingsWindow()
57 | }
58 | .keyboardShortcut(",", modifiers: [.command])
59 | }
60 | }
61 | .commands {
62 | CommandMenu("Prompt") {
63 | MenuButton(title: "Copy Generation Data", symbol: .copy, action: {
64 | sidebarModel.selectedSidebarItem?.prompt?.copyMetadataToClipboard()
65 | })
66 | MenuButton(title: "Paste Generation Data", symbol: .paste, action: {
67 | pastableService.newWorkspaceItemFromParsedPasteboard(sidebarModel: sidebarModel, checkpoints: checkpointsManager.models, vaeModels: vaeModelsManager.models)
68 | })
69 | }
70 | }
71 | }
72 |
73 | init() {
74 | let fileManager = FileManager.default
75 | guard let appSupportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
76 | fatalError("Application Support directory not found.")
77 | }
78 | let storeURL = appSupportURL
79 | .appendingPathComponent(Constants.FileStructure.AppSupportFolderName)
80 | .appendingPathComponent("UserData").appendingPathComponent("LocalDatabase")
81 | .appendingPathComponent(Constants.FileStructure.AppSwiftDataFileName)
82 |
83 | let subfolderURL = storeURL.deletingLastPathComponent()
84 | if !fileManager.fileExists(atPath: subfolderURL.path) {
85 | try! fileManager.createDirectory(at: subfolderURL, withIntermediateDirectories: true)
86 | }
87 |
88 | do {
89 | modelContainer = try ModelContainer(for: SidebarFolder.self, configurations: ModelConfiguration(url: storeURL))
90 | } catch {
91 | fatalError("Failed to configure SwiftData container: \(error)")
92 | }
93 | modelContainer.mainContext.autosaveEnabled = true
94 | sidebarModel = SidebarModel(modelContext: modelContainer.mainContext)
95 | setupAppFileStructure()
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/DetailView/FileHierarchy/FileHierarchy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileHierarchy.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/6/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | class FileHierarchy: ObservableObject {
11 | @Published var rootNodes: [FileNode] = []
12 | @Published var isLoading: Bool = false
13 | var rootPath: String
14 |
15 | init(rootPath: String) {
16 | self.rootPath = rootPath
17 | Task { await self.refresh() }
18 | }
19 |
20 | func refresh() async {
21 | await MainActor.run {
22 | self.isLoading = true
23 | }
24 | let loadedFiles = await FileHierarchy.loadFiles(from: self.rootPath)
25 | await MainActor.run {
26 | self.rootNodes = loadedFiles
27 | self.isLoading = false
28 | }
29 | }
30 |
31 | static func loadFiles(from directory: String) async -> [FileNode] {
32 | var nodes: [FileNode] = []
33 | let fileManager = FileManager.default
34 | do {
35 | let items = try fileManager.contentsOfDirectory(atPath: directory)
36 | for item in items where item != ".DS_Store" {
37 | let itemPath = (directory as NSString).appendingPathComponent(item)
38 | var isDir: ObjCBool = false
39 | let attributes = try fileManager.attributesOfItem(atPath: itemPath)
40 | let modificationDate = attributes[.modificationDate] as? Date ?? Date()
41 | fileManager.fileExists(atPath: itemPath, isDirectory: &isDir)
42 | if isDir.boolValue {
43 | let children = await loadFiles(from: itemPath)
44 | nodes.append(FileNode(name: item, fullPath: itemPath, children: children, lastModified: modificationDate))
45 | } else {
46 | nodes.append(FileNode(name: item, fullPath: itemPath, children: nil, lastModified: modificationDate))
47 | }
48 | }
49 |
50 | nodes.sort { $0.lastModified > $1.lastModified }
51 | } catch {
52 | await MainActor.run {
53 | Debug.log("[FileHierarchy] loadFiles(from: \(directory))\n > \(error)")
54 | }
55 | }
56 | return nodes
57 | }
58 |
59 | }
60 |
61 | extension FileHierarchy {
62 | func findMostRecentlyModifiedImageFile() async -> FileNode? {
63 | func isImageFile(_ path: String) -> Bool {
64 | let imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff"]
65 | return imageExtensions.contains((path as NSString).pathExtension.lowercased())
66 | }
67 |
68 | func searchDirectory(_ directory: String) async -> FileNode? {
69 | var mostRecentImageNode: FileNode? = nil
70 | let fileManager = FileManager.default
71 | do {
72 | let items = try fileManager.contentsOfDirectory(atPath: directory)
73 | for item in items {
74 | let itemPath = (directory as NSString).appendingPathComponent(item)
75 | var isDir: ObjCBool = false
76 | fileManager.fileExists(atPath: itemPath, isDirectory: &isDir)
77 | if isDir.boolValue {
78 | if let foundNode = await searchDirectory(itemPath) {
79 | if mostRecentImageNode == nil || foundNode.lastModified > mostRecentImageNode!.lastModified {
80 | mostRecentImageNode = foundNode
81 | }
82 | }
83 | } else if isImageFile(itemPath) {
84 | let attributes = try fileManager.attributesOfItem(atPath: itemPath)
85 | if let modificationDate = attributes[FileAttributeKey.modificationDate] as? Date {
86 | let fileNode = FileNode(name: item, fullPath: itemPath, lastModified: modificationDate)
87 | if mostRecentImageNode == nil || fileNode.lastModified > mostRecentImageNode!.lastModified {
88 | mostRecentImageNode = fileNode
89 | }
90 | }
91 | }
92 | }
93 | } catch {
94 | Debug.log("Failed to list directory: \(error.localizedDescription)")
95 | }
96 | return mostRecentImageNode
97 | }
98 |
99 | return await searchDirectory(self.rootPath)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/SwiftDiffusion/Views/MainViews/PromptView/PromptMenus/CheckpointMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckpointMenu.swift
3 | // SwiftDiffusion
4 | //
5 | // Created by Justin Bush on 2/18/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CheckpointMenu: View {
11 | @ObservedObject var scriptManager = ScriptManager.shared
12 | @EnvironmentObject var currentPrompt: PromptModel
13 | @EnvironmentObject var checkpointsManager: CheckpointsManager
14 |
15 | @State var showSelectedCheckpointModelWasRemovedAlert: Bool = false
16 | @State var showModelLoadTypeErrorThrownAlert: Bool = false
17 |
18 | var body: some View {
19 | VStack(alignment: .leading, spacing: 0) {
20 | PromptRowHeading(title: "Checkpoint")
21 | .padding(.bottom, 6)
22 | HStack {
23 | Menu {
24 | Section(header: Text(" CoreML")) {
25 | ForEach(checkpointsManager.models.filter { $0.type == .coreMl }
26 | .sorted(by: { $0.name.lowercased() < $1.name.lowercased() })) { model in
27 | Button(model.name) {
28 | currentPrompt.selectedModel = model
29 | }
30 | }
31 | }
32 | Section(header: Text(" Python")) {
33 | ForEach(checkpointsManager.models.filter { $0.type == .python }
34 | .sorted(by: { $0.name.lowercased() < $1.name.lowercased() })) { model in
35 | Button(model.name) {
36 | currentPrompt.selectedModel = model
37 | }
38 | }
39 | }
40 | } label: {
41 | Label(currentPrompt.selectedModel?.name ?? "Choose Model", systemImage: "arkit")
42 | }
43 | }
44 | }
45 |
46 |
47 | // MARK: Handle Recently Removed
48 | .onChange(of: checkpointsManager.recentlyRemovedCheckpointModels) {
49 | handleRecentlyRemovedCheckpointIfSelectedMenuItem()
50 | }
51 | .alert(isPresented: $showSelectedCheckpointModelWasRemovedAlert) {
52 | var message: String = ""
53 | if let model = currentPrompt.selectedModel { message = model.name }
54 |
55 | return Alert(
56 | title: Text("Warning: Model checkpoint was either moved or deleted"),
57 | message: Text(message),
58 | dismissButton: .cancel(Text("OK")) {
59 | currentPrompt.selectedModel = nil
60 | }
61 | )
62 | }
63 | .onChange(of: scriptManager.modelLoadTypeErrorThrown) {
64 | if scriptManager.modelLoadTypeErrorThrown {
65 | showModelLoadTypeErrorThrownAlert = true
66 | }
67 | }
68 | .alert(isPresented: $showModelLoadTypeErrorThrownAlert) {
69 | var message: String = ""
70 | //if let model = currentPrompt.selectedModel { message.append("\(model.name)\n\n") }
71 | message.append("Don't panic! This is a common issue. For whatever reason, this model has issues loading with RAM optimizations.\n\nOpen the Engine Settings and toggle the 'Disable model loading RAM optimizations' option to ON.")
72 |
73 | return Alert(
74 | title: Text("MPS Framework TypeError"),
75 | message: Text(message),
76 | primaryButton: .default(Text("Open Engine Settings")) {
77 | scriptManager.modelLoadTypeErrorThrown = false
78 | WindowManager.shared.showSettingsWindow(withTab: SettingsTab.engine)
79 | },
80 | secondaryButton: .cancel(Text("Ignore")) {
81 | scriptManager.modelLoadTypeErrorThrown = false
82 | }
83 | )
84 | }
85 | }
86 |
87 | func handleRecentlyRemovedCheckpointIfSelectedMenuItem() {
88 | if !checkpointsManager.recentlyRemovedCheckpointModels.isEmpty, let selectedModel = currentPrompt.selectedModel {
89 | for removedModel in checkpointsManager.recentlyRemovedCheckpointModels {
90 | if selectedModel.path == removedModel.path {
91 | showSelectedCheckpointModelWasRemovedAlert = true
92 | }
93 | }
94 | }
95 | checkpointsManager.recentlyRemovedCheckpointModels = []
96 | }
97 | }
98 |
99 | #Preview {
100 | CommonPreviews.promptView
101 | }
102 |
--------------------------------------------------------------------------------