Repository: NSAntoine/Samra Branch: main Commit: a25a91f3c884 Files: 46 Total size: 162.7 KB Directory structure: gitextract_kxyr337y/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── Samra/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Backend/ │ │ ├── AppKitPrivates/ │ │ │ ├── AppKitPrivates.h │ │ │ └── module.modulemap │ │ ├── AssetCatalogInput.swift │ │ ├── ClosureMenuItem.swift │ │ ├── DetailItem.swift │ │ ├── Extensions.swift │ │ ├── Preferences.swift │ │ ├── RenditionDiff.swift │ │ └── UI Support/ │ │ ├── AssetCatalogDocument.swift │ │ ├── BasicLayoutAnchorsHolding.swift │ │ ├── QuickLooKPreviewSource.swift │ │ └── URLHandler.swift │ ├── Info.plist │ ├── Samra.entitlements │ ├── UI/ │ │ ├── AboutViewController.swift │ │ ├── ClosureBasedButton.swift │ │ ├── CollapseNotifierSplitViewController.swift │ │ ├── Diff/ │ │ │ ├── AssetCatalogDiffSelectionViewController.swift │ │ │ ├── DiffFilePreviewView.swift │ │ │ └── DiffListViewController.swift │ │ ├── MenuableCollectionView.swift │ │ ├── PastFilesListViewController.swift │ │ ├── Rendition/ │ │ │ ├── AssetCatalogDetailsView.swift │ │ │ ├── RenditionCollectionViewItem.swift │ │ │ ├── RenditionInformationView.swift │ │ │ ├── RenditionListViewController.swift │ │ │ ├── RenditionTypeHeaderView.swift │ │ │ └── TypesListViewController.swift │ │ ├── WelcomeScreenOption.swift │ │ └── WelcomeViewController.swift │ └── WindowController.swift ├── Samra.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ ├── Samra.xcscheme │ └── extractutil.xcscheme └── extractutil/ └── main.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ ko_fi: nsantoine ================================================ FILE: .github/workflows/main.yml ================================================ name: Xcode - Build and Analyze on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: name: Build and analyse default scheme using xcodebuild command runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Build run: | xcodebuild build -scheme Samra -project Samra.xcodeproj -configuration Release CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO BUILD_DIR=${{ github.workspace }}/xcodebuild xcodebuild build -scheme extractutil -project Samra.xcodeproj -configuration Release CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO BUILD_DIR=${{ github.workspace }}/xcodebuild mkdir -p ${{ github.workspace }}/product cp -R ${{ github.workspace }}/xcodebuild/Release/Samra.app ${{ github.workspace }}/product mv ${{ github.workspace }}/xcodebuild/Release/extractutil ${{ github.workspace }}/product/Samra.app/Contents/MacOS cd ${{ github.workspace }}/product zip -r ${{ github.workspace }}/Samra.zip . - name: Upload app to artifacts uses: actions/upload-artifact@v3 with: name: Samra path: ${{ github.workspace }}/Samra.zip ================================================ FILE: .gitignore ================================================ .DS_Store Package.resolved *.xcuserdatad ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Antoine Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Samra A macOS Application to explore and edit Asset Catalog (.car) files on macOS, with a nicer native, modern-feeling UI that does not crash every couple of seconds. image ## Why Samra? There are a few existing asset catalog viewer applications for macOS, however, none felt feature complete, Samra offers the following: - Browse through the Asset Catalog file - Show all types of objects (renditions) in it, not just images (colors, pdfs, image sets, etc) - Ability to edit icons/images & colors - Search in Asset Catalog for renditions by name - View detailed information about each rendition, such as lookup name, width, height, appearance (if it's meant for dark mode or light mode) and other type-specific information (ie, RGB values for colors). ## What versions does this support? macOS 10.15.1+ ## How can I download this? Download the .app from the Releases ## Preview image image image image ================================================ FILE: Samra/AppDelegate.swift ================================================ // // AppDelegate.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import AssetCatalogWrapper @main class AppDelegate: NSObject, NSApplicationDelegate { var showWelcomeViewController: Bool = false static func main() { let instance = AppDelegate() NSApplication.shared.delegate = instance NSApplication.shared.run() } func applicationWillFinishLaunching(_ notification: Notification) {} @objc func openMenuItemClicked() { URLHandler.shared.presentArchiveChooserPanel(insertToRecentItems: true, senderView: nil) } func applicationDidFinishLaunching(_ aNotification: Notification) { // Insert code here to initialize your application if Preferences.showWelcomeVCOnLaunch { WindowController(kind: .welcome).showWindow(self) } let items = RenditionType.allCases.map { type in let item = NSMenuItem(title: type.description, action: #selector(TypesListViewController.goToSection(menuItemSender:))) item.tag = type.rawValue item.isEnabled = false return item } NSApplication.shared.mainMenu = NSMenu(items: [ NSMenuItem(submenuTitle: "App", items: [ NSMenuItem(title: "About Samra", action: #selector(openAboutPanel), keyEquivalent: ""), NSMenuItem.separator(), NSMenuItem(title: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h", keyEquivalentModifierMask: [.command, .option]), NSMenuItem(title: "Show All", action: #selector(NSApplication.unhideAllApplications(_:))), NSMenuItem.separator(), NSMenuItem(title: "Quit Samra", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"), ]), NSMenuItem(submenuTitle: "File", items: [ NSMenuItem(title: "Open...", action: #selector(openMenuItemClicked), keyEquivalent: "o"), NSMenuItem(title: "Export to...", action: #selector(RenditionListViewController.exportCatalog)), NSMenuItem.separator(), NSMenuItem(title: "Diff Asset Catalogs", action: #selector(openDiffViewController), keyEquivalent: "d"), NSMenuItem.separator(), NSMenuItem(title: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") ]), NSMenuItem(submenuTitle: "Edit", items: [ NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z"), NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "Z"), NSMenuItem.separator(), NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"), NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"), NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"), NSMenuItem(title: "Paste and Match Style", action: #selector(NSText.paste(_:)), keyEquivalent: "V", keyEquivalentModifierMask: [.command, .option]), NSMenuItem(title: "Delete", action: #selector(NSText.delete(_:))), NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"), NSMenuItem.separator(), NSMenuItem( submenuTitle: "Find", items: [ NSMenuItem(title: "Find…", action: #selector(NSResponder.performTextFinderAction(_:)), keyEquivalent: "f", tag: NSTextFinder.Action.showFindInterface.rawValue), NSMenuItem(title: "Find and Replace…", action: #selector(NSResponder.performTextFinderAction(_:)), keyEquivalent: "f", keyEquivalentModifierMask: [.command, .option], tag: NSTextFinder.Action.replaceAndFind.rawValue), NSMenuItem(title: "Find Next", action: #selector(NSResponder.performTextFinderAction(_:)), keyEquivalent: "g", tag: NSTextFinder.Action.nextMatch.rawValue), NSMenuItem(title: "Find Previous", action: #selector(NSResponder.performTextFinderAction(_:)), keyEquivalent: "G", tag: NSTextFinder.Action.previousMatch.rawValue), NSMenuItem(title: "Use Selection for Find", action: #selector(NSResponder.performTextFinderAction(_:)), keyEquivalent: "e", tag: NSTextFinder.Action.setSearchString.rawValue), NSMenuItem(title: "Jump to Selection", action: #selector(NSResponder.centerSelectionInVisibleArea(_:)), keyEquivalent: "j"), ]), NSMenuItem( submenuTitle: "Spelling and Grammar", items: [ NSMenuItem(title: "Show Spelling and Grammar", action: #selector(NSTextCheckingController.showGuessPanel(_:)), keyEquivalent: ":"), NSMenuItem(title: "Check Document Now", action: #selector(NSTextCheckingController.checkSpelling(_:)), keyEquivalent: ";"), NSMenuItem(title: "Check Spelling While Typing", action: #selector(NSTextView.toggleContinuousSpellChecking(_:))), NSMenuItem(title: "Correct Spelling Automatically", action: #selector(NSTextView.toggleAutomaticSpellingCorrection(_:))), ]), NSMenuItem( submenuTitle: "Substitutions", items: [ NSMenuItem(title: "Show Substitutions", action: #selector(NSTextCheckingController.orderFrontSubstitutionsPanel(_:))), NSMenuItem.separator(), NSMenuItem(title: "Smart Copy/Paste", action: #selector(NSTextView.toggleSmartInsertDelete(_:))), NSMenuItem(title: "Smart Quotes", action: #selector(NSTextView.toggleAutomaticQuoteSubstitution(_:))), NSMenuItem(title: "Smart Dashes", action: #selector(NSTextView.toggleAutomaticDashSubstitution(_:))), NSMenuItem(title: "Smart Links", action: #selector(NSTextView.toggleAutomaticLinkDetection(_:))), NSMenuItem(title: "Data Detectors", action: #selector(NSTextView.toggleAutomaticDataDetection(_:))), NSMenuItem(title: "Text Replacement", action: #selector(NSTextView.toggleAutomaticTextReplacement(_:))), ]), NSMenuItem( submenuTitle: "Transformations", items: [ NSMenuItem(title: "Make Upper Case", action: #selector(NSResponder.uppercaseWord(_:))), NSMenuItem(title: "Make Lower Case", action: #selector(NSResponder.lowercaseWord(_:))), NSMenuItem(title: "Capitalize", action: #selector(NSResponder.capitalizeWord(_:))), ]), NSMenuItem( submenuTitle: "Speech", items: [ NSMenuItem(title: "Start Speaking", action: #selector(NSSpeechSynthesizer.startSpeaking(_:))), NSMenuItem(title: "Stop Speaking", action: #selector(NSTextView.stopSpeaking(_:))), ]), ]), NSMenuItem(submenuTitle: "Sections", items: items), NSMenuItem(submenuTitle: "Help", items: [ NSMenuItem(title: "Help", action: #selector(NSApplication.showHelp(_:)), keyEquivalent: "?") ]), ]) } func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application } func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { guard !flag else { return true } if Preferences.showWelcomeVCOnLaunch { WindowController(kind: .welcome).showWindow(self) } else { URLHandler.shared.presentArchiveChooserPanel(insertToRecentItems: true, senderView: nil) } return false } @objc func openAboutPanel() { WindowController(kind: .aboutPanel).showWindow(self) } @objc func openDiffViewController() { WindowController(kind: .diffSelection).showWindow(self) } func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { let items = Preferences.recentlyOpenedFilePaths guard !items.isEmpty else { return nil } let parentMenu = NSMenu() let submenu = NSMenu() let submenuItem = NSMenuItem() submenuItem.title = "Recents" submenuItem.submenu = submenu for (index, item) in items.enumerated() { let menuItem = NSMenuItem(title: URL(fileURLWithPath: item).lastPathComponent, action: #selector(openItemFromDockMenu(sender:)), keyEquivalent: "") menuItem.tag = index submenu.addItem(menuItem) } parentMenu.addItem(submenuItem) return parentMenu } @objc func openItemFromDockMenu(sender: NSMenuItem) { let item = Preferences.recentlyOpenedFilePaths[sender.tag] URLHandler.shared.handleURLChosen(urlChosen: URL(fileURLWithPath: item), senderView: nil, insertToRecentItems: true) } func application(_ application: NSApplication, open urls: [URL]) { for url in urls { URLHandler.shared.handleURLChosen(urlChosen: url, senderView: nil, insertToRecentItems: true, openWelcomeScreenUponError: true) } } } ================================================ FILE: Samra/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Samra/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "icon_16x16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "icon_16x16@2x.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "icon_32x32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "icon_32x32@2x.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "icon_128x128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "icon_128x128@2x.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "icon_256x256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "icon_256x256@2x.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "icon_512x512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "icon_512x512@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "iconfly" } } ================================================ FILE: Samra/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Samra/Backend/AppKitPrivates/AppKitPrivates.h ================================================ // // AppKitPrivates.h // Samra // // Created by Serena on 22/02/2023. // #ifndef AppKitPrivates_h #define AppKitPrivates_h #import // Never go Eric Benét @interface NSSplitViewController (PrivateForWhateverReason) - (void)splitViewItem:(NSSplitViewItem * _Nonnull)item didChangeCollapsed:(BOOL)didCollapse animated:(BOOL)animated; @end #endif /* AppKitPrivates_h */ ================================================ FILE: Samra/Backend/AppKitPrivates/module.modulemap ================================================ module AppKitPrivates { header "AppKitPrivates.h" } ================================================ FILE: Samra/Backend/AssetCatalogInput.swift ================================================ // // AssetCatalogInput.swift // Samra // // Created by Serena on 06/03/2023. // import AssetCatalogWrapper struct AssetCatalogInput { let fileURL: URL let catalog: CUICatalog let collection: RenditionCollection init(fileURL: URL, catalog: CUICatalog, collection: RenditionCollection) { self.fileURL = fileURL self.catalog = catalog self.collection = collection } init(fileURL: URL) throws { let (catalog, collection) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL) self.catalog = catalog self.collection = collection self.fileURL = fileURL } } ================================================ FILE: Samra/Backend/ClosureMenuItem.swift ================================================ // // ClosureMenuItem.swift // Samra // // Created by Serena on 02/03/2023. // import Cocoa class ClosureMenuItem: NSMenuItem { var closure: (() -> Void) @objc func performClosure() { closure() } init(title: String, closure: @escaping (() -> Void)) { self.closure = closure super.init(title: title, action: #selector(performClosure), keyEquivalent: "") self.target = self } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Samra/Backend/DetailItem.swift ================================================ // // DetailItem.swift // Samra // // Created by Serena on 21/02/2023. // import Cocoa import AssetCatalogWrapper struct DetailItem: Hashable { /// The Primary Text, such as "Height" let primaryText: String /// The Secondary Text, such as the height itself in String form let secondaryText: String init(primaryText: String, secondaryText: String) { self.primaryText = primaryText self.secondaryText = secondaryText } init(primaryText: String, secondaryText: T?, fallback: String = "Unknown") { self.primaryText = primaryText self.secondaryText = secondaryText?.description ?? fallback } } struct DetailItemSection: Hashable { let sectionHeader: String let items: [DetailItem] static func from(assetStorage: CUICommonAssetStorage) -> [DetailItemSection] { let toolSection = DetailItemSection(sectionHeader: "Authoring Tool", items: [ DetailItem(primaryText: "Tool", secondaryText: assetStorage.authoringTool()), DetailItem(primaryText: "Version", secondaryText: String(cString: assetStorage.versionString())), ]) let argumentsSection = DetailItemSection(sectionHeader: "Arguments", items: [ DetailItem(primaryText: "Thinning Arguments", secondaryText: assetStorage.thinningArguments()) ]) let dateFormatter = DateFormatter() dateFormatter.dateFormat = "E, d MMM yyyy h:mm a" let date = Date(timeIntervalSince1970: TimeInterval(assetStorage.storageTimestamp())) let dateSection = DetailItemSection(sectionHeader: "Date", items: [ DetailItem(primaryText: "Date", secondaryText: dateFormatter.string(from: date)), DetailItem(primaryText: "UNIX Timestamp", secondaryText: assetStorage.storageTimestamp()) ]) let coreUIVersionText = assetStorage.responds(to: #selector(CUICommonAssetStorage.coreuiVersion)) ? assetStorage.coreuiVersion().description : "Unknown" let coreUISection = DetailItemSection(sectionHeader: "Other", items: [ DetailItem(primaryText: "CoreUI Version", secondaryText: coreUIVersionText), DetailItem(primaryText: "Schema Version", secondaryText: assetStorage.schemaVersion()), ]) return [toolSection, argumentsSection, dateSection, coreUISection] } static func from(rendition: Rendition) -> [DetailItemSection] { let cuiRend = rendition.cuiRend let namedLookup = rendition.namedLookup let formatter = ByteCountFormatter() formatter.countStyle = .memory formatter.includesActualByteCount = true let diskSize = formatter.string(fromByteCount: Int64(cuiRend.srcData.count)) let sizeOnDisk = DetailItem(primaryText: "Size On Disk", secondaryText: diskSize) var items: [DetailItemSection] = [] switch rendition.type { case .rawData: items.append(DetailItemSection(sectionHeader: "Base Attributes", items: [ DetailItem(primaryText: "Name", secondaryText: namedLookup.name), sizeOnDisk, DetailItem(primaryText: "Compression", secondaryText:cuiRend.bitmapEncoding()) ])) var details : [DetailItem] = [] if let data = cuiRend.data() { let size = formatter.string(fromByteCount: Int64(data.count)) details.append(DetailItem(primaryText: "Data Length", secondaryText:size)) } if let uti = cuiRend.utiType() { details.append(DetailItem(primaryText: "UTI", secondaryText:uti)) } items.append(DetailItemSection(sectionHeader: "Data Attributes", items: details)) case .color: items.append(DetailItemSection(sectionHeader: "Base Attributes", items: [ DetailItem(primaryText: "Name", secondaryText: cuiRend.name()), sizeOnDisk, ])) let cgColor = cuiRend.cgColor().takeUnretainedValue() let nsColor = NSColor(cgColor:cgColor)?.usingColorSpace(.deviceRGB) var red: CGFloat = 0 var green: CGFloat = 0 var blue: CGFloat = 0 var alpha: CGFloat = 0 nsColor?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) items.append(DetailItemSection(sectionHeader: "Color Attributes", items: [ DetailItem(primaryText: "Red", secondaryText: Int(red * 255)), DetailItem(primaryText: "Green", secondaryText: Int(green * 255)), DetailItem(primaryText: "Blue", secondaryText: Int(blue * 255)), ])) case .svg, .pdf: items.append(DetailItemSection(sectionHeader: "Base Attributes", items: [ DetailItem(primaryText: "Rendition Name", secondaryText: cuiRend.name()), DetailItem(primaryText: "Lookup Name", secondaryText: namedLookup.name), sizeOnDisk, ])) var size = CGSizeZero switch rendition.type { case .svg: if let svgDoc = cuiRend.svgDocument() { size = CGSVGDocumentGetCanvasSize(svgDoc) } case .pdf: if let pdfDoc = cuiRend.pdfDocument()?.takeUnretainedValue(), let page = pdfDoc.page(at:1) { size = page.getBoxRect(.artBox).size } default: break } items.append(DetailItemSection(sectionHeader: "Dimensions", items: [ DetailItem(primaryText: "Width", secondaryText: size.width), DetailItem(primaryText: "Height", secondaryText: size.height), ])) default: items.append(DetailItemSection(sectionHeader: "Base Attributes", items: [ DetailItem(primaryText: "Rendition Name", secondaryText: cuiRend.name()), DetailItem(primaryText: "Lookup Name", secondaryText: namedLookup.name), sizeOnDisk, DetailItem(primaryText: "Compression", secondaryText:cuiRend.bitmapEncoding()) ])) let size = cuiRend.unslicedSize() items.append(DetailItemSection(sectionHeader: "Dimensions", items: [ DetailItem(primaryText: "Width", secondaryText: size.width), DetailItem(primaryText: "Height", secondaryText: size.height), DetailItem(primaryText: "Scale", secondaryText: cuiRend.scale()) ])) } let key = namedLookup.key items.append(DetailItemSection(sectionHeader: "Rendition Information", items: [ DetailItem(primaryText: "Display Gamut", secondaryText: Rendition.DisplayGamut(key)), DetailItem(primaryText: "Appearance", secondaryText: namedLookup.appearance), DetailItem(primaryText: "Idiom", secondaryText: Rendition.Idiom(key)) ])) return items } } ================================================ FILE: Samra/Backend/Extensions.swift ================================================ // // Extensions.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import AssetCatalogWrapper import UniformTypeIdentifiers @available(macOS 11, *) extension UTType { static var carFile: UTType = UTType(filenameExtension: "car")! } extension NSUserInterfaceItemIdentifier: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { self.init(value) } } extension NSToolbarItem.Identifier { static let searchBar = NSToolbarItem.Identifier("SearchBar") } extension NSMenu { convenience init(title: String? = nil, items: [NSMenuItem]?) { defer { items.flatMap { self.items = $0 } } guard let title = title else { self.init() return } self.init(title: title) } } extension NSMenuItem { convenience init(submenuTitle: String, items: [NSMenuItem]?) { self.init(title: submenuTitle, action: nil, keyEquivalent: "") submenu = NSMenu(title: submenuTitle, items: items) } convenience init(title: String, action: Selector? = nil, keyEquivalent: String = "", keyEquivalentModifierMask: NSEvent.ModifierFlags? = nil, tag: Int? = nil) { self.init(title: title, action: action, keyEquivalent: keyEquivalent) keyEquivalentModifierMask.flatMap { self.keyEquivalentModifierMask = $0 } tag.flatMap { self.tag = $0 } } } extension CGImage { var size: CGSize { return CGSize(width: width, height: height) } } extension NSAlert { convenience init(title: String, message: String? = nil) { self.init() self.messageText = title self.informativeText = message ?? self.informativeText } } extension NSWindow { /// Makes the title bar of the NSWindow transparent and removes the window's ability to be resized func makeTitleBarTransparentAndUnresizable() { styleMask.remove(.resizable) titleVisibility = .hidden titlebarAppearsTransparent = true } } extension NSColor { static func _makeStandardWindowBg(appearance: NSAppearance) -> NSColor { switch appearance.name { case .aqua, .vibrantLight, .accessibilityHighContrastAqua, .accessibilityHighContrastVibrantLight: // light return .white case .darkAqua, .accessibilityHighContrastVibrantDark, .accessibilityHighContrastDarkAqua, .vibrantDark: // dark return NSColor(red: 0.19, green: 0.19, blue: 0.19, alpha: 1) default: fatalError() } } static var standardWindowBackgroundColor: NSColor { return NSColor(name: nil, dynamicProvider: _makeStandardWindowBg(appearance:)) } } extension NSImage { convenience init?(systemName: String) { if #available(macOS 11, *) { self.init(systemSymbolName: systemName, accessibilityDescription: nil) } else { return nil } } } ================================================ FILE: Samra/Backend/Preferences.swift ================================================ // // Preferences.swift // Samra // // Created by Serena on 18/02/2023. // import Foundation @propertyWrapper struct Storage { let key: String var defaultValue: T var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } } enum Preferences { @Storage(key: "RecentlyOpenedPaths", defaultValue: []) static var recentlyOpenedFilePaths: [String] @Storage(key: "ShowWelcomeViewControllerOnLaunch", defaultValue: true) static var showWelcomeVCOnLaunch: Bool /* static var recentlyOpenedFilePaths: [String] { get { let arr = UserDefaults.standard.stringArray(forKey: "RecentlyOpenedPaths") ?? [] return arr } set { UserDefaults.standard.set(newValue, forKey: "RecentlyOpenedPaths") } } */ } ================================================ FILE: Samra/Backend/RenditionDiff.swift ================================================ // // DiffKind.swift // Samra // // Created by Serena on 07/03/2023. // import AssetCatalogWrapper struct RenditionDiff { let rend: Rendition let kind: Kind enum Kind: CustomStringConvertible { case added case removed var description: String { switch self { case .added: return "Added" case .removed: return "Removed" } } } } enum DiffSide: Int { case left = 1 case right = 2 } ================================================ FILE: Samra/Backend/UI Support/AssetCatalogDocument.swift ================================================ // // AssetCatalogDocument.swift // Samra // // Created by Serena on 02/03/2023. // import Cocoa import AssetCatalogWrapper // this NSDocument subclass is from https://github.com/insidegui/AssetCatalogTinkerer // (because this app is my first attempt at AppKit and I didn't really know how to do NSDocument).. // but adjusted for Samra class AssetCatalogDocument: NSDocument { override func read(from url: URL, ofType typeName: String) throws { // close the welcome view controller if opened for window in NSApplication.shared.windows { if window.contentViewController is WelcomeViewController { window.close() } } let windowController = WindowController(kind: .assetCatalog(try AssetCatalogInput(fileURL: url))) addWindowController(windowController) windowController.showWindow(nil) } } ================================================ FILE: Samra/Backend/UI Support/BasicLayoutAnchorsHolding.swift ================================================ // // BasicLayoutAnchorsHolding.swift // Samra // // Created by Serena on 18/02/2023. // #if canImport(AppKit) import AppKit #elseif canImport(UIKit) import UIKit #endif protocol BasicLayoutAnchorsHolding { var topAnchor: NSLayoutYAxisAnchor { get } var bottomAnchor: NSLayoutYAxisAnchor { get } var leadingAnchor: NSLayoutXAxisAnchor { get } var trailingAnchor: NSLayoutXAxisAnchor { get } var centerXAnchor: NSLayoutXAxisAnchor { get } var centerYAnchor: NSLayoutYAxisAnchor { get } } extension BasicLayoutAnchorsHolding { /// Activate constraints to cover the target with the current item. func constraintCompletely(to target: Target) { NSLayoutConstraint.activate([ leadingAnchor.constraint(equalTo: target.leadingAnchor), trailingAnchor.constraint(equalTo: target.trailingAnchor), topAnchor.constraint(equalTo: target.topAnchor), bottomAnchor.constraint(equalTo: target.bottomAnchor) ]) } /// Activate constraints to center the target with the current item. func centerConstraints(to target: Target) { NSLayoutConstraint.activate([ centerXAnchor.constraint(equalTo: target.centerXAnchor), centerYAnchor.constraint(equalTo: target.centerYAnchor) ]) } } #if canImport(UIKit) extension UIView: BasicLayoutAnchorsHolding {} extension UILayoutGuide: BasicLayoutAnchorsHolding {} #else extension NSView: BasicLayoutAnchorsHolding {} extension NSLayoutGuide: BasicLayoutAnchorsHolding {} #endif ================================================ FILE: Samra/Backend/UI Support/QuickLooKPreviewSource.swift ================================================ // // QuickLooKPreviewSource.swift // Samra // // Created by Serena on 03/03/2023. // import Cocoa import QuickLookUI class QuickLookPreviewSource: NSObject, QLPreviewPanelDataSource { let fileURL: URL init(fileURL: URL) { self.fileURL = fileURL } func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { return 1 } func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! { return fileURL as QLPreviewItem } } ================================================ FILE: Samra/Backend/UI Support/URLHandler.swift ================================================ // // URLHandler.swift // Samra // // Created by Serena on 20/02/2023. // import Cocoa import AssetCatalogWrapper class URLHandler { static let shared = URLHandler() private init() {} @objc func presentArchiveChooserPanel(insertToRecentItems: Bool = false, senderView: NSView?, handler: ((URL) -> Void)? = nil) { let panel = NSOpenPanel() let button = ClosureBasedButton(checkboxWithTitle: "Treat Bundles as directories", target: nil, action: nil) button.allowsMixedState = false button.setAction { switch button.state { case .on: panel.treatsFilePackagesAsDirectories = true case .off: panel.treatsFilePackagesAsDirectories = false default: print("Not supposed to be here") } } panel.accessoryView = button panel.accessoryView?.frame.size.height += 18 panel.canChooseDirectories = false panel.allowsMultipleSelection = false if #available(macOS 11, *) { panel.allowedContentTypes = [.carFile, .application] } else { panel.allowedFileTypes = ["car", "app"] } if panel.runModal() == .OK { if let handler { handler(panel.urls[0]) } else { handleURLChosen(urlChosen: panel.urls[0], senderView: senderView, insertToRecentItems: insertToRecentItems) } } } func handleURLChosen(urlChosen: URL, senderView: NSView?, insertToRecentItems: Bool = false, openWelcomeScreenUponError: Bool = false) { let urlToOpen: URL switch urlChosen.pathExtension { case "car": // in case the URL was opened through the samra:// URL scheme, // let's init with URL(fileURLWithPath:), // to make sure that we have the file:// URL scheme urlToOpen = URL(fileURLWithPath: urlChosen.path) case "app": // find Assets.car file for the application // and make sure it exists urlToOpen = URL(fileURLWithPath: urlChosen.path) // set to file URL .appendingPathComponent("Contents/Resources/Assets.car") guard FileManager.default.fileExists(atPath: urlToOpen.path) else { NSAlert(title: "Assets.car file does not exist for Application \(urlChosen.path)").runModal() return } default: NSAlert(title: "File has unrecognized extension \"\(urlChosen.pathExtension)\"").runModal() return } do { let input = try AssetCatalogInput(fileURL: urlToOpen) // open new window & view controller for it WindowController(kind: .assetCatalog(input)).showWindow(self) if insertToRecentItems { var copy = Preferences.recentlyOpenedFilePaths copy.removeAll { $0 == urlChosen.path } copy.append(urlChosen.path) Preferences.recentlyOpenedFilePaths = copy } senderView?.window?.close() } catch { if openWelcomeScreenUponError { WindowController(kind: .welcome).showWindow(NSApplication.shared.delegate) } let alert = NSAlert() alert.messageText = "Unable to load Assets file" alert.informativeText = "Error: \(error.localizedDescription)" alert.runModal() } } } ================================================ FILE: Samra/Info.plist ================================================ CFBundleDocumentTypes CFBundleTypeExtensions car CFBundleTypeIconFile CFBundleTypeName Asset Catalog CFBundleTypeIconSystemGenerated 1 CFBundleTypeRole Viewer LSItemContentTypes com.apple.assetcatalog LSTypeIsPackage 0 NSDocumentClass $(PRODUCT_MODULE_NAME).AssetCatalogDocument UTExportedTypeDeclarations UTTypeConformsTo public.data UTTypeDescription Asset Catalog UTTypeIdentifier com.apple.assetcatalog UTTypeIconFile UTTypeIcons UTTypeIconBackgroundName AssetCatalog UTTypeIconBadgeName UTTypeIconText UTTypeTagSpecification public.filename-extension car CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName com.serena.samra.openfile CFBundleURLSchemes samra ================================================ FILE: Samra/Samra.entitlements ================================================ ================================================ FILE: Samra/UI/AboutViewController.swift ================================================ // // AboutViewController.swift // Samra // // Created by Serena on 28/02/2023. // import Cocoa class AboutViewController: NSViewController { override func loadView() { view = NSView() view.frame.size = CGSize(width: 530.0, height:219.0) } override func viewDidLoad() { super.viewDidLoad() let imageView = NSImageView(image: NSApplication.shared.applicationIconImage) let titleLabel = NSTextField(labelWithString: "Samra") titleLabel.font = .systemFont(ofSize: 38) let version = Bundle.main.infoDictionary?["CFBundleVersion"] as! String let versionLabel = NSTextField(labelWithString: "Version \(version)") versionLabel.textColor = .secondaryLabelColor let explanation = "An open source macOS Application to browse and edit Asset Catalog files, created by Antoine" let explanationLabel = NSTextField(wrappingLabelWithString: explanation) explanationLabel.textColor = NSColor(red: 0.60, green: 0.60, blue: 0.60, alpha: 1.00) if #available(macOS 11, *) { explanationLabel.font = .preferredFont(forTextStyle: .footnote) } else { explanationLabel.font = .systemFont(ofSize: 10) } imageView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false versionLabel.translatesAutoresizingMaskIntoConstraints = false explanationLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(imageView) view.addSubview(titleLabel) view.addSubview(versionLabel) view.addSubview(explanationLabel) NSLayoutConstraint.activate([ imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 15), titleLabel.centerYAnchor.constraint(equalTo: imageView.topAnchor, constant: 32), versionLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), versionLabel.centerYAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), explanationLabel.leadingAnchor.constraint(equalTo: versionLabel.leadingAnchor), explanationLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor), explanationLabel.centerYAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: 20) ]) let twitterButton = NSButton(title: "Twitter", target: self, action: #selector(openTwitter)) let sourceCodeButton = NSButton(title: "Source Code", target: self, action: #selector(openSourceCode)) twitterButton.bezelStyle = .rounded sourceCodeButton.bezelStyle = .rounded let buttonsStackView = NSStackView(views: [twitterButton, sourceCodeButton]) buttonsStackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(buttonsStackView) NSLayoutConstraint.activate([ buttonsStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -26.4), buttonsStackView.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: -25), // twitterButton.widthAnchor.constraint(equalToConstant: 154), // sourceCodeButton.widthAnchor.constraint(equalToConstant: 160) ]) } @objc func openSourceCode() { NSWorkspace.shared.open(URL(string: "https://github.com/NSAntoine/Samra")!) } @objc func openTwitter() { NSWorkspace.shared.open(URL(string: "https://twitter.com/NSAntoine")!) } override func viewDidAppear() { super.viewDidAppear() guard let window = view.window else { return } window.backgroundColor = .standardWindowBackgroundColor window.standardWindowButton(.miniaturizeButton)?.isEnabled = false window.standardWindowButton(.zoomButton)?.isEnabled = false } } ================================================ FILE: Samra/UI/ClosureBasedButton.swift ================================================ // // ClosureBasedButton.swift // Samra // // Created by Serena on 05/03/2024. // import Cocoa class ClosureBasedButton: NSButton { var closureAction: (() -> Void)? @objc func performClosureAction() { closureAction?() } func setAction(_ action: @escaping () -> Void) { self.closureAction = action self.action = #selector(performClosureAction) self.target = self } } ================================================ FILE: Samra/UI/CollapseNotifierSplitViewController.swift ================================================ // // CollapseNotifierSplitViewController.swift // Samra // // Created by Serena on 22/02/2023. // import Cocoa import AppKitPrivates /// A NSSPlitViewController subclass that notifies it's reciever /// when a collapse status changes class CollapseNotifierSplitViewController: NSSplitViewController { typealias Handler = (_ item: NSSplitViewItem, _ didCollapse: Bool, _ animated: Bool) -> Void var handler: Handler? = nil /// Whether or not the view controller should focus on the search bar /// when the cmd+f combo is clicked var shouldFocusOnSearchBar: Bool = false override func splitViewItem(_ item: NSSplitViewItem, didChangeCollapsed didCollapse: Bool, animated: Bool) { super.splitViewItem(item, didChangeCollapsed: didCollapse, animated: animated) handler?(item, didCollapse, animated) } } ================================================ FILE: Samra/UI/Diff/AssetCatalogDiffSelectionViewController.swift ================================================ // // AssetCatalogDiffSelectionViewController.swift // Samra // // Created by Serena on 06/03/2023. // import Cocoa import AssetCatalogWrapper /// A View Controller to select 2 files to diff them. class AssetCatalogDiffSelectionViewController: NSViewController { override func loadView() { view = NSView() view.frame.size = CGSize(width: 577, height: 208) } typealias DataSource = NSCollectionViewDiffableDataSource var dataSource: DataSource! var leftCatalogInput: AssetCatalogInput? var rightCatalogInput: AssetCatalogInput? var leftCatalogPathLabel: NSTextField! var rightCatalogPathLabel: NSTextField! var leftCatalogPreview: DiffFilePreviewView! var rightCatalogPreview: DiffFilePreviewView! var diffCatalogsButton: NSButton! override func viewDidLoad() { super.viewDidLoad() let leftButton = NSButton(title: "Left...", target: self, action: #selector(leftOrRightButtonClicked(sender:))) leftButton.tag = DiffSide.left.rawValue let rightButton = NSButton(title: "Right...", target: self, action: #selector(leftOrRightButtonClicked(sender:))) rightButton.tag = DiffSide.right.rawValue leftButton.translatesAutoresizingMaskIntoConstraints = false rightButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(leftButton) view.addSubview(rightButton) leftCatalogPathLabel = NSTextField(labelWithString: "") rightCatalogPathLabel = NSTextField(labelWithString: "") leftCatalogPathLabel.translatesAutoresizingMaskIntoConstraints = false rightCatalogPathLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(leftCatalogPathLabel) view.addSubview(rightCatalogPathLabel) diffCatalogsButton = NSButton(title: "Start Diff", target: self, action: #selector(diffButtonPressed)) diffCatalogsButton.translatesAutoresizingMaskIntoConstraints = false diffCatalogsButton.isEnabled = false view.addSubview(diffCatalogsButton) let previewBackgroundColor = NSColor(red: 0.22, green: 0.21, blue: 0.21, alpha: 1.00) leftCatalogPreview = makePreview(color: previewBackgroundColor, side: .left) leftCatalogPreview.translatesAutoresizingMaskIntoConstraints = false view.addSubview(leftCatalogPreview) let leftCatalogPreviewLabel = NSTextField(labelWithString: "Left") leftCatalogPreviewLabel.translatesAutoresizingMaskIntoConstraints = false leftCatalogPreviewLabel.alignment = .center view.addSubview(leftCatalogPreviewLabel) rightCatalogPreview = makePreview(color: previewBackgroundColor, side: .right) rightCatalogPreview.translatesAutoresizingMaskIntoConstraints = false view.addSubview(rightCatalogPreview) let rightCatalogPreviewLabel = NSTextField(labelWithString: "Right") rightCatalogPreviewLabel.translatesAutoresizingMaskIntoConstraints = false rightCatalogPreviewLabel.alignment = .center view.addSubview(rightCatalogPreviewLabel) NSLayoutConstraint.activate([ leftButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -10), leftButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), leftButton.widthAnchor.constraint(equalToConstant: 80), rightButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 25), rightButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), rightButton.widthAnchor.constraint(equalToConstant: 80), rightCatalogPreview.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -13), rightCatalogPreview.widthAnchor.constraint(equalToConstant: 55), rightCatalogPreview.topAnchor.constraint(equalTo: leftButton.topAnchor), rightCatalogPreview.bottomAnchor.constraint(equalTo: rightButton.bottomAnchor), rightCatalogPreviewLabel.topAnchor.constraint(equalTo: rightCatalogPreview.topAnchor, constant: -20), rightCatalogPreviewLabel.leadingAnchor.constraint(equalTo: rightCatalogPreview.leadingAnchor), rightCatalogPreviewLabel.trailingAnchor.constraint(equalTo: rightCatalogPreview.trailingAnchor), leftCatalogPreview.trailingAnchor.constraint(equalTo: rightCatalogPreviewLabel.leadingAnchor, constant: -20), leftCatalogPreview.widthAnchor.constraint(equalToConstant: 55), leftCatalogPreview.topAnchor.constraint(equalTo: leftButton.topAnchor), leftCatalogPreview.bottomAnchor.constraint(equalTo: rightButton.bottomAnchor), leftCatalogPathLabel.centerYAnchor.constraint(equalTo: leftButton.centerYAnchor), leftCatalogPathLabel.leadingAnchor.constraint(equalTo: leftButton.trailingAnchor, constant: 10), leftCatalogPathLabel.trailingAnchor.constraint(equalTo: leftCatalogPreview.leadingAnchor, constant: -20), leftCatalogPreviewLabel.topAnchor.constraint(equalTo: leftCatalogPreview.topAnchor, constant: -20), leftCatalogPreviewLabel.leadingAnchor.constraint(equalTo: leftCatalogPreview.leadingAnchor), leftCatalogPreviewLabel.trailingAnchor.constraint(equalTo: leftCatalogPreview.trailingAnchor), rightCatalogPathLabel.centerYAnchor.constraint(equalTo: rightButton.centerYAnchor), rightCatalogPathLabel.leadingAnchor.constraint(equalTo: rightButton.trailingAnchor, constant: 10), rightCatalogPathLabel.trailingAnchor.constraint(equalTo: leftCatalogPreview.leadingAnchor, constant: -20), diffCatalogsButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -26.4), diffCatalogsButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: -25), ]) } override func viewDidAppear() { super.viewDidAppear() view.window?.title = "Diff Catalogs" view.window?.styleMask.remove(.resizable) } func makePreview(color: NSColor, side: DiffSide) -> DiffFilePreviewView { let preview = DiffFilePreviewView(side: side) preview.delegate = self return preview } @objc func diffButtonPressed() { guard let left = leftCatalogInput, let right = rightCatalogInput else { return } let rightCollection = right.collection.flatMap(\.renditions) let leftCollection = left.collection.flatMap(\.renditions) let diff = leftCollection.difference(from: rightCollection) { rend1, rend2 in return rend1.namedLookup.name == rend2.namedLookup.name } var finalDiffs: [RenditionDiff] = [] for meow in diff { switch meow { case .insert(_, let element, _): finalDiffs.append(RenditionDiff(rend: element, kind: .added)) case .remove(_, let element, _): finalDiffs.append(RenditionDiff(rend: element, kind: .removed)) } } WindowController(kind: .diffShow(finalDiffs, left.catalog, left.fileURL)).showWindow(nil) } @objc func leftOrRightButtonClicked(sender: NSButton) { URLHandler.shared.presentArchiveChooserPanel(senderView: nil) { [unowned self] url in validateAndProcessURL(url, forSide: DiffSide(rawValue: sender.tag)!) } } func validateAndProcessURL(_ url: URL, forSide side: DiffSide) { // if it's an .app, point to it's .car file let urlToChoose = url.pathExtension == "app" ? url.appendingPathComponent("Contents/Resources/Assets.car") : url guard FileManager.default.fileExists(atPath: urlToChoose.path) else { NSAlert(title: "Asset Catalog file \(urlToChoose.path) doesn't exist").runModal() return } do { switch side { case .left: leftCatalogInput = try AssetCatalogInput(fileURL: urlToChoose) leftCatalogPathLabel.stringValue = urlToChoose.path leftCatalogPreview.imageView.image = NSWorkspace.shared.icon(forFile: url.path) case .right: rightCatalogInput = try AssetCatalogInput(fileURL: urlToChoose) rightCatalogPathLabel.stringValue = urlToChoose.path rightCatalogPreview.imageView.image = NSWorkspace.shared.icon(forFile: url.path) } diffCatalogsButton.isEnabled = rightCatalogInput != nil && leftCatalogInput != nil } catch { NSAlert(title: "Unable to open Asset Catalog file \(urlToChoose.path)") .runModal() } } func setImageViewForPreview(url: URL, side: DiffSide) { switch side { case .left: leftCatalogPreview.imageView.image = NSWorkspace.shared.icon(forFile: url.path) case .right: rightCatalogPreview.imageView.image = NSWorkspace.shared.icon(forFile: url.path) } } } extension AssetCatalogDiffSelectionViewController: DiffFilePreviewDelegate { func diffFilePreview(_ view: DiffFilePreviewView, didGetURLDragged urlRecieved: URL) { validateAndProcessURL(urlRecieved, forSide: view.side) } } ================================================ FILE: Samra/UI/Diff/DiffFilePreviewView.swift ================================================ // // DiffFilePreviewView.swift // Samra // // Created by Serena on 03/05/2023. // import Cocoa class DiffFilePreviewView: NSView { let side: DiffSide let imageView = NSImageView() weak var delegate: DiffFilePreviewDelegate? init(side: DiffSide) { self.side = side super.init(frame: .zero) commonInit() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // setup the view func commonInit() { let previewBackgroundColor = NSColor(red: 0.22, green: 0.21, blue: 0.21, alpha: 1.00) let previewLayer = CALayer() previewLayer.backgroundColor = previewBackgroundColor.cgColor previewLayer.borderColor = NSColor.lightGray.cgColor previewLayer.borderWidth = 1.34 previewLayer.cornerRadius = 8 layer = previewLayer wantsLayer = true registerForDraggedTypes([.fileURL]) imageView.translatesAutoresizingMaskIntoConstraints = false addSubview(imageView) NSLayoutConstraint.activate([ imageView.heightAnchor.constraint(equalTo: heightAnchor), imageView.centerYAnchor.constraint(equalTo: centerYAnchor), imageView.centerXAnchor.constraint(equalTo: centerXAnchor) ]) } override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { return .generic } override func draggingEnded(_ sender: NSDraggingInfo) { sender.enumerateDraggingItems(for: nil, classes: [NSURL.self]) { [unowned self] item, t, ptr in let asURL = item.item as! URL delegate?.diffFilePreview(self, didGetURLDragged: asURL) } } } protocol DiffFilePreviewDelegate: AnyObject { func diffFilePreview(_ view: DiffFilePreviewView, didGetURLDragged: URL) } ================================================ FILE: Samra/UI/Diff/DiffListViewController.swift ================================================ // // DiffListViewController.swift // Samra // // Created by Serena on 07/03/2023. // import Cocoa import class SwiftUI.NSHostingController import AssetCatalogWrapper class DiffListViewController: NSViewController { typealias DataSource = NSCollectionViewDiffableDataSource var dataSource: DataSource! let diffs: [RenditionDiff] var catalog: CUICatalog var fileURL: URL init(diffs: [RenditionDiff], catalog: CUICatalog, fileURL: URL) { self.diffs = diffs self.catalog = catalog self.fileURL = fileURL super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { let collectionView = NSCollectionView() collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.collectionViewLayout = RenditionListViewController.makeLayout(layout: .horizontal) dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, rendition in let cell = collectionView.makeItem(withIdentifier: RenditionCollectionViewItem.reuseIdentifier, for: indexPath) as! RenditionCollectionViewItem cell.configure(rendition: rendition) return cell } dataSource.supplementaryViewProvider = { [unowned self] collectionView, kind, indexPath in guard kind == NSCollectionView.elementKindSectionHeader else { return nil } let header = collectionView.makeSupplementaryView( ofKind: kind, withIdentifier: RenditionTypeHeaderView.identifier, for: indexPath) as! RenditionTypeHeaderView let snapshot = dataSource.snapshot() let section = snapshot.sectionIdentifiers[indexPath.section] header.configure(typeLabelText: section.description, numberOfItems: snapshot.numberOfItems(inSection: section)) return header } collectionView.delegate = self collectionView.allowsMultipleSelection = false collectionView.isSelectable = true collectionView.register(RenditionCollectionViewItem.self, forItemWithIdentifier: RenditionCollectionViewItem.reuseIdentifier) collectionView.register(RenditionTypeHeaderView.self, forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, withIdentifier: RenditionTypeHeaderView.identifier) addSnapshot(diffs: diffs) let scrollView = NSScrollView() scrollView.verticalScroller = nil scrollView.documentView = collectionView scrollView.hasHorizontalScroller = false view = scrollView view.frame.size = CGSize(width: 724, height: 676) } func addSnapshot(diffs: [RenditionDiff]) { var snapshot = NSDiffableDataSourceSnapshot() // i want to cuddle a femboyyyy 🥺 let justSections = Set(diffs.map(\.kind)) // remove duplicates snapshot.appendSections(Array(justSections)) for diff in diffs { snapshot.appendItems([diff.rend], toSection: diff.kind) } dataSource.apply(snapshot) } override func performTextFinderAction(_ sender: Any?) { for item in view.window?.toolbar?.items ?? [] { if let search = item.view as? NSSearchField { search.becomeFirstResponder() break } } } } extension DiffListViewController: NSCollectionViewDelegate { func collectionView(_ collectionView: NSCollectionView, shouldSelectItemsAt indexPaths: Set) -> Set { return [indexPaths.first!] } func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { guard let first = indexPaths.first, let item = dataSource.itemIdentifier(for: first) else { return } let view = RenditionInformationView(rendition: item, catalog: catalog, fileURL: fileURL, canEdit: false, canDelete: false, changeCallback: nil) { [unowned self] in // done callback guard let currentlyBeingPresented = presentedViewControllers?.first else { return } dismiss(currentlyBeingPresented) } let controller = NSHostingController(rootView: view) controller.view.frame.size = CGSize(width: 650, height: 500) presentAsSheet(controller) } } extension DiffListViewController: NSSearchFieldDelegate { func controlTextDidChange(_ obj: Notification) { guard let searchText = (obj.object as? NSSearchField)?.stringValue else { return } if searchText.isEmpty { addSnapshot(diffs: diffs) return } let new = diffs.filter { diff in return diff.rend.name.localizedCaseInsensitiveContains(searchText) } addSnapshot(diffs: new) } } ================================================ FILE: Samra/UI/MenuableCollectionView.swift ================================================ // // MenuableCollectionView.swift // Samra // // Created by Serena on 02/03/2023. // import Cocoa class CollectionViewWithMenu: NSCollectionView { weak var menuProvider: MenuProvider? override func menu(for event: NSEvent) -> NSMenu? { guard event.type == .rightMouseDown, let indexPath = indexPathForItem( at: convert(event.locationInWindow, from: nil) ) else { return nil } return menuProvider?.collectionView(self, menuForItemAt: indexPath) } } protocol MenuProvider: AnyObject { func collectionView(_ collectionView: NSCollectionView, menuForItemAt: IndexPath) -> NSMenu? } ================================================ FILE: Samra/UI/PastFilesListViewController.swift ================================================ // // PastFilesListViewController.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import QuickLookUI import AssetCatalogWrapper /// A View Controller showing the past files opened class PastFilesListViewController: NSViewController { var paths: [String] = Preferences.recentlyOpenedFilePaths.reversed() var tableView: NSTableView! override func loadView() { tableView = NSTableView() tableView.dataSource = self tableView.delegate = self tableView.headerView = nil tableView.doubleAction = #selector(doubeClickedItem) let col = NSTableColumn(identifier: "Column") tableView.addTableColumn(col) let menu = NSMenu() menu.delegate = self menu.addItem(withTitle: "Show in Finder", action: #selector(showInFinder), keyEquivalent: "") menu.addItem(withTitle: "Remove", action: #selector(deleteItem), keyEquivalent: "") menu.autoenablesItems = false tableView.menu = menu let scrollView = NSScrollView() scrollView.documentView = tableView scrollView.hasHorizontalScroller = false view = scrollView view.frame.size = CGSize(width: 250, height: 0) } } extension PastFilesListViewController { // Menu item actions @objc func deleteItem() { guard tableView.clickedRow >= 0 else { return } paths.remove(at: tableView.clickedRow) Preferences.recentlyOpenedFilePaths = paths.reversed() tableView.removeRows(at: [tableView.clickedRow], withAnimation: [.slideRight]) } @objc func showInFinder() { guard tableView.clickedRow >= 0 else { return } let item = paths[tableView.clickedRow] NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: item)]) } } extension PastFilesListViewController: NSMenuDelegate { func menuNeedsUpdate(_ menu: NSMenu) { // if no item is selected, then disable the menu items let keepItemsEnabled = tableView.clickedRow >= 0 for item in menu.items { item.isEnabled = keepItemsEnabled } } override func keyDown(with event: NSEvent) { guard tableView.selectedRow != -1 else { return } super.keyDown(with: event) // space, show QuickLook if event.characters == " " { if let sharedPanel = QLPreviewPanel.shared() { let url = URL(fileURLWithPath: paths[tableView.selectedRow]) let source = QuickLookPreviewSource(fileURL: url) sharedPanel.dataSource = source sharedPanel.makeKeyAndOrderFront(nil) } } // carriage return, open up the item if event.characters == "\r" { doubeClickedItem() } } } extension PastFilesListViewController: NSTableViewDataSource, NSTableViewDelegate { func numberOfRows(in tableView: NSTableView) -> Int { return paths.count } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { let rowView = NSTableRowView() rowView.isEmphasized = false return rowView } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let item = URL(fileURLWithPath: paths[row]) let cell = NSTableCellView() let imageView = NSImageView(image: NSWorkspace.shared.icon(forFile: item.path)) let text = NSTextField(labelWithString: item.lastPathComponent) let subtitleText = NSTextField(labelWithString: item.deletingLastPathComponent().path) if #available(macOS 11, *) { subtitleText.font = .preferredFont(forTextStyle: .subheadline) } else { subtitleText.font = .systemFont(ofSize: 11) } subtitleText.lineBreakMode = .byTruncatingMiddle subtitleText.textColor = .secondaryLabelColor let titlesStackView = NSStackView(views: [text, subtitleText]) titlesStackView.alignment = .left titlesStackView.distribution = .equalCentering titlesStackView.orientation = .vertical titlesStackView.spacing = 0 let stackView = NSStackView(views: [imageView, titlesStackView]) stackView.translatesAutoresizingMaskIntoConstraints = false cell.addSubview(stackView) NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: cell.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: cell.trailingAnchor), stackView.centerYAnchor.constraint(equalTo: cell.centerYAnchor), ]) return cell } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 40 } @objc func doubeClickedItem() { guard tableView.selectedRow != -1 else { return } var copy = Preferences.recentlyOpenedFilePaths let item = paths[tableView.selectedRow] copy.removeAll { $0 == item } // remove if exists copy.append(item) // add item to amke it most recent Preferences.recentlyOpenedFilePaths = copy paths = Array(copy) URLHandler.shared.handleURLChosen(urlChosen: URL(fileURLWithPath: item), senderView: view) } } ================================================ FILE: Samra/UI/Rendition/AssetCatalogDetailsView.swift ================================================ // // AssetCatalogDetailsView.swift // Samra // // Created by Serena on 27/02/2023. // import SwiftUI import AssetCatalogWrapper /// Shows information about a given asset catalog. struct AssetCatalogDetailsView: View { var assetStorage: CUICommonAssetStorage var doneCallback: () -> Void var body: some View { mainView .frame(width: 630, height: 450) } @ViewBuilder var mainView: some View { List(DetailItemSection.from(assetStorage: assetStorage), id: \.self) { section in Section(header: Text(section.sectionHeader)) { ForEach(section.items, id: \.self) { item in HStack { Text(item.primaryText) .foregroundColor(Color(NSColor.secondaryLabelColor)) Spacer() Text(item.secondaryText) .multilineTextAlignment(.center) } .contextMenu { Button("Copy") { NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(item.secondaryText, forType: .string) } } } } } Spacer() Divider() Button("Done", action: doneCallback) .frame(height: 35, alignment: .center) } } ================================================ FILE: Samra/UI/Rendition/RenditionCollectionViewItem.swift ================================================ // // RenditionCollectionViewItem.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import AssetCatalogWrapper class RenditionCollectionViewItem: NSCollectionViewItem { static let reuseIdentifier = NSUserInterfaceItemIdentifier("RenditionCollectionViewItem") var nameLabel: NSTextField! var representationPreview: NSView! override func loadView() { view = NSView() } func configure(rendition: Rendition) { nameLabel = NSTextField(labelWithString: rendition.name) nameLabel.translatesAutoresizingMaskIntoConstraints = false nameLabel.maximumNumberOfLines = 0 nameLabel.alignment = .center nameLabel.lineBreakMode = .byCharWrapping switch rendition.representation { case .color(let cGColor): let circleView = NSView() circleView.translatesAutoresizingMaskIntoConstraints = false let layer = CALayer() layer.cornerRadius = 20 layer.cornerCurve = .circular layer.backgroundColor = cGColor circleView.wantsLayer = false circleView.layer = layer view.addSubview(circleView) NSLayoutConstraint.activate([ circleView.centerXAnchor.constraint(equalTo: view.centerXAnchor), circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -10), circleView.heightAnchor.constraint(equalToConstant: 40), circleView.widthAnchor.constraint(equalToConstant: 40) ]) representationPreview = circleView case .image(let cGImage): let imageView = NSImageView() imageView.image = NSImage(cgImage: cGImage, size: cGImage.size) imageView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(imageView) imageView.imageScaling = .scaleProportionallyUpOrDown imageView.imageAlignment = .alignCenter // imageView.centerConstraints(to: view) NSLayoutConstraint.activate([ imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -12.34), ]) /* if #available(macOS 11, *) { NSLayoutConstraint.activate([ imageView.widthAnchor.constraint(equalTo: view.layoutMarginsGuide.widthAnchor), imageView.heightAnchor.constraint(equalTo: view.layoutMarginsGuide.heightAnchor) ]) } else {*/ NSLayoutConstraint.activate([ imageView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -20), imageView.heightAnchor.constraint(equalTo: view.heightAnchor, constant: -34) ]) /*}*/ representationPreview = imageView case .rawData(let data): var visibleString = "No Preview Available" if let string = String(data:data, encoding:.utf8) { visibleString = string.count > 500 ? String(string.prefix(500)) : string } let textField = NSTextField(wrappingLabelWithString:visibleString) textField.isBordered = true textField.isEditable = false textField.isSelectable = false textField.translatesAutoresizingMaskIntoConstraints = false textField.maximumNumberOfLines = 5 view.addSubview(textField) representationPreview = textField NSLayoutConstraint.activate([ textField.centerXAnchor.constraint(equalTo: view.centerXAnchor), textField.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -12.34), textField.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -20), textField.heightAnchor.constraint(equalTo: view.heightAnchor, constant: -34) ]) case nil: representationPreview = .init() } view.addSubview(nameLabel) NSLayoutConstraint.activate([ nameLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10) ]) let layer = CALayer() layer.borderWidth = 1.87 layer.cornerRadius = 10 layer.cornerCurve = .continuous layer.masksToBounds = true layer.borderColor = NSColor.systemGray.cgColor view.layer = layer } override func prepareForReuse() { super.prepareForReuse() nameLabel.stringValue = "" representationPreview.removeFromSuperview() representationPreview = nil } } ================================================ FILE: Samra/UI/Rendition/RenditionInformationView.swift ================================================ // // RenditionInformationView.swift // Samra // // Created by Serena on 21/02/2023. // import SwiftUI import UniformTypeIdentifiers import AssetCatalogWrapper struct RenditionInformationView: View { @State var showDeleteAlert: Bool = false var rendition: Rendition var catalog: CUICatalog var fileURL: URL var canEdit: Bool var canDelete: Bool var changeCallback: ((Change) -> Void)? var doneButtonCallback: (() -> Void)? var body: some View { switch rendition.representation { case .image(let cgImage): // GeometryReader { proxy in Image(cgImage, scale: NSScreen.main!.backingScaleFactor, label: Text("")) .resizable() .aspectRatio(contentMode: .fit) // .frame(width: proxy.size.width, // height: proxy.size.height, alignment: .center) // } .frame(alignment: .center) .contextMenu { Button("Copy Image") { NSPasteboard.general.declareTypes([.tiff], owner: nil) NSPasteboard.general.setData(NSImage(cgImage: cgImage, size: cgImage.size).tiffRepresentation, forType: .tiff) } Button("Save Image As..") { let panel = NSSavePanel() panel.nameFieldStringValue = rendition.cuiRend.name() if panel.runModal() == .OK, let chosenURL = panel.url { let rep = NSBitmapImageRep(cgImage: cgImage) guard let data = rep.representation(using: .png, properties: [.compressionFactor: 1]) else { NSAlert(title: "Unable to generate png data for image").runModal() return } do { try data.write(to: chosenURL, options: .atomic) } catch { NSAlert(title: "Unable to write image data to \(chosenURL.path)", message: error.localizedDescription).runModal() } } } } /* .onDrag { } */ case .color(let cgColor): Circle() .fill(Color(NSColor(cgColor: cgColor)!)) .frame(width: 130, height: 230, alignment: .center) case .rawData(let data): if let string = String(data:data, encoding:.utf8) { Text(String(string.prefix(1024))) .font(.body) .padding(5) } else { Text("No Preview Available") .font(.title.italic()) .padding(30) } default: Text("No Preview Available.") .font(.title.italic()) .frame(width: 130, height: 230) } HStack { if rendition.type == .rawData, rendition.cuiRend.responds(to: #selector(CUIThemeRendition.data)) { Button("Export Data to...") { guard let data = rendition.cuiRend.data() else { NSAlert(title: "Failed to export data", message: "Unable to get data (rendition.cuiRend.data() returned null)") .runModal() return } let savePanel = NSSavePanel() savePanel.nameFieldStringValue = rendition.name if savePanel.runModal() == .OK, let url = savePanel.url { do { try data.write(to: url) } catch { NSAlert(title: "Error trying to write data to file \(url)", message: error.localizedDescription) .runModal() } } } } Button("Edit") { switch rendition.representation { case .color(let cgColor): let colorPanel = CallbackableColorPanel() colorPanel.color = NSColor(cgColor: cgColor) ?? colorPanel.color colorPanel.isContinuous = false colorPanel.makeKeyAndOrderFront(nil) colorPanel.callback = { nsColor in do { try catalog.editItem(rendition, fileURL: fileURL, to: .color(nsColor.cgColor)) changeCallback?(.edit) } catch { NSAlert(title: "Failed to edit item", message: error.localizedDescription) .runModal() } } case .image(_): let panel = NSOpenPanel() panel.canChooseDirectories = false panel.canChooseFiles = true if #available(macOS 11, *) { panel.allowedContentTypes = [.image] } else { panel.allowedFileTypes = [kUTTypeImage as String] } if panel.runModal() == .OK, let chosenURL = panel.url { guard let cgImage = NSImage(contentsOf: chosenURL)?.cgImage(forProposedRect: nil, context: nil, hints: nil) else { NSAlert(title: "Failed to edit item", message: "Unable to get image representation of the file selected").runModal() return } do { try catalog.editItem(rendition, fileURL: fileURL, to: .image(cgImage)) changeCallback?(.edit) } catch { NSAlert(title: "Failed to edit item", message: error.localizedDescription) .runModal() } } default: break // never supposed to get here } } .disabled(!canEdit || !rendition.type.isEditable) if let doneButtonCallback { Button("Done", action: doneButtonCallback) } Button { showDeleteAlert = true } label: { Text("Delete") .foregroundColor(.red) } .disabled(!canDelete) } mainView .frame(maxWidth: .infinity, maxHeight: .infinity) .alert(isPresented: $showDeleteAlert) { let deleteButton: Alert.Button = .destructive(Text("Delete")) { do { try catalog.removeItem(rendition, fileURL: fileURL) changeCallback?(.delete) } catch { NSAlert(title: "Error encountered while trying to delete \(rendition.name)", message: error.localizedDescription).runModal() } } return Alert(title: Text("Are you sure you want to delete \(rendition.name)?"), message: Text("This action cannot be undone"), primaryButton: deleteButton, secondaryButton: .cancel()) } } @ViewBuilder var mainView: some View { List(DetailItemSection.from(rendition: rendition), id: \.self) { section in Section(header: Text(section.sectionHeader)) { ForEach(section.items, id: \.self) { item in HStack { Text(item.primaryText) Spacer() Text(item.secondaryText) .multilineTextAlignment(.trailing) } .contextMenu { Button("Copy") { NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(item.secondaryText, forType: .string) } } } } } } enum Change { /// item was deleted case delete /// item was edited case edit } } class CallbackableColorPanel: NSColorPanel, NSWindowDelegate { var callback: ((NSColor) -> Void)? = nil override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) self.delegate = self } func windowWillClose(_ notification: Notification) { callback?(color) } } ================================================ FILE: Samra/UI/Rendition/RenditionListViewController.swift ================================================ // // RenditionListViewController.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import AppKitPrivates import class SwiftUI.NSHostingController import AssetCatalogWrapper import SVGWrapper /// A View Controller displaying all the renditions of a given Asset Catalog. class RenditionListViewController: NSViewController { static let titleHeaderIdentifier = "Identifier" typealias DataSource = NSCollectionViewDiffableDataSource var dataSource: DataSource! var collectionView: CollectionViewWithMenu! lazy var allItemsSnapshot = addSnapshot(collectionToAdd: collection) var itemToDeleteIndexPath: IndexPath? = nil var catalog: CUICatalog var collection: RenditionCollection let fileURL: URL private var scrollObserver: NSObjectProtocol? init(catalog: CUICatalog, collection: RenditionCollection, fileURL: URL) { self.catalog = catalog self.collection = collection self.fileURL = fileURL super.init(nibName: nil, bundle: nil) } var splitViewParent: CollapseNotifierSplitViewController? { parent as? CollapseNotifierSplitViewController } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { collectionView = CollectionViewWithMenu() dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, rendition in let cell = collectionView.makeItem(withIdentifier: RenditionCollectionViewItem.reuseIdentifier, for: indexPath) as! RenditionCollectionViewItem cell.configure(rendition: rendition) return cell } #warning("Add footers for explanations for multisizeImageSet") dataSource.supplementaryViewProvider = { [unowned self] collectionView, kind, indexPath in guard kind == NSCollectionView.elementKindSectionHeader else { return nil } let header = collectionView.makeSupplementaryView( ofKind: kind, withIdentifier: RenditionTypeHeaderView.identifier, for: indexPath) as! RenditionTypeHeaderView let snapshot = dataSource.snapshot() let section = snapshot.sectionIdentifiers[indexPath.section] header.configure(typeLabelText: section.description, numberOfItems: snapshot.numberOfItems(inSection: section)) return header } collectionView.allowsMultipleSelection = false collectionView.isSelectable = true collectionView.delegate = self collectionView.menuProvider = self collectionView.collectionViewLayout = Self.makeLayout(layout: .horizontal) collectionView.identifier = "HorizLayout" collectionView.register(RenditionCollectionViewItem.self, forItemWithIdentifier: RenditionCollectionViewItem.reuseIdentifier) collectionView.register(RenditionTypeHeaderView.self, forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, withIdentifier: RenditionTypeHeaderView.identifier) addSnapshot(collectionToAdd: collection) splitViewParent?.handler = { [unowned self] item, didCollapse, _ in guard item.viewController.identifier == "RenditionInfo" else { return } collectionView.collectionViewLayout = Self.makeLayout( layout: didCollapse ? .horizontal : .vertical ) collectionView.identifier = didCollapse ? "HorizLayout" : "VerticalLayout" } let scrollView = NSScrollView() scrollView.verticalScroller = nil scrollView.documentView = collectionView scrollView.hasHorizontalScroller = false view = scrollView view.frame.size = CGSize(width: 724, height: 676) let observer = NotificationCenter.default.addObserver(forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, queue: nil) { [weak self] _ in guard let self = self else { return } let vc = self.splitViewParent?.splitViewItems[0].viewController as? TypesListViewController guard let vc, let currentSection = self.collectionView.indexPathsForVisibleItems().first?.section else { return } vc.ignoreChanges = true vc.tableView.deselectRow(vc.tableView.selectedRow) vc.tableView.selectRowIndexes([currentSection], byExtendingSelection: true) vc.ignoreChanges = false } self.scrollObserver = observer collectionView.registerForDraggedTypes(NSImage.imageTypes.map { .init($0) }) collectionView.setDraggingSourceOperationMask(.every, forLocal: true) collectionView.setDraggingSourceOperationMask(.every, forLocal: false) } func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set, with event: NSEvent) -> Bool { return true } func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? { // return dataSource.itemIdentifier(for: indexPath)?.name as? NSString switch dataSource.itemIdentifier(for: indexPath)?.representation { case .image(let cgImage): return NSImage(cgImage: cgImage, size: cgImage.size) default: return nil } } func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {} @discardableResult func addSnapshot(collectionToAdd: RenditionCollection) -> NSDiffableDataSourceSnapshot { var snapshot = NSDiffableDataSourceSnapshot() for item in collectionToAdd { snapshot.appendSections([item.type]) snapshot.appendItems(item.renditions, toSection: item.type) } dataSource.apply(snapshot) return snapshot } @discardableResult func refreshAssetCatalog() -> Bool { do { let (newCatalog, newCollection) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL) self.catalog = newCatalog self.collection = newCollection addSnapshot(collectionToAdd: collection) return true } catch { NSAlert(title: "Failed to refresh Asset Catalog", message: error.localizedDescription) .runModal() return false } } deinit { if let observer = scrollObserver { NotificationCenter.default.removeObserver(observer) } print("And I'm tripping and falling..") } } extension RenditionListViewController { static func makeLayout(layout: LayoutMode) -> NSCollectionViewCompositionalLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(115)) let group: NSCollectionLayoutGroup switch layout { case .vertical: group = .vertical(layoutSize: groupSize, subitems: [item]/*, count: 3*/) case .horizontal: group = .horizontal(layoutSize: groupSize, subitem: item, count: 3) } let spacing = CGFloat(15) group.interItemSpacing = .fixed(spacing) let titleHeaderSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50) ) let titleSupplementary = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: titleHeaderSize, elementKind: NSCollectionView.elementKindSectionHeader, alignment: .topTrailing ) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = spacing section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: spacing, bottom: 20, trailing: spacing) section.boundarySupplementaryItems = [titleSupplementary] //section.orthogonalScrollingBehavior = .continuous return NSCollectionViewCompositionalLayout(section: section) } } extension RenditionListViewController: MenuProvider { static private func _promptToSaveImage(cgImage: CGImage, formatType: NSBitmapImageRep.FileType, defaultFileName: String, displayFormat: String) { let savePanel = NSSavePanel() savePanel.nameFieldStringValue = defaultFileName guard savePanel.runModal() == .OK, let urlToSaveTo = savePanel.url else { return } guard let data = NSBitmapImageRep(cgImage: cgImage).representation(using: formatType, properties: [.compressionFactor: 1]) else { NSAlert(title: "Failed to save Image as \(displayFormat)", message: "NSBitmapImageRep representation returned nil.").runModal() return } do { try data.write(to: urlToSaveTo) } catch { NSAlert(title: "Failed to save Image as \(displayFormat)", message: error.localizedDescription).runModal() } } func collectionView(_ collectionView: NSCollectionView, menuForItemAt indexPath: IndexPath) -> NSMenu? { guard let item = dataSource.itemIdentifier(for: indexPath) else { return nil } let copyName = ClosureMenuItem(title: "Copy Name") { NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(item.name, forType: .string) } var items: [NSMenuItem] = [copyName] switch item.representation { case .image(let cgImage): let copyImage = ClosureMenuItem(title: "Copy Image") { NSPasteboard.general.declareTypes([.tiff], owner: nil) NSPasteboard.general.setData(NSImage(cgImage: cgImage, size: cgImage.size).tiffRepresentation, forType: .tiff) } items.append(copyImage) var saveImageAsItems = [ ClosureMenuItem(title: "PNG") { Self._promptToSaveImage(cgImage: cgImage, formatType: .png, defaultFileName: "image.png", displayFormat: "PNG") }, ClosureMenuItem(title: "JPEG") { Self._promptToSaveImage(cgImage: cgImage, formatType: .jpeg, defaultFileName: "image.jpeg", displayFormat: "JPEG") } ] if item.type == .svg, let svgDoc = item.cuiRend.svgDocument() { let asSVG = ClosureMenuItem(title: "SVG") { let savePanel = NSSavePanel() savePanel.nameFieldStringValue = "image.svg" guard savePanel.runModal() == .OK, let urlToSaveTo = savePanel.url else { return } CGSVGDocumentWriteToURL(svgDoc, urlToSaveTo as CFURL, nil) } saveImageAsItems.insert(asSVG, at: 0) } let saveImageAs = NSMenuItem(submenuTitle: "Save Image As...", items: saveImageAsItems) items.insert(saveImageAs, at: 0) items.insert(.separator(), at: 1) default: break } let deleteItem = ClosureMenuItem(title: "Delete") { [unowned self] in let alert = NSAlert(title: "Are you sure you want to delete \(item.name)?", message: "This action cannot be undone") let deleteButton = alert.addButton(withTitle: "Delete") deleteButton.target = self deleteButton.action = #selector(deleteItem(sender:)) if #available(macOS 11, *) { deleteButton.hasDestructiveAction = true } itemToDeleteIndexPath = indexPath alert.addButton(withTitle: "Cancel") alert.runModal() } items.append(deleteItem) return NSMenu(items: items) } @objc func deleteItem(sender: NSButton) { guard let itemToDeleteIndexPath, let item = dataSource.itemIdentifier(for: itemToDeleteIndexPath) else { return } do { try catalog.removeItem(item, fileURL: fileURL) NSApplication.shared.abortModal() refreshAssetCatalog() } catch { NSAlert(title: "Failed to remove \(item.name)", message: error.localizedDescription) .runModal() return } } } extension RenditionListViewController { @objc func infoButtonClicked(sender: NSButton) { guard let ass = CUICommonAssetStorage(path: fileURL.path, forWriting: false) else { NSAlert( title: "Failed to display details of Assets.car file", message: "Failed to init CUICommonAssetStorage for \(fileURL.path)" ) .runModal() return } /* let popover = NSPopover() popover.behavior = .transient popover.contentSize = NSSize(width: 400, height: 200) */ let detailsView = AssetCatalogDetailsView(assetStorage: ass) { [unowned self] in // Callback for 'Done' button guard let currentlyPresenting = presentedViewControllers?.first else { return } dismiss(currentlyPresenting) } presentAsSheet(NSHostingController(rootView: detailsView)) } @objc func exportCatalog() { let panel = NSOpenPanel() panel.title = "Directory to export to" panel.canChooseDirectories = true panel.canCreateDirectories = true panel.canChooseFiles = false guard panel.runModal() == .OK, let destinationURL = panel.url else { return } do { try AssetCatalogWrapper.shared.extract(collection: collection, to: destinationURL) NSWorkspace.shared.activateFileViewerSelecting([destinationURL]) } catch { NSAlert(title: "Failed to export (some) items", message: error.localizedDescription) .runModal() } } } extension RenditionListViewController { // MARK: - Layout enum LayoutMode { case vertical case horizontal } } extension RenditionListViewController: NSCollectionViewDelegate { func collectionView(_ collectionView: NSCollectionView, shouldSelectItemsAt indexPaths: Set) -> Set { return [indexPaths.first!] } func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { guard let firstIndexPath = indexPaths.first, let item = dataSource.itemIdentifier(for: firstIndexPath), let parent = splitViewParent else { return } let layer = collectionView.item(at: firstIndexPath)?.view.layer layer?.borderColor = NSColor.controlAccentColor.cgColor layer?.borderWidth = 3.5 // enlargen border width when selected // if we already have an existing info vc then remove it if parent.splitViewItems.count == 3 { parent.removeSplitViewItem(parent.splitViewItems[2]) } let view = RenditionInformationView(rendition: item, catalog: catalog, fileURL: fileURL, canEdit: true, canDelete: true) { [unowned self] change in switch change { case .delete: refreshAssetCatalog() case .edit: if refreshAssetCatalog() { self.collectionView(collectionView, didSelectItemsAt: indexPaths) } } } let renditionVC = NSHostingController(rootView: view) renditionVC.identifier = "RenditionInfo" let splitViewItem = NSSplitViewItem(contentListWithViewController: renditionVC) splitViewItem.minimumThickness = 400 splitViewItem.canCollapse = true splitViewItem.maximumThickness = 600 splitViewItem.automaticMaximumThickness = 600 splitViewItem.preferredThicknessFraction = 2 parent.addSplitViewItem(splitViewItem) if collectionView.identifier == "HorizLayout" { collectionView.collectionViewLayout = Self.makeLayout(layout: .vertical) collectionView.identifier = "VerticalLayout" // scroll back here because switching between layouts may cause the item to not be visible // in the new layout collectionView.scrollToItems(at: indexPaths, scrollPosition: [.centeredVertically, .centeredHorizontally]) } } func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { for indexPath in indexPaths { let layer = collectionView.item(at: indexPath)?.view.layer layer?.borderColor = NSColor.systemGray.cgColor // item is no longer in focus, set it's border width to the standard amount layer?.borderWidth = 1.87 } } override func performTextFinderAction(_ sender: Any?) { for item in view.window?.toolbar?.items ?? [] { if let search = item.view as? NSSearchField { search.becomeFirstResponder() break } } } /* func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set, with event: NSEvent) -> Bool { return indexPaths.allSatisfy { [unowned self] indxPath in switch dataSource.itemIdentifier(for: indxPath)?.type { case .image, .icon: return true default: return false } } } func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set) { print(#function) } */ } extension RenditionListViewController: NSSearchFieldDelegate { /// Set the types in the sidebar, /// if nil, then this will default to all the types func setSidebarTypes(_ types: [RenditionType]?) { if let sidebar = splitViewParent?.splitViewItems[0].viewController as? TypesListViewController { sidebar.types = types ?? sidebar.allTypes sidebar.tableView.reloadData() } } func controlTextDidChange(_ obj: Notification) { guard let searchText = (obj.object as? NSSearchField)?.stringValue else { return } if searchText.isEmpty { dataSource.apply(allItemsSnapshot) setSidebarTypes(nil) return } var newSidebarTypes: [RenditionType] = [] let newCollection: RenditionCollection = collection.compactMap { type, renditions in // query by the renditions that have the search text in their name let newRends = renditions.filter { rend in return rend.name.localizedCaseInsensitiveContains(searchText) } // Don't include the section if no items match the query if newRends.isEmpty { return nil } // the section has renditions that match our description, add it to the sidebar newSidebarTypes.append(type) return (type, newRends) } addSnapshot(collectionToAdd: newCollection) setSidebarTypes(newSidebarTypes) } } ================================================ FILE: Samra/UI/Rendition/RenditionTypeHeaderView.swift ================================================ // // RenditionTypeHeaderView.swift // Samra // // Created by Serena on 19/02/2023. // import Cocoa import AssetCatalogWrapper class RenditionTypeHeaderView: NSView, NSCollectionViewElement { static let identifier = NSUserInterfaceItemIdentifier("RenditionTypeHeaderView") var typeLabel: NSTextField! var amountOfItemsLabel: NSTextField! func configure(typeLabelText: String, numberOfItems: Int) { typeLabel = NSTextField(labelWithString: typeLabelText) typeLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(typeLabel) amountOfItemsLabel = NSTextField(labelWithString: "\(numberOfItems) Items") amountOfItemsLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(amountOfItemsLabel) if #available(macOS 11, *) { typeLabel.font = .preferredFont(forTextStyle: .largeTitle) amountOfItemsLabel.font = .preferredFont(forTextStyle: .caption1) } else { amountOfItemsLabel.font = .systemFont(ofSize: 10) typeLabel.font = .systemFont(ofSize: 26) } NSLayoutConstraint.activate([ typeLabel.leadingAnchor.constraint(equalTo: leadingAnchor), typeLabel.topAnchor.constraint(equalTo: topAnchor), amountOfItemsLabel.leadingAnchor.constraint(equalTo: leadingAnchor), amountOfItemsLabel.centerXAnchor.constraint(equalTo: typeLabel.centerXAnchor, constant: -30) ]) } override func prepareForReuse() { super.prepareForReuse() typeLabel.removeFromSuperview() typeLabel = nil amountOfItemsLabel.removeFromSuperview() amountOfItemsLabel = nil } } ================================================ FILE: Samra/UI/Rendition/TypesListViewController.swift ================================================ // // TypesListViewController.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import AssetCatalogWrapper class TypesListViewController: NSViewController { typealias SectionClickedHandler = (RenditionType) -> Void let changeHandler: SectionClickedHandler let allTypes: [RenditionType] // the types shown in the UI, if there is a search session, this may not be equal to allTypes // depending on if the search result's types are less than allTypes var types: [RenditionType] // for when manually doing select and deselectRow var ignoreChanges: Bool = false var tableView: NSTableView! init(types: [RenditionType], changeHandler: @escaping SectionClickedHandler) { self.types = types self.allTypes = types self.changeHandler = changeHandler super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { tableView = NSTableView() tableView.dataSource = self tableView.delegate = self tableView.target = self tableView.headerView = nil let col = NSTableColumn(identifier: "Column") tableView.addTableColumn(col) let scrollView = NSScrollView() scrollView.documentView = tableView scrollView.hasHorizontalScroller = false view = scrollView view.frame.size = CGSize(width: 200, height: 0) setupMenuBarItems() } override func viewDidDisappear() { super.viewDidDisappear() // disable section items for item in NSApplication.shared.mainMenu?.items ?? [] { guard item.title == "Sections", let submenu = item.submenu else { continue } for item in submenu.items { item.isEnabled = false item.keyEquivalent = "" } } } override func performTextFinderAction(_ sender: Any?) { for item in view.window?.toolbar?.items ?? [] { if let search = item.view as? NSSearchField { search.becomeFirstResponder() break } } } // Map this function to the main list vc exportCatalog function @objc func exportCatalog() { for item in (parent as? NSSplitViewController)?.splitViewItems ?? [] { if let list = item.viewController as? RenditionListViewController { list.exportCatalog() break } } } func setupMenuBarItems() { for item in NSApplication.shared.mainMenu?.items ?? [] { // we just want to modify the "Sections" section guard item.title == "Sections", let submenu = item.submenu else { continue } submenu.autoenablesItems = false submenu.removeAllItems() // add only the types that we have // to the section for (index, item) in allTypes.enumerated() { // make the keyEquivalent index + 1 // so that it's less confusing to the user, // ie, if `Color` was the first section, this would make it cmd 1 // rather than cmd 0 let item = NSMenuItem(title: item.description, action: #selector(goToSection), keyEquivalent: (index + 1).description, tag: index) submenu.addItem(item) } } } @objc func goToSection(menuItemSender: NSMenuItem) { changeSection(to: menuItemSender.tag) } } extension TypesListViewController: NSTableViewDataSource, NSTableViewDelegate { func numberOfRows(in tableView: NSTableView) -> Int { return types.count } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 30 } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let type = types[row] let cell = NSTableCellView() let imageIconView = NSImageView() imageIconView.image = NSImage(systemName: type.displayIconName) let stackView = NSStackView(views: [imageIconView, NSTextField(labelWithString: type.description)]) stackView.translatesAutoresizingMaskIntoConstraints = false cell.addSubview(stackView) NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: cell.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: cell.trailingAnchor), stackView.centerYAnchor.constraint(equalTo: cell.centerYAnchor) ]) return cell } func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { return true } func changeSection(to index: Int) { if !ignoreChanges { changeHandler(types[index]) } } func tableViewSelectionDidChange(_ notification: Notification) { changeSection(to: tableView.selectedRow) } } extension RenditionType { var displayIconName: String { switch self { case .image, .svg: return "photo" case .icon: return "app" case .imageSet: if #available(macOS 13, iOS 16, *) { return "photo.stack" } return "rectangle.stack" case .multiSizeImageSet: return "cube.box" case .pdf: return "doc.richtext" case .color: return "paintbrush" case .rawData: return "text.quote" case .unknown: return "questionmark.app" } } } ================================================ FILE: Samra/UI/WelcomeScreenOption.swift ================================================ // // WelcomeScreenOption.swift // Samra // // Created by Serena on 21/02/2023. // import Cocoa /// Represents an option on the main menu screen, /// similar to that of Xcode's. class WelcomeScreenOption: NSView { var actionClosure: () -> Void init(primaryText: String, secondaryText: String, image: NSImage?, action: @escaping () -> Void) { self.actionClosure = action super.init(frame: .zero) let finalImage: NSImage? if #available(macOS 11, *) { finalImage = image? .withSymbolConfiguration(.init(pointSize: 30, weight: .regular)) } else { finalImage = image } let finalImageView = NSImageView() finalImageView.image = finalImage finalImageView.contentTintColor = .controlAccentColor let primaryTextLabel = NSTextField(labelWithString: primaryText) let secondaryTextLabel = NSTextField(labelWithString: secondaryText) secondaryTextLabel.textColor = .secondaryLabelColor if #available(macOS 11, *) { primaryTextLabel.font = .preferredFont(forTextStyle: .headline) secondaryTextLabel.font = .preferredFont(forTextStyle: .subheadline) } else { primaryTextLabel.font = .boldSystemFont(ofSize: 13) secondaryTextLabel.font = .systemFont(ofSize: 11) } let textLabelsStackView = NSStackView(views: [primaryTextLabel, secondaryTextLabel]) textLabelsStackView.alignment = .left textLabelsStackView.spacing = 0.4 textLabelsStackView.orientation = .vertical let completeStackView = NSStackView(views: [finalImageView, textLabelsStackView]) completeStackView.addGestureRecognizer(NSClickGestureRecognizer(target: self, action: #selector(performAction))) completeStackView.orientation = .horizontal completeStackView.translatesAutoresizingMaskIntoConstraints = false addSubview(completeStackView) completeStackView.constraintCompletely(to: self) } @objc func performAction() { actionClosure() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Samra/UI/WelcomeViewController.swift ================================================ // // WelcomeViewController.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import AssetCatalogWrapper class WelcomeViewController: NSViewController { // override so that it doesn't try to load a fucking nib override func loadView() { view = NSView() view.frame.size = CGSize(width: 570, height: 460) } override func viewDidLoad() { super.viewDidLoad() let appIcon = NSImageView(image: NSApplication.shared.applicationIconImage) let welcomeTextLabel = NSTextField(labelWithString: "Welcome to Samra") welcomeTextLabel.font = .systemFont(ofSize: 30) let subtitleLabel = NSTextField(labelWithString: "Created by Antoine (formerly known as Serena)") subtitleLabel.textColor = .secondaryLabelColor let stackView = NSStackView(views: [appIcon, welcomeTextLabel, subtitleLabel]) stackView.orientation = .vertical stackView.translatesAutoresizingMaskIntoConstraints = false stackView.spacing = 0.3 view.addSubview(stackView) NSLayoutConstraint.activate([ stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -40) ]) let openFolderOption = WelcomeScreenOption( primaryText: "Open Assets File", secondaryText: "Browse and Edit Assets Files on your Mac", image: NSImage(systemName: "folder")) { [unowned self] in URLHandler.shared.presentArchiveChooserPanel(insertToRecentItems: true, senderView: view) } openFolderOption.translatesAutoresizingMaskIntoConstraints = false view.addSubview(openFolderOption) NSLayoutConstraint.activate([ openFolderOption.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 40), openFolderOption.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) let diffCatalogsOption = WelcomeScreenOption(primaryText: "Diff Catalogs", secondaryText: "Diff 2 different Asset Catalogs on your Mac", image: NSImage(systemName: "doc.plaintext")) { WindowController(kind: .diffSelection).showWindow(nil) } diffCatalogsOption.translatesAutoresizingMaskIntoConstraints = false view.addSubview(diffCatalogsOption) NSLayoutConstraint.activate([ diffCatalogsOption.topAnchor.constraint(equalTo: openFolderOption.bottomAnchor, constant: 20), diffCatalogsOption.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) let closeWindowButton = NSButton() closeWindowButton.image = NSImage(systemName: "xmark") closeWindowButton.action = #selector(closeWindowButtonClicked) closeWindowButton.target = self closeWindowButton.showsBorderOnlyWhileMouseInside = true closeWindowButton.bezelStyle = .roundRect closeWindowButton.bezelColor = .gray closeWindowButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(closeWindowButton) NSLayoutConstraint.activate([ closeWindowButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 10), closeWindowButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8) ]) let showThisWindowButton = NSButton(title: "Show this window when Samra launches", target: self, action: #selector(showThisWindowOnLaunchButtonClicked(sender:))) showThisWindowButton.setButtonType(.switch) showThisWindowButton.state = Preferences.showWelcomeVCOnLaunch ? .on : .off showThisWindowButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(showThisWindowButton) NSLayoutConstraint.activate([ showThisWindowButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), showThisWindowButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) // Register for when cursor is on Window // if it's not, hide the closeWindowButton and the showThisWindowButton // otherwise show it NSEvent.addLocalMonitorForEvents(matching: [.mouseEntered, .mouseExited]) { event in let newAlphaValue: CGFloat = (event.type == .mouseExited) ? 0 : 1 NSAnimationContext.runAnimationGroup { context in context.duration = 0.2 context.allowsImplicitAnimation = true closeWindowButton.animator().alphaValue = newAlphaValue showThisWindowButton.animator().alphaValue = newAlphaValue } return event } } @objc func closeWindowButtonClicked() { view.window?.close() } @objc func showThisWindowOnLaunchButtonClicked(sender: NSButton) { var newValue = Preferences.showWelcomeVCOnLaunch newValue.toggle() Preferences.showWelcomeVCOnLaunch = newValue } deinit { print("Magna Carta.. Holy Grail.") print("deinit called for WelcomeViewController") } override func viewDidAppear() { super.viewDidAppear() guard let window = view.window else { return } window.backgroundColor = .standardWindowBackgroundColor window.standardWindowButton(.closeButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true window.standardWindowButton(.miniaturizeButton)?.isHidden = true } } ================================================ FILE: Samra/WindowController.swift ================================================ // // WindowController.swift // Samra // // Created by Serena on 18/02/2023. // import Cocoa import AssetCatalogWrapper class WindowController: NSWindowController, NSWindowDelegate { enum Kind { /// The 'Welcome to Samra' screen case welcome /// The 'About Samra' Panel. case aboutPanel /// A View Controller to select 2 AssetCatalogs to diff between them case diffSelection /// A View Controller to show the diff between 2 asset catalogs case diffShow([RenditionDiff], CUICatalog, URL) /// Show a View Controller of a rendition collection case assetCatalog(AssetCatalogInput) } convenience init(kind: Kind) { let viewController: NSViewController switch kind { case .welcome: let splitViewController = CollapseNotifierSplitViewController() let welcomeViewController = WelcomeViewController() let list = PastFilesListViewController() splitViewController.addSplitViewItem(NSSplitViewItem(viewController: welcomeViewController)) splitViewController.addSplitViewItem(NSSplitViewItem(sidebarWithViewController: list)) viewController = splitViewController case .assetCatalog(let input): let splitViewController = CollapseNotifierSplitViewController() let renditionVC = RenditionListViewController(catalog: input.catalog, collection: input.collection, fileURL: input.fileURL) let typesSidebar = TypesListViewController(types: input.collection.map(\.type)) { type in if let index = renditionVC.dataSource.snapshot().indexOfSection(type) { renditionVC.collectionView.scrollToItems(at: [IndexPath(item: 0, section: index)], scrollPosition: .top) } } splitViewController.addSplitViewItem(NSSplitViewItem(sidebarWithViewController: typesSidebar)) splitViewController.addSplitViewItem(NSSplitViewItem(viewController: renditionVC)) viewController = splitViewController case .aboutPanel: viewController = AboutViewController() case .diffSelection: viewController = AssetCatalogDiffSelectionViewController() case .diffShow(let diffs, let catalog, let fileURL): viewController = DiffListViewController(diffs: diffs, catalog: catalog, fileURL: fileURL) } let window = NSWindow(contentViewController: viewController) window.styleMask.insert(.fullSizeContentView) self.init(window: window) switch kind { case .assetCatalog(let input): let toolbar = NSToolbar() toolbar.delegate = self window.toolbar = toolbar toolbar.insertItem(withItemIdentifier: .flexibleSpace, at: 0) toolbar.insertItem(withItemIdentifier: .searchBar, at: 1) toolbar.insertItem(withItemIdentifier: .init("infoButton"), at: 2) window.toolbar?.centeredItemIdentifier = .searchBar window.animationBehavior = .documentWindow window.delegate = self window.title = input.fileURL.lastPathComponent if #available(macOS 11, *) { window.subtitle = input.fileURL.deletingLastPathComponent().lastPathComponent } case .welcome: window.makeTitleBarTransparentAndUnresizable() window.animationBehavior = .utilityWindow window.title = "Samra" case .aboutPanel: window.makeTitleBarTransparentAndUnresizable() window.title = "Samra" case .diffSelection: window.title = "Diff" case .diffShow(_, _, _): let toolbar = NSToolbar() toolbar.delegate = self window.toolbar = toolbar window.title = "Diff" toolbar.insertItem(withItemIdentifier: .flexibleSpace, at: 0) toolbar.insertItem(withItemIdentifier: .searchBar, at: 1) window.animationBehavior = .documentWindow window.delegate = self } } } extension WindowController: NSToolbarDelegate { func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { return [] } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { return [] } func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { switch itemIdentifier { case .searchBar: let rendVC: NSSearchFieldDelegate? if let splitVC = contentViewController as? NSSplitViewController { rendVC = splitVC.splitViewItems[1].viewController as? NSSearchFieldDelegate } else { rendVC = contentViewController as? NSSearchFieldDelegate } /* if #available(macOS 11, *) { let item = NSSearchToolbarItem(itemIdentifier: .searchBar) item.searchField.delegate = rendVC return item } */ let item = NSToolbarItem(itemIdentifier: .searchBar) let searchField = NSSearchField() searchField.delegate = rendVC item.view = searchField return item case .init("infoButton"): let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) let button = NSButton() if #available(macOS 11, *) { button.image = NSImage(systemSymbolName: "info.circle", accessibilityDescription: nil)? .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 18, weight: .regular)) } else { button.title = "Info" } button.action = #selector(RenditionListViewController.infoButtonClicked(sender:)) button.target = (contentViewController as? NSSplitViewController)?.splitViewItems[1].viewController as? RenditionListViewController button.setButtonType(.momentaryPushIn) button.bezelStyle = .texturedRounded toolbarItem.view = button // toolbarItem.action = #selector(RenditionListViewController.infoPopoverItemClicked(sender:)) // toolbarItem.target = (contentViewController as? NSSplitViewController)?.splitViewItems[1].viewController as? RenditionListViewController // toolbarItem.image = NSImage(systemSymbolName: "info.circle", accessibilityDescription: nil) // toolbarItem.isEnabled = true return toolbarItem case .init("flexSpace"): #warning("Fix this (want flexible space between search bar and sidebar)") let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: "flexSpace")) return toolbarItem default: return NSToolbarItem(itemIdentifier: itemIdentifier) } } func toolbar(_ toolbar: NSToolbar, itemIdentifier: NSToolbarItem.Identifier, canBeInsertedAt index: Int) -> Bool { return true } } ================================================ FILE: Samra.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 56; objects = { /* Begin PBXBuildFile section */ CE1126A929A556C0000AC770 /* RenditionInformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1126A829A556C0000AC770 /* RenditionInformationView.swift */; }; CE15D4BF29A3E5D5001D66E6 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE15D4BE29A3E5D5001D66E6 /* URLHandler.swift */; }; CE1673DB29ACDF8100F94683 /* AssetCatalogDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1673DA29ACDF8100F94683 /* AssetCatalogDetailsView.swift */; }; CE1F1D3429B0A4C1000B288C /* MenuableCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F1D3329B0A4C1000B288C /* MenuableCollectionView.swift */; }; CE1F1D3629B0ADCE000B288C /* ClosureMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F1D3529B0ADCE000B288C /* ClosureMenuItem.swift */; }; CE375E6429B65D3900CAC2F0 /* AssetCatalogDiffSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE375E6329B65D3900CAC2F0 /* AssetCatalogDiffSelectionViewController.swift */; }; CE375E6629B6675900CAC2F0 /* AssetCatalogInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE375E6529B6675900CAC2F0 /* AssetCatalogInput.swift */; }; CE3BC09829A0C626009823CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BC09729A0C626009823CF /* AppDelegate.swift */; }; CE3BC09A29A0C626009823CF /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BC09929A0C626009823CF /* WindowController.swift */; }; CE3BC09C29A0C626009823CF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE3BC09B29A0C626009823CF /* Assets.xcassets */; }; CE3BC0A729A0CC27009823CF /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BC0A629A0CC27009823CF /* WelcomeViewController.swift */; }; CE3BC0A929A0D713009823CF /* PastFilesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BC0A829A0D713009823CF /* PastFilesListViewController.swift */; }; CE3BC0AF29A0E345009823CF /* BasicLayoutAnchorsHolding.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BC0AE29A0E345009823CF /* BasicLayoutAnchorsHolding.swift */; }; CE3BC0B129A0E990009823CF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BC0B029A0E990009823CF /* Preferences.swift */; }; CE3F2D052A02DABC0026A9F9 /* DiffFilePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3F2D042A02DABC0026A9F9 /* DiffFilePreviewView.swift */; }; CE3F9C6E2CCA22C400662232 /* AssetCatalogWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = CE3F9C6D2CCA22C400662232 /* AssetCatalogWrapper */; }; CE45E09D29C24E1F00817359 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE45E09C29C24E1F00817359 /* main.swift */; }; CE5AF1A829A2516500C675D8 /* RenditionTypeHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5AF1A729A2516500C675D8 /* RenditionTypeHeaderView.swift */; }; CE7D54A729A1238F00862873 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D54A629A1238F00862873 /* Extensions.swift */; }; CE7D54AD29A1313000862873 /* TypesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D54AC29A1313000862873 /* TypesListViewController.swift */; }; CE7D54AF29A1370D00862873 /* RenditionListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D54AE29A1370D00862873 /* RenditionListViewController.swift */; }; CE7D54B129A14F2200862873 /* RenditionCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D54B029A14F2200862873 /* RenditionCollectionViewItem.swift */; }; CE7E5D9229B1D3AC0064B91B /* QuickLooKPreviewSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7E5D9129B1D3AC0064B91B /* QuickLooKPreviewSource.swift */; }; CE7E5D9829B1D5D30064B91B /* QuickLookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE7E5D9729B1D5D30064B91B /* QuickLookUI.framework */; }; CEA6629029A4E5FF00215B08 /* WelcomeScreenOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA6628F29A4E5FF00215B08 /* WelcomeScreenOption.swift */; }; CEA71A5E29AE760900BEBE93 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA71A5D29AE760900BEBE93 /* AboutViewController.swift */; }; CEC3B27C29B14551007E853E /* AssetCatalogDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3B27B29B14551007E853E /* AssetCatalogDocument.swift */; }; CEC5EBC729B7CCD6009BA873 /* DiffListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC5EBC629B7CCD6009BA873 /* DiffListViewController.swift */; }; CED4DE3129A626D7008B2B8A /* CollapseNotifierSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED4DE3029A626D7008B2B8A /* CollapseNotifierSplitViewController.swift */; }; CEE9FA3E2CCA245C00F3F356 /* AssetCatalogWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = CEE9FA3D2CCA245C00F3F356 /* AssetCatalogWrapper */; }; CEEA6AB429A515EA00B3CEA9 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA6AB329A515EA00B3CEA9 /* DetailItem.swift */; }; CEEE131029B73B99009C1ACD /* RenditionDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEE130F29B73B99009C1ACD /* RenditionDiff.swift */; }; CEF987032B974C53002177A2 /* ClosureBasedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF987022B974C53002177A2 /* ClosureBasedButton.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ CE45E09829C24E1F00817359 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ CE1126A829A556C0000AC770 /* RenditionInformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenditionInformationView.swift; sourceTree = ""; }; CE15D4BE29A3E5D5001D66E6 /* URLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHandler.swift; sourceTree = ""; }; CE1673DA29ACDF8100F94683 /* AssetCatalogDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogDetailsView.swift; sourceTree = ""; }; CE1F1D3329B0A4C1000B288C /* MenuableCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuableCollectionView.swift; sourceTree = ""; }; CE1F1D3529B0ADCE000B288C /* ClosureMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureMenuItem.swift; sourceTree = ""; }; CE375E6329B65D3900CAC2F0 /* AssetCatalogDiffSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogDiffSelectionViewController.swift; sourceTree = ""; }; CE375E6529B6675900CAC2F0 /* AssetCatalogInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogInput.swift; sourceTree = ""; }; CE3BC09429A0C626009823CF /* Samra.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Samra.app; sourceTree = BUILT_PRODUCTS_DIR; }; CE3BC09729A0C626009823CF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CE3BC09929A0C626009823CF /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; CE3BC09B29A0C626009823CF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CE3BC0A029A0C626009823CF /* Samra.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Samra.entitlements; sourceTree = ""; }; CE3BC0A629A0CC27009823CF /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; CE3BC0A829A0D713009823CF /* PastFilesListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastFilesListViewController.swift; sourceTree = ""; }; CE3BC0AE29A0E345009823CF /* BasicLayoutAnchorsHolding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicLayoutAnchorsHolding.swift; sourceTree = ""; }; CE3BC0B029A0E990009823CF /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; CE3F2D042A02DABC0026A9F9 /* DiffFilePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffFilePreviewView.swift; sourceTree = ""; }; CE45E09A29C24E1F00817359 /* extractutil */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = extractutil; sourceTree = BUILT_PRODUCTS_DIR; }; CE45E09C29C24E1F00817359 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; CE5AF1A729A2516500C675D8 /* RenditionTypeHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenditionTypeHeaderView.swift; sourceTree = ""; }; CE7D54A629A1238F00862873 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; CE7D54A829A1243C00862873 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; CE7D54AC29A1313000862873 /* TypesListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypesListViewController.swift; sourceTree = ""; }; CE7D54AE29A1370D00862873 /* RenditionListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenditionListViewController.swift; sourceTree = ""; }; CE7D54B029A14F2200862873 /* RenditionCollectionViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenditionCollectionViewItem.swift; sourceTree = ""; }; CE7E5D9129B1D3AC0064B91B /* QuickLooKPreviewSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLooKPreviewSource.swift; sourceTree = ""; }; CE7E5D9529B1D5CC0064B91B /* QuickLookThumbnailing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookThumbnailing.framework; path = System/Library/PrivateFrameworks/QuickLookThumbnailing.framework; sourceTree = SDKROOT; }; CE7E5D9729B1D5D30064B91B /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; }; CEA6628F29A4E5FF00215B08 /* WelcomeScreenOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenOption.swift; sourceTree = ""; }; CEA71A5D29AE760900BEBE93 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; CEC3B27B29B14551007E853E /* AssetCatalogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogDocument.swift; sourceTree = ""; }; CEC5EBC629B7CCD6009BA873 /* DiffListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffListViewController.swift; sourceTree = ""; }; CED4DE2E29A62566008B2B8A /* AppKitPrivates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppKitPrivates.h; sourceTree = ""; }; CED4DE2F29A62586008B2B8A /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; CED4DE3029A626D7008B2B8A /* CollapseNotifierSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapseNotifierSplitViewController.swift; sourceTree = ""; }; CEEA6AB329A515EA00B3CEA9 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = ""; }; CEEE130F29B73B99009C1ACD /* RenditionDiff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenditionDiff.swift; sourceTree = ""; }; CEF987022B974C53002177A2 /* ClosureBasedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureBasedButton.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ CE3BC09129A0C625009823CF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CE7E5D9829B1D5D30064B91B /* QuickLookUI.framework in Frameworks */, CE3F9C6E2CCA22C400662232 /* AssetCatalogWrapper in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; CE45E09729C24E1F00817359 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CEE9FA3E2CCA245C00F3F356 /* AssetCatalogWrapper in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ CE3BC08B29A0C625009823CF = { isa = PBXGroup; children = ( CE3BC09629A0C626009823CF /* Samra */, CE45E09B29C24E1F00817359 /* extractutil */, CE3BC09529A0C626009823CF /* Products */, CE7E5D9429B1D5CC0064B91B /* Frameworks */, ); sourceTree = ""; }; CE3BC09529A0C626009823CF /* Products */ = { isa = PBXGroup; children = ( CE3BC09429A0C626009823CF /* Samra.app */, CE45E09A29C24E1F00817359 /* extractutil */, ); name = Products; sourceTree = ""; }; CE3BC09629A0C626009823CF /* Samra */ = { isa = PBXGroup; children = ( CE7D54A829A1243C00862873 /* Info.plist */, CE3BC09729A0C626009823CF /* AppDelegate.swift */, CE3BC09929A0C626009823CF /* WindowController.swift */, CE3BC0AA29A0E2E7009823CF /* UI */, CE3BC0AB29A0E2F6009823CF /* Backend */, CE3BC09B29A0C626009823CF /* Assets.xcassets */, CE3BC0A029A0C626009823CF /* Samra.entitlements */, ); path = Samra; sourceTree = ""; }; CE3BC0AA29A0E2E7009823CF /* UI */ = { isa = PBXGroup; children = ( CE3BC0A629A0CC27009823CF /* WelcomeViewController.swift */, CEA6628F29A4E5FF00215B08 /* WelcomeScreenOption.swift */, CE3BC0A829A0D713009823CF /* PastFilesListViewController.swift */, CED4DE3029A626D7008B2B8A /* CollapseNotifierSplitViewController.swift */, CEA71A5D29AE760900BEBE93 /* AboutViewController.swift */, CE1F1D3329B0A4C1000B288C /* MenuableCollectionView.swift */, CEF987022B974C53002177A2 /* ClosureBasedButton.swift */, CEC5EBC529B7CCCA009BA873 /* Diff */, CECA445029A23D80003222D0 /* Rendition */, ); path = UI; sourceTree = ""; }; CE3BC0AB29A0E2F6009823CF /* Backend */ = { isa = PBXGroup; children = ( CE7E5D9329B1D45B0064B91B /* UI Support */, CED4DE2D29A62558008B2B8A /* AppKitPrivates */, CEEA6AB329A515EA00B3CEA9 /* DetailItem.swift */, CEEE130F29B73B99009C1ACD /* RenditionDiff.swift */, CE3BC0B029A0E990009823CF /* Preferences.swift */, CE7D54A629A1238F00862873 /* Extensions.swift */, CE375E6529B6675900CAC2F0 /* AssetCatalogInput.swift */, CE1F1D3529B0ADCE000B288C /* ClosureMenuItem.swift */, ); path = Backend; sourceTree = ""; }; CE45E09B29C24E1F00817359 /* extractutil */ = { isa = PBXGroup; children = ( CE45E09C29C24E1F00817359 /* main.swift */, ); path = extractutil; sourceTree = ""; }; CE7E5D9329B1D45B0064B91B /* UI Support */ = { isa = PBXGroup; children = ( CE3BC0AE29A0E345009823CF /* BasicLayoutAnchorsHolding.swift */, CE15D4BE29A3E5D5001D66E6 /* URLHandler.swift */, CEC3B27B29B14551007E853E /* AssetCatalogDocument.swift */, CE7E5D9129B1D3AC0064B91B /* QuickLooKPreviewSource.swift */, ); path = "UI Support"; sourceTree = ""; }; CE7E5D9429B1D5CC0064B91B /* Frameworks */ = { isa = PBXGroup; children = ( CE7E5D9729B1D5D30064B91B /* QuickLookUI.framework */, CE7E5D9529B1D5CC0064B91B /* QuickLookThumbnailing.framework */, ); name = Frameworks; sourceTree = ""; }; CEC5EBC529B7CCCA009BA873 /* Diff */ = { isa = PBXGroup; children = ( CE375E6329B65D3900CAC2F0 /* AssetCatalogDiffSelectionViewController.swift */, CE3F2D042A02DABC0026A9F9 /* DiffFilePreviewView.swift */, CEC5EBC629B7CCD6009BA873 /* DiffListViewController.swift */, ); path = Diff; sourceTree = ""; }; CECA445029A23D80003222D0 /* Rendition */ = { isa = PBXGroup; children = ( CE7D54AC29A1313000862873 /* TypesListViewController.swift */, CE5AF1A729A2516500C675D8 /* RenditionTypeHeaderView.swift */, CE7D54AE29A1370D00862873 /* RenditionListViewController.swift */, CE7D54B029A14F2200862873 /* RenditionCollectionViewItem.swift */, CE1126A829A556C0000AC770 /* RenditionInformationView.swift */, CE1673DA29ACDF8100F94683 /* AssetCatalogDetailsView.swift */, ); path = Rendition; sourceTree = ""; }; CED4DE2D29A62558008B2B8A /* AppKitPrivates */ = { isa = PBXGroup; children = ( CED4DE2E29A62566008B2B8A /* AppKitPrivates.h */, CED4DE2F29A62586008B2B8A /* module.modulemap */, ); path = AppKitPrivates; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ CE3BC09329A0C625009823CF /* Samra */ = { isa = PBXNativeTarget; buildConfigurationList = CE3BC0A329A0C626009823CF /* Build configuration list for PBXNativeTarget "Samra" */; buildPhases = ( CE3BC09029A0C625009823CF /* Sources */, CE3BC09129A0C625009823CF /* Frameworks */, CE3BC09229A0C625009823CF /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Samra; packageProductDependencies = ( CE3F9C6D2CCA22C400662232 /* AssetCatalogWrapper */, ); productName = Samra; productReference = CE3BC09429A0C626009823CF /* Samra.app */; productType = "com.apple.product-type.application"; }; CE45E09929C24E1F00817359 /* extractutil */ = { isa = PBXNativeTarget; buildConfigurationList = CE45E0A029C24E1F00817359 /* Build configuration list for PBXNativeTarget "extractutil" */; buildPhases = ( CE45E09629C24E1F00817359 /* Sources */, CE45E09729C24E1F00817359 /* Frameworks */, CE45E09829C24E1F00817359 /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); name = extractutil; packageProductDependencies = ( CEE9FA3D2CCA245C00F3F356 /* AssetCatalogWrapper */, ); productName = extractutil; productReference = CE45E09A29C24E1F00817359 /* extractutil */; productType = "com.apple.product-type.tool"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ CE3BC08C29A0C625009823CF /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; LastUpgradeCheck = 1420; TargetAttributes = { CE3BC09329A0C625009823CF = { CreatedOnToolsVersion = 14.2; }; CE45E09929C24E1F00817359 = { CreatedOnToolsVersion = 14.2; }; }; }; buildConfigurationList = CE3BC08F29A0C625009823CF /* Build configuration list for PBXProject "Samra" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = CE3BC08B29A0C625009823CF; packageReferences = ( CE3F9C6C2CCA22C400662232 /* XCRemoteSwiftPackageReference "PrivateKits" */, ); productRefGroup = CE3BC09529A0C626009823CF /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( CE3BC09329A0C625009823CF /* Samra */, CE45E09929C24E1F00817359 /* extractutil */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ CE3BC09229A0C625009823CF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( CE3BC09C29A0C626009823CF /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ CE3BC09029A0C625009823CF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CE3BC0A729A0CC27009823CF /* WelcomeViewController.swift in Sources */, CE3BC09A29A0C626009823CF /* WindowController.swift in Sources */, CE3BC0B129A0E990009823CF /* Preferences.swift in Sources */, CE3BC0A929A0D713009823CF /* PastFilesListViewController.swift in Sources */, CE3BC0AF29A0E345009823CF /* BasicLayoutAnchorsHolding.swift in Sources */, CE1126A929A556C0000AC770 /* RenditionInformationView.swift in Sources */, CE15D4BF29A3E5D5001D66E6 /* URLHandler.swift in Sources */, CE1673DB29ACDF8100F94683 /* AssetCatalogDetailsView.swift in Sources */, CE7D54AD29A1313000862873 /* TypesListViewController.swift in Sources */, CEF987032B974C53002177A2 /* ClosureBasedButton.swift in Sources */, CE1F1D3429B0A4C1000B288C /* MenuableCollectionView.swift in Sources */, CE375E6429B65D3900CAC2F0 /* AssetCatalogDiffSelectionViewController.swift in Sources */, CE7D54B129A14F2200862873 /* RenditionCollectionViewItem.swift in Sources */, CE3BC09829A0C626009823CF /* AppDelegate.swift in Sources */, CEC5EBC729B7CCD6009BA873 /* DiffListViewController.swift in Sources */, CE5AF1A829A2516500C675D8 /* RenditionTypeHeaderView.swift in Sources */, CE1F1D3629B0ADCE000B288C /* ClosureMenuItem.swift in Sources */, CEA71A5E29AE760900BEBE93 /* AboutViewController.swift in Sources */, CE7D54A729A1238F00862873 /* Extensions.swift in Sources */, CE3F2D052A02DABC0026A9F9 /* DiffFilePreviewView.swift in Sources */, CED4DE3129A626D7008B2B8A /* CollapseNotifierSplitViewController.swift in Sources */, CE375E6629B6675900CAC2F0 /* AssetCatalogInput.swift in Sources */, CEEA6AB429A515EA00B3CEA9 /* DetailItem.swift in Sources */, CE7D54AF29A1370D00862873 /* RenditionListViewController.swift in Sources */, CEA6629029A4E5FF00215B08 /* WelcomeScreenOption.swift in Sources */, CE7E5D9229B1D3AC0064B91B /* QuickLooKPreviewSource.swift in Sources */, CEEE131029B73B99009C1ACD /* RenditionDiff.swift in Sources */, CEC3B27C29B14551007E853E /* AssetCatalogDocument.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CE45E09629C24E1F00817359 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CE45E09D29C24E1F00817359 /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ CE3BC0A129A0C626009823CF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; CE3BC0A229A0C626009823CF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; CE3BC0A429A0C626009823CF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Samra/Samra.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1.4; DEVELOPMENT_TEAM = L9735M962H; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Samra/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15.1; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.serena.Samra; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Samra/Backend/AppKitPrivates"; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); }; name = Debug; }; CE3BC0A529A0C626009823CF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Samra/Samra.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1.4; DEVELOPMENT_TEAM = L9735M962H; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Samra/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSPrincipalClass = NSApplication; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15.1; MARKETING_VERSION = 1; PRODUCT_BUNDLE_IDENTIFIER = com.serena.Samra; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Samra/Backend/AppKitPrivates"; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); }; name = Release; }; CE45E09E29C24E1F00817359 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = L9735M962H; ENABLE_HARDENED_RUNTIME = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Debug; }; CE45E09F29C24E1F00817359 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = L9735M962H; ENABLE_HARDENED_RUNTIME = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ CE3BC08F29A0C625009823CF /* Build configuration list for PBXProject "Samra" */ = { isa = XCConfigurationList; buildConfigurations = ( CE3BC0A129A0C626009823CF /* Debug */, CE3BC0A229A0C626009823CF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CE3BC0A329A0C626009823CF /* Build configuration list for PBXNativeTarget "Samra" */ = { isa = XCConfigurationList; buildConfigurations = ( CE3BC0A429A0C626009823CF /* Debug */, CE3BC0A529A0C626009823CF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CE45E0A029C24E1F00817359 /* Build configuration list for PBXNativeTarget "extractutil" */ = { isa = XCConfigurationList; buildConfigurations = ( CE45E09E29C24E1F00817359 /* Debug */, CE45E09F29C24E1F00817359 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ CE3F9C6C2CCA22C400662232 /* XCRemoteSwiftPackageReference "PrivateKits" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SerenaKit/PrivateKits"; requirement = { branch = main; kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ CE3F9C6D2CCA22C400662232 /* AssetCatalogWrapper */ = { isa = XCSwiftPackageProductDependency; package = CE3F9C6C2CCA22C400662232 /* XCRemoteSwiftPackageReference "PrivateKits" */; productName = AssetCatalogWrapper; }; CEE9FA3D2CCA245C00F3F356 /* AssetCatalogWrapper */ = { isa = XCSwiftPackageProductDependency; package = CE3F9C6C2CCA22C400662232 /* XCRemoteSwiftPackageReference "PrivateKits" */; productName = AssetCatalogWrapper; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = CE3BC08C29A0C625009823CF /* Project object */; } ================================================ FILE: Samra.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Samra.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Samra.xcodeproj/xcshareddata/xcschemes/Samra.xcscheme ================================================ ================================================ FILE: Samra.xcodeproj/xcshareddata/xcschemes/extractutil.xcscheme ================================================ ================================================ FILE: extractutil/main.swift ================================================ // // main.swift // extractutil // // Created by Serena on 15/03/2023. // // smol CommandLine tool to just extract an asset catalog :3 import Foundation import AssetCatalogWrapper guard CommandLine.arguments.count >= 3 else { fatalError("usage: \(CommandLine.arguments[0]) ") } let catalogURL = URL(fileURLWithPath: CommandLine.arguments[1]) let destinationURL = URL(fileURLWithPath: CommandLine.arguments[2]) let rends: RenditionCollection do { rends = try AssetCatalogWrapper.shared.renditions(forCarArchive: catalogURL).1 } catch { fatalError("Failed to fetch Catalog from URL \(catalogURL.path), error: \(error.localizedDescription)") } // try create the destination URL if it doesn't exist if !FileManager.default.fileExists(atPath: destinationURL.path) { do { print("destination URL \(destinationURL.path) doesn't exist, will try to create") try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true) } catch { fatalError("Failed to create \(destinationURL.path), error: \(error.localizedDescription)") } } do { try AssetCatalogWrapper.shared.extract(collection: rends, to: destinationURL) print("Extracted catalog to \(destinationURL.path)") } catch { fatalError("Failed to extract (some) items, error: \(error.localizedDescription)") }