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.
## 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
================================================
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)")
}