├── 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 | --------------------------------------------------------------------------------