its whats on the tin; culls raw photos
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add sidecar support

+244 -99
+13 -2
cull/CullApp.swift
··· 5 5 @State private var session = CullSession() 6 6 @State private var thumbnailCache = ThumbnailCache() 7 7 @AppStorage("recentFolders") private var recentFoldersData: Data = Data() 8 + @AppStorage("importRecursive") private var importRecursive: Bool = true 8 9 9 10 private var recentFolders: [URL] { 10 11 (try? JSONDecoder().decode([String].self, from: recentFoldersData))?.compactMap { URL(fileURLWithPath: $0) } ?? [] ··· 28 29 addRecentFolder(url) 29 30 } 30 31 } 32 + .onAppear { 33 + session.importRecursive = importRecursive 34 + } 31 35 } 32 36 .windowStyle(.automatic) 33 37 .commands { ··· 39 43 .keyboardShortcut("o") 40 44 41 45 Toggle("Include Subfolders", isOn: Binding( 42 - get: { session.importRecursive }, 43 - set: { session.importRecursive = $0 } 46 + get: { importRecursive }, 47 + set: { newValue in 48 + importRecursive = newValue 49 + session.importRecursive = newValue 50 + } 44 51 )) 45 52 46 53 Menu("Open Recent") { ··· 160 167 .disabled(session.groups.isEmpty) 161 168 162 169 } 170 + } 171 + 172 + Settings { 173 + SettingsView() 163 174 } 164 175 } 165 176
+8
cull/Models/CullSession.swift
··· 3 3 4 4 @Observable 5 5 final class CullSession { 6 + /// Whether XMP sidecars are auto-written on rating/flag changes (mirrors @AppStorage) 7 + var autoWriteXMP: Bool { 8 + get { UserDefaults.standard.object(forKey: "autoWriteXMP") as? Bool ?? true } 9 + set { UserDefaults.standard.set(newValue, forKey: "autoWriteXMP") } 10 + } 6 11 var sourceFolder: URL? 7 12 var groups: [PhotoGroup] = [] 8 13 var selectedGroupIndex: Int = 0 ··· 267 272 } 268 273 undoManager?.setActionName(actionName) 269 274 scheduleSave() 275 + if autoWriteXMP { 276 + XMPSidecar.write(photo) 277 + } 270 278 } 271 279 272 280 func setRating(_ rating: Int) {
+19 -2
cull/Services/PhotoExporter.swift
··· 37 37 destination: URL, 38 38 fileType: ExportFileType, 39 39 mode: ExportMode, 40 - folderStructure: ExportFolderStructure = .flat 40 + folderStructure: ExportFolderStructure = .flat, 41 + includeXMP: Bool = true 41 42 ) async -> ExportResult { 42 43 let fm = FileManager.default 43 44 ··· 90 91 errors.append("\(sourceURL.lastPathComponent): \(error.localizedDescription)") 91 92 } 92 93 } 93 - if photoExported { exported += 1 } 94 + if photoExported { 95 + exported += 1 96 + if includeXMP { 97 + let primaryURL = urls.first! 98 + let subfolder = subfolder(for: primaryURL, photo: photo, structure: folderStructure) 99 + let destDir = subfolder.isEmpty ? destination : destination.appendingPathComponent(subfolder) 100 + let xmpName = primaryURL.deletingPathExtension().lastPathComponent + ".xmp" 101 + let xmpDest = destDir.appendingPathComponent(xmpName) 102 + let xmpRating = photo.flag == .reject ? -1 : photo.rating 103 + let xmpContent = XMPSidecar.freshXMP(rating: xmpRating) 104 + do { 105 + try xmpContent.data(using: .utf8)?.write(to: xmpDest) 106 + } catch { 107 + errors.append("\(xmpName): XMP write failed — \(error.localizedDescription)") 108 + } 109 + } 110 + } 94 111 } 95 112 96 113 return ExportResult(exported: exported, skipped: skipped, errors: errors)
+7
cull/Services/PhotoImporter.swift
··· 112 112 113 113 photos.sort { ($0.captureDate ?? .distantPast) < ($1.captureDate ?? .distantPast) } 114 114 115 + // Read existing XMP sidecars 116 + for photo in photos { 117 + if let meta = XMPSidecar.read(for: photo.url) { 118 + XMPSidecar.apply(meta, to: photo) 119 + } 120 + } 121 + 115 122 return ImportResult(photos: photos, paired: pairedCount) 116 123 } 117 124
+136
cull/Services/XMPSidecar.swift
··· 1 + import Foundation 2 + 3 + /// Reads and writes XMP sidecar files for photo metadata interoperability. 4 + /// Uses `<basename>.xmp` naming (Lightroom/Bridge compatible). 5 + struct XMPSidecar { 6 + 7 + /// Metadata stored in an XMP sidecar 8 + struct Metadata { 9 + var rating: Int = 0 // 0 = unrated, 1-5 = stars, -1 = rejected 10 + var label: String? // color label: "Red", "Yellow", "Green", "Blue", "Purple" 11 + } 12 + 13 + /// Returns the XMP sidecar URL for a given photo URL 14 + static func sidecarURL(for photoURL: URL) -> URL { 15 + photoURL.deletingPathExtension().appendingPathExtension("xmp") 16 + } 17 + 18 + // MARK: - Read 19 + 20 + /// Read metadata from an existing XMP sidecar, if present 21 + static func read(for photoURL: URL) -> Metadata? { 22 + let url = sidecarURL(for: photoURL) 23 + guard FileManager.default.fileExists(atPath: url.path) else { return nil } 24 + guard let doc = try? XMLDocument(contentsOf: url, options: []) else { return nil } 25 + 26 + var meta = Metadata() 27 + 28 + // Try attribute form: xmp:Rating="3" on rdf:Description 29 + if let ratingStr = try? doc.nodes(forXPath: "//@xmp:Rating").first?.stringValue, 30 + let rating = Int(ratingStr) { 31 + meta.rating = rating 32 + } 33 + // Try element form: <xmp:Rating>3</xmp:Rating> 34 + else if let ratingStr = try? doc.nodes(forXPath: "//xmp:Rating").first?.stringValue, 35 + let rating = Int(ratingStr) { 36 + meta.rating = rating 37 + } 38 + 39 + if let label = try? doc.nodes(forXPath: "//@xmp:Label").first?.stringValue { 40 + meta.label = label 41 + } else if let label = try? doc.nodes(forXPath: "//xmp:Label").first?.stringValue { 42 + meta.label = label 43 + } 44 + 45 + return meta 46 + } 47 + 48 + // MARK: - Write 49 + 50 + /// Write metadata to an XMP sidecar, merging with existing content if present 51 + static func write(_ photo: Photo) { 52 + let url = sidecarURL(for: photo.url) 53 + let xmpRating = xmpRating(for: photo) 54 + 55 + // If an existing XMP exists, try to merge 56 + if FileManager.default.fileExists(atPath: url.path), 57 + let doc = try? XMLDocument(contentsOf: url, options: [.nodePreserveWhitespace]) { 58 + if mergeInto(doc, rating: xmpRating) { 59 + try? doc.xmlData(options: [.nodePrettyPrint]).write(to: url) 60 + return 61 + } 62 + } 63 + 64 + // Write fresh XMP 65 + let xml = freshXMP(rating: xmpRating) 66 + try? xml.data(using: .utf8)?.write(to: url) 67 + } 68 + 69 + // MARK: - Mapping 70 + 71 + /// Map Cull's rating/flag to XMP rating value 72 + private static func xmpRating(for photo: Photo) -> Int { 73 + if photo.flag == .reject { return -1 } 74 + return photo.rating // 0 = unrated, 1-5 = stars 75 + } 76 + 77 + /// Map XMP metadata back to Cull's rating/flag 78 + static func apply(_ meta: Metadata, to photo: Photo) { 79 + if meta.rating == -1 { 80 + photo.flag = .reject 81 + } else if meta.rating >= 1 && meta.rating <= 5 { 82 + photo.rating = meta.rating 83 + } 84 + // Don't overwrite existing state with "unrated" 85 + } 86 + 87 + // MARK: - XMP Generation 88 + 89 + static func freshXMP(rating: Int) -> String { 90 + """ 91 + <?xpacket begin="\u{FEFF}" id="W5M0MpCehiHzreSzNTczkc9d"?> 92 + <x:xmpmeta xmlns:x="adobe:ns:meta/"> 93 + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> 94 + <rdf:Description rdf:about="" 95 + xmlns:xmp="http://ns.adobe.com/xap/1.0/" 96 + xmp:Rating="\(rating)" 97 + xmp:CreatorTool="Cull"> 98 + </rdf:Description> 99 + </rdf:RDF> 100 + </x:xmpmeta> 101 + <?xpacket end="w"?> 102 + """ 103 + } 104 + 105 + /// Merge rating into an existing XMLDocument's rdf:Description 106 + private static func mergeInto(_ doc: XMLDocument, rating: Int) -> Bool { 107 + // Find rdf:Description element 108 + guard let descriptions = try? doc.nodes(forXPath: "//rdf:Description"), 109 + let desc = descriptions.first as? XMLElement else { return false } 110 + 111 + // Update or add xmp:Rating attribute 112 + let ns = "http://ns.adobe.com/xap/1.0/" 113 + if let existing = desc.attribute(forLocalName: "Rating", uri: ns) { 114 + existing.stringValue = "\(rating)" 115 + } else { 116 + let attr = XMLNode.attribute( 117 + withName: "xmp:Rating", 118 + uri: ns, 119 + stringValue: "\(rating)" 120 + ) as! XMLNode 121 + desc.addAttribute(attr) 122 + } 123 + 124 + // Ensure CreatorTool mentions Cull 125 + if desc.attribute(forLocalName: "CreatorTool", uri: ns) == nil { 126 + let attr = XMLNode.attribute( 127 + withName: "xmp:CreatorTool", 128 + uri: ns, 129 + stringValue: "Cull" 130 + ) as! XMLNode 131 + desc.addAttribute(attr) 132 + } 133 + 134 + return true 135 + } 136 + }
+4 -1
cull/Views/ExportSheet.swift
··· 11 11 @State private var isExporting: Bool = false 12 12 @State private var result: ExportResult? 13 13 @State private var useCurrentFilters: Bool = true 14 + @State private var includeXMP: Bool = true 14 15 15 16 private var eligiblePhotos: [Photo] { 16 17 if useCurrentFilters { ··· 26 27 27 28 Form { 28 29 Toggle("Export only visible photos", isOn: $useCurrentFilters) 30 + Toggle("Include XMP sidecars", isOn: $includeXMP) 29 31 30 32 Picker("File Type", selection: $fileType) { 31 33 ForEach(ExportFileType.allCases) { type in ··· 132 134 destination: destination, 133 135 fileType: fileType, 134 136 mode: exportMode, 135 - folderStructure: folderStructure 137 + folderStructure: folderStructure, 138 + includeXMP: includeXMP 136 139 ) 137 140 await MainActor.run { 138 141 result = exportResult
+24
cull/Views/SettingsView.swift
··· 1 + import SwiftUI 2 + 3 + struct SettingsView: View { 4 + @AppStorage("autoWriteXMP") private var autoWriteXMP: Bool = true 5 + @AppStorage("importRecursive") private var importRecursive: Bool = true 6 + 7 + var body: some View { 8 + Form { 9 + Section("Sidecars") { 10 + Toggle("Automatically write XMP sidecars", isOn: $autoWriteXMP) 11 + Text("Writes rating and flag changes to .xmp files alongside your photos for Lightroom/Bridge/darktable compatibility.") 12 + .font(.caption) 13 + .foregroundStyle(.secondary) 14 + } 15 + 16 + Section("Import") { 17 + Toggle("Include subfolders by default", isOn: $importRecursive) 18 + } 19 + } 20 + .formStyle(.grouped) 21 + .frame(width: 450) 22 + .fixedSize() 23 + } 24 + }
+12
cull/cull.xcodeproj/project.pbxproj
··· 25 25 0B0EC2982F722491004523FA /* ThumbnailCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2812F722491004523FA /* ThumbnailCache.swift */; }; 26 26 0B0EC2A12F72570F004523FA /* icon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A02F72570F004523FA /* icon.icon */; }; 27 27 0B0EC2A52F727789004523FA /* WorkspaceDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A42F727789004523FA /* WorkspaceDB.swift */; }; 28 + 0B0EC2A72F7314F5004523FA /* XMPSidecar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A62F7314F5004523FA /* XMPSidecar.swift */; }; 29 + 0B0EC2A92F73150B004523FA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0EC2A82F73150B004523FA /* SettingsView.swift */; }; 28 30 /* End PBXBuildFile section */ 29 31 30 32 /* Begin PBXFileReference section */ ··· 47 49 0B0EC2992F724FE5004523FA /* cull.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cull.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 50 0B0EC2A02F72570F004523FA /* icon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = icon.icon; sourceTree = "<group>"; }; 49 51 0B0EC2A42F727789004523FA /* WorkspaceDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceDB.swift; sourceTree = "<group>"; }; 52 + 0B0EC2A62F7314F5004523FA /* XMPSidecar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMPSidecar.swift; sourceTree = "<group>"; }; 53 + 0B0EC2A82F73150B004523FA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 50 54 /* End PBXFileReference section */ 51 55 52 56 /* Begin PBXFrameworksBuildPhase section */ ··· 86 90 0B0EC2822F722491004523FA /* Services */ = { 87 91 isa = PBXGroup; 88 92 children = ( 93 + 0B0EC2A62F7314F5004523FA /* XMPSidecar.swift */, 89 94 0B0EC2A42F727789004523FA /* WorkspaceDB.swift */, 90 95 0B0EC27D2F722491004523FA /* PhotoExporter.swift */, 91 96 0B0EC27E2F722491004523FA /* PhotoImporter.swift */, ··· 99 104 0B0EC2892F722491004523FA /* Views */ = { 100 105 isa = PBXGroup; 101 106 children = ( 107 + 0B0EC2A82F73150B004523FA /* SettingsView.swift */, 102 108 0B0EC2832F722491004523FA /* ContentView.swift */, 103 109 0B0EC2842F722491004523FA /* ExportSheet.swift */, 104 110 0B0EC2852F722491004523FA /* GroupDetailView.swift */, ··· 185 191 0B0EC28A2F722491004523FA /* ImportView.swift in Sources */, 186 192 0B0EC2A52F727789004523FA /* WorkspaceDB.swift in Sources */, 187 193 0B0EC28B2F722491004523FA /* ExportSheet.swift in Sources */, 194 + 0B0EC2A92F73150B004523FA /* SettingsView.swift in Sources */, 195 + 0B0EC2A72F7314F5004523FA /* XMPSidecar.swift in Sources */, 188 196 0B0EC28C2F722491004523FA /* ShotGrouper.swift in Sources */, 189 197 0B0EC28D2F722491004523FA /* Photo.swift in Sources */, 190 198 0B0EC28E2F722491004523FA /* GroupListView.swift in Sources */, ··· 348 356 ENABLE_RESOURCE_ACCESS_USB = NO; 349 357 ENABLE_USER_SELECTED_FILES = readwrite; 350 358 GENERATE_INFOPLIST_FILE = YES; 359 + INFOPLIST_KEY_CFBundleDisplayName = Cull; 360 + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; 351 361 INFOPLIST_KEY_NSHumanReadableCopyright = ""; 352 362 LD_RUNPATH_SEARCH_PATHS = ( 353 363 "$(inherited)", ··· 390 400 ENABLE_RESOURCE_ACCESS_USB = NO; 391 401 ENABLE_USER_SELECTED_FILES = readwrite; 392 402 GENERATE_INFOPLIST_FILE = YES; 403 + INFOPLIST_KEY_CFBundleDisplayName = Cull; 404 + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; 393 405 INFOPLIST_KEY_NSHumanReadableCopyright = ""; 394 406 LD_RUNPATH_SEARCH_PATHS = ( 395 407 "$(inherited)",
cull/icon.icon/Assets/C.png

This is a binary file and will not be displayed.

-16
cull/icon.icon/Assets/Subtract.svg
··· 1 - <svg width="835" height="533" viewBox="0 0 835 533" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <g filter="url(#filter0_d_122_24)"> 3 - <path d="M22.8826 43.1101C24.4795 17.7553 46.3285 -1.50434 71.6834 0.0925453L790.971 45.3943C816.326 46.9912 835.586 68.8402 833.989 94.1951L811.199 456.047C809.602 481.401 787.753 500.661 762.398 499.064L43.1101 453.762C17.7554 452.165 -1.50432 430.316 0.0925424 404.962L22.8826 43.1101ZM720.307 193.733C685.382 193.733 657.068 222.047 657.068 256.972C657.068 291.898 685.382 320.212 720.307 320.212C755.233 320.211 783.546 291.898 783.546 256.972C783.546 222.047 755.233 193.733 720.307 193.733Z" fill="#D6A071"/> 4 - </g> 5 - <defs> 6 - <filter id="filter0_d_122_24" x="0" y="0" width="834.082" height="532.157" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> 7 - <feFlood flood-opacity="0" result="BackgroundImageFix"/> 8 - <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> 9 - <feOffset dy="33"/> 10 - <feComposite in2="hardAlpha" operator="out"/> 11 - <feColorMatrix type="matrix" values="0 0 0 0 0.665584 0 0 0 0 0.47187 0 0 0 0 0.303268 0 0 0 1 0"/> 12 - <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_122_24"/> 13 - <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_122_24" result="shape"/> 14 - </filter> 15 - </defs> 16 - </svg>
-27
cull/icon.icon/Assets/emoji_u1f39e 1 (1).svg
··· 1 - <svg width="566" height="566" viewBox="0 0 566 566" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <g clip-path="url(#clip0_122_25)"> 3 - <g opacity="0.85"> 4 - <path d="M486.57 129.172L38.0752 211.201L80.4361 442.811L528.931 360.783L486.57 129.172Z" fill="#96A6E7"/> 5 - </g> 6 - <path d="M487.342 133.396L475.913 70.9055L27.418 152.934L38.8541 215.462L83.6288 207.272L124.445 430.436L79.6701 438.625L91.3523 502.498L539.847 420.47L528.158 356.559L483.383 364.748L442.567 141.585L487.342 133.396ZM441.427 107.263C440.805 103.862 443.065 100.59 446.466 99.9682L454.614 98.478C458.015 97.856 461.286 100.116 461.908 103.517L464.649 118.504C465.271 121.905 463.012 125.177 459.611 125.799L451.463 127.289C448.062 127.911 444.79 125.651 444.168 122.25L441.427 107.263ZM75.2812 189.718C75.9032 193.12 73.6434 196.391 70.2423 197.013L62.0946 198.503C58.6935 199.125 55.422 196.866 54.7999 193.464L52.0588 178.477C51.4367 175.076 53.6966 171.805 57.0977 171.183L65.2454 169.692C68.6465 169.07 71.918 171.33 72.5401 174.731L75.2812 189.718ZM125.585 464.758C126.207 468.159 123.947 471.431 120.546 472.053L112.399 473.543C108.997 474.165 105.726 471.905 105.104 468.504L102.356 453.479C101.734 450.078 103.994 446.807 107.395 446.185L115.542 444.694C118.944 444.072 122.215 446.332 122.837 449.733L125.585 464.758ZM491.724 382.265C491.102 378.864 493.362 375.592 496.763 374.97L504.911 373.48C508.312 372.858 511.583 375.118 512.205 378.519L514.947 393.506C515.569 396.907 513.309 400.179 509.908 400.801L501.76 402.291C498.359 402.913 495.087 400.653 494.465 397.252L491.724 382.265ZM398.147 115.179C397.525 111.778 399.785 108.506 403.186 107.884L411.334 106.394C414.735 105.772 418.007 108.032 418.629 111.433L421.37 126.42C421.992 129.821 419.732 133.093 416.331 133.715L408.183 135.205C404.782 135.827 401.51 133.567 400.888 130.166L398.147 115.179ZM354.905 123.088C354.283 119.686 356.543 116.415 359.944 115.793L368.091 114.303C371.493 113.681 374.764 115.941 375.386 119.342L378.127 134.329C378.749 137.73 376.489 141.001 373.088 141.623L364.941 143.114C361.54 143.736 358.268 141.476 357.646 138.075L354.905 123.088ZM311.625 131.003C311.003 127.602 313.263 124.331 316.664 123.709L324.812 122.218C328.213 121.596 331.484 123.856 332.106 127.257L334.848 142.245C335.47 145.646 333.21 148.917 329.809 149.539L321.661 151.029C318.26 151.651 314.988 149.392 314.366 145.99L311.625 131.003ZM268.383 138.912C267.761 135.511 270.021 132.24 273.422 131.618L281.569 130.127C284.97 129.505 288.242 131.765 288.864 135.166L291.605 150.153C292.227 153.555 289.967 156.826 286.566 157.448L278.419 158.938C275.018 159.56 271.746 157.3 271.124 153.899L268.383 138.912ZM225.103 146.828C224.481 143.427 226.741 140.155 230.142 139.533L238.29 138.043C241.691 137.421 244.962 139.681 245.584 143.082L248.325 158.069C248.947 161.47 246.688 164.742 243.287 165.364L235.139 166.854C231.738 167.476 228.466 165.216 227.844 161.815L225.103 146.828ZM181.861 154.737C181.239 151.336 183.499 148.064 186.9 147.442L195.047 145.952C198.448 145.33 201.72 147.59 202.342 150.991L205.083 165.978C205.705 169.379 203.445 172.651 200.044 173.273L191.896 174.763C188.495 175.385 185.224 173.125 184.602 169.724L181.861 154.737ZM138.581 162.653C137.959 159.252 140.219 155.98 143.62 155.358L151.768 153.868C155.169 153.246 158.44 155.506 159.062 158.907L161.803 173.894C162.425 177.295 160.165 180.566 156.764 181.189L148.617 182.679C145.216 183.301 141.944 181.041 141.322 177.64L138.581 162.653ZM95.3385 170.562C94.7165 167.16 96.9764 163.889 100.377 163.267L108.525 161.777C111.926 161.155 115.198 163.414 115.82 166.816L118.561 181.803C119.183 185.204 116.923 188.475 113.522 189.097L105.374 190.588C101.973 191.21 98.7017 188.95 98.0797 185.549L95.3385 170.562ZM168.865 456.842C169.487 460.243 167.227 463.515 163.826 464.137L155.678 465.627C152.277 466.249 149.006 463.989 148.384 460.588L145.636 445.564C145.014 442.163 147.273 438.891 150.675 438.269L158.822 436.779C162.223 436.157 165.495 438.417 166.117 441.818L168.865 456.842ZM212.107 448.933C212.729 452.334 210.469 455.606 207.068 456.228L198.921 457.718C195.52 458.34 192.248 456.08 191.626 452.679L188.878 437.655C188.256 434.254 190.516 430.982 193.917 430.36L202.065 428.87C205.466 428.248 208.737 430.508 209.359 433.909L212.107 448.933ZM255.387 441.018C256.009 444.419 253.749 447.69 250.348 448.312L242.2 449.802C238.799 450.424 235.528 448.165 234.906 444.764L232.158 429.739C231.536 426.338 233.796 423.066 237.197 422.444L245.344 420.954C248.745 420.332 252.017 422.592 252.639 425.993L255.387 441.018ZM298.629 433.109C299.251 436.51 296.992 439.781 293.59 440.403L285.443 441.894C282.042 442.516 278.77 440.256 278.148 436.855L275.4 421.83C274.778 418.429 277.038 415.157 280.439 414.535L288.587 413.045C291.988 412.423 295.259 414.683 295.881 418.084L298.629 433.109ZM341.909 425.193C342.531 428.594 340.271 431.866 336.87 432.488L328.723 433.978C325.321 434.6 322.05 432.34 321.428 428.939L318.68 413.914C318.058 410.513 320.318 407.242 323.719 406.62L331.866 405.129C335.268 404.507 338.539 406.767 339.161 410.168L341.909 425.193ZM385.151 417.284C385.774 420.685 383.514 423.957 380.113 424.579L371.965 426.069C368.564 426.691 365.292 424.431 364.67 421.03L361.922 406.005C361.3 402.604 363.56 399.333 366.961 398.711L375.109 397.221C378.51 396.598 381.781 398.858 382.404 402.259L385.151 417.284ZM428.431 409.368C429.053 412.769 426.793 416.041 423.392 416.663L415.245 418.153C411.844 418.775 408.572 416.515 407.95 413.114L405.202 398.09C404.58 394.689 406.84 391.417 410.241 390.795L418.389 389.305C421.79 388.683 425.061 390.943 425.683 394.344L428.431 409.368ZM471.674 401.459C472.296 404.86 470.036 408.132 466.635 408.754L458.487 410.244C455.086 410.866 451.814 408.606 451.192 405.205L448.444 390.181C447.822 386.78 450.082 383.508 453.483 382.886L461.631 381.396C465.032 380.774 468.304 383.034 468.926 386.435L471.674 401.459ZM468.508 367.469L139.313 427.678L98.4971 204.514L427.692 144.306L468.508 367.469Z" fill="url(#paint0_linear_122_25)"/> 7 - <path d="M29.8848 152.483L475.913 70.9054" stroke="#757575" stroke-width="0.9587" stroke-miterlimit="10"/> 8 - <path d="M429.056 144.79L99.8604 204.999L140.676 428.163L469.872 367.954L429.056 144.79Z" fill="url(#paint1_linear_122_25)"/> 9 - </g> 10 - <defs> 11 - <linearGradient id="paint0_linear_122_25" x1="121.991" y1="370.17" x2="365.536" y2="244.378" gradientUnits="userSpaceOnUse"> 12 - <stop stop-color="#424242"/> 13 - <stop offset="0.3935" stop-color="#44545B"/> 14 - <stop offset="0.5171" stop-color="#455A64"/> 15 - <stop offset="0.7406" stop-color="#445157"/> 16 - <stop offset="1" stop-color="#424242"/> 17 - </linearGradient> 18 - <linearGradient id="paint1_linear_122_25" x1="156.247" y1="371.236" x2="366.492" y2="232.656" gradientUnits="userSpaceOnUse"> 19 - <stop stop-color="#FFCC80" stop-opacity="0"/> 20 - <stop offset="0.4864" stop-color="#FFCC80" stop-opacity="0.18"/> 21 - <stop offset="1" stop-color="#FFCC80" stop-opacity="0"/> 22 - </linearGradient> 23 - <clipPath id="clip0_122_25"> 24 - <rect width="486.33" height="486.33" fill="white" transform="translate(0 87.497) rotate(-10.3647)"/> 25 - </clipPath> 26 - </defs> 27 - </svg>
-16
cull/icon.icon/Assets/emoji_u1f52a 1.svg
··· 1 - <svg width="820" height="820" viewBox="0 0 820 820" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <g clip-path="url(#clip0_122_12)"> 3 - <path d="M320.499 315.776L294.225 380.552C294.225 380.552 300.291 382.13 311.317 387.834C322.327 393.489 331.293 406.589 331.293 406.589C331.293 406.589 404.813 375.911 404.346 374.457C403.879 373.004 363.644 333.447 363.644 333.447L324.148 314.657L320.499 315.776Z" fill="#B0B0B0"/> 4 - <path d="M331.574 468.177L313.754 477.591C313.754 477.591 302.605 495.123 307.106 501.48C310.941 506.929 372.23 538.652 413.494 556.125C463.664 577.365 557.392 607.911 638.819 615.313C715.792 622.328 760.863 613.725 776.857 607.944C791.317 602.71 794.382 591.624 787.899 589.912C781.417 588.2 567.252 510.516 567.252 510.516L331.574 468.177Z" fill="#B0B0B0"/> 5 - <path d="M363.58 333.414L327.252 400.833C327.252 400.833 336.375 415.753 330.631 434.969C324.871 454.136 313.558 477.814 313.558 477.814C313.558 477.814 390.042 524.057 472.368 552.388C588.666 592.464 666.028 603.684 716.363 602.315C762.884 601.05 789.74 590.817 789.74 590.817C789.74 590.817 723.313 537.281 656.88 494.704C614.626 467.661 486.277 394.738 445.389 373.777C404.549 352.8 363.58 333.414 363.58 333.414Z" fill="#E0E0E0"/> 6 - <path d="M70.8147 250.306L54.1888 243.142C54.1888 243.142 39.0151 280.086 48.0988 292.72C56.0096 303.7 124.604 329.549 186.359 353.907C236.289 373.567 276.274 390.222 285.52 389.389C296.393 388.461 300.063 377.928 300.063 377.928L70.8147 250.306Z" fill="#474C4F"/> 7 - <path d="M91.3188 195.187C76.6937 196.413 56.5963 230.342 51.5334 252.012C46.4705 273.681 60.913 281.868 80.9333 290.614C90.1909 294.641 299.848 378.424 299.848 378.424C299.848 378.424 311.171 358.271 317.468 345.932C324.935 331.08 333.705 316.29 332.032 311.75C329.483 304.979 232.646 261.428 195.766 241.798C152.262 218.684 103.978 194.166 91.3188 195.187Z" fill="#5E6367"/> 8 - <path d="M112.807 236.279C107.459 233.775 101.064 235.83 96.7315 242.139C92.447 248.433 94.4512 257.997 102.048 260.848C109.645 263.698 117.754 259.328 119.947 253.012C122.124 246.647 119.035 239.195 112.807 236.279Z" fill="#B0B0B0"/> 9 - <path d="M287.268 317.314C279.96 314.532 273.274 319.673 271.524 325.206C269.759 330.69 271.589 338.547 278.014 341.079C284.44 343.611 291.33 338.938 293.495 333.7C296.04 327.483 293.294 319.601 287.268 317.314Z" fill="#B0B0B0"/> 10 - </g> 11 - <defs> 12 - <clipPath id="clip0_122_12"> 13 - <rect width="651.329" height="651.329" fill="white" transform="translate(0 199.249) rotate(-17.813)"/> 14 - </clipPath> 15 - </defs> 16 - </svg>
+21 -35
cull/icon.icon/icon.json
··· 1 1 { 2 2 "fill-specializations" : [ 3 3 { 4 - "value" : { 5 - "solid" : "display-p3:0.12880,0.13019,0.13434,1.00000" 6 - } 4 + "value" : "system-dark" 7 5 }, 8 6 { 9 7 "appearance" : "dark", ··· 16 14 { 17 15 "layers" : [ 18 16 { 19 - "blend-mode" : "normal", 20 - "fill" : "none", 21 - "glass" : true, 22 - "hidden" : false, 23 - "image-name" : "emoji_u1f52a 1.svg", 24 - "name" : "emoji_u1f52a 1", 25 - "opacity" : 1, 17 + "fill" : { 18 + "linear-gradient" : [ 19 + "display-p3:1.00000,0.12046,0.22478,1.00000", 20 + "display-p3:0.50511,0.67153,0.96783,1.00000" 21 + ], 22 + "orientation" : { 23 + "start" : { 24 + "x" : 0.5, 25 + "y" : 0 26 + }, 27 + "stop" : { 28 + "x" : 0.5652511760752689, 29 + "y" : 0.8858258928571429 30 + } 31 + } 32 + }, 33 + "image-name" : "C.png", 34 + "name" : "C", 26 35 "position" : { 27 36 "scale" : 1, 28 37 "translation-in-points" : [ 29 - -97.0625, 30 - -105.04424700203003 31 - ] 32 - } 33 - }, 34 - { 35 - "image-name" : "emoji_u1f39e 1 (1).svg", 36 - "name" : "emoji_u1f39e 1 (1)", 37 - "position" : { 38 - "scale" : 1.23, 39 - "translation-in-points" : [ 40 - -0.7779750000000831, 41 - 23.765625 42 - ] 43 - } 44 - }, 45 - { 46 - "glass" : true, 47 - "image-name" : "Subtract.svg", 48 - "name" : "Subtract", 49 - "position" : { 50 - "scale" : 1.07, 51 - "translation-in-points" : [ 52 - 0.4913672408982279, 53 - 106.2578125 38 + 0, 39 + 0 54 40 ] 55 41 } 56 42 }